summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Makefile261
-rw-r--r--README60
-rw-r--r--VERSION55
-rw-r--r--contrib/RHEL4/Makefile.am13
-rw-r--r--contrib/RHEL4/configure.ac55
-rw-r--r--contrib/RHEL4/ipa-client-setup368
-rw-r--r--contrib/RHEL4/ipa-client.spec54
-rw-r--r--contrib/RHEL4/ipa.conf3
-rw-r--r--contrib/RHEL4/ipachangeconf.py459
-rw-r--r--contrib/RHEL4/setup.py75
-rw-r--r--ipa-admintools/Makefile47
-rw-r--r--ipa-admintools/README13
-rw-r--r--ipa-admintools/ipa-adddelegation201
-rw-r--r--ipa-admintools/ipa-addgroup162
-rw-r--r--ipa-admintools/ipa-addservice104
-rw-r--r--ipa-admintools/ipa-adduser290
-rw-r--r--ipa-admintools/ipa-admintools.spec.in87
-rw-r--r--ipa-admintools/ipa-change-master-key387
-rw-r--r--ipa-admintools/ipa-defaultoptions181
-rw-r--r--ipa-admintools/ipa-deldelegation143
-rw-r--r--ipa-admintools/ipa-delgroup109
-rw-r--r--ipa-admintools/ipa-delservice110
-rw-r--r--ipa-admintools/ipa-deluser90
-rw-r--r--ipa-admintools/ipa-findgroup157
-rw-r--r--ipa-admintools/ipa-findservice105
-rw-r--r--ipa-admintools/ipa-finduser177
-rw-r--r--ipa-admintools/ipa-listdelegation140
-rw-r--r--ipa-admintools/ipa-lockuser109
-rw-r--r--ipa-admintools/ipa-moddelegation241
-rw-r--r--ipa-admintools/ipa-modgroup234
-rw-r--r--ipa-admintools/ipa-moduser256
-rw-r--r--ipa-admintools/ipa-passwd146
-rw-r--r--ipa-admintools/ipa-pwpolicy153
-rw-r--r--ipa-admintools/man/Makefile32
-rw-r--r--ipa-admintools/man/ipa-adddelegation.162
-rw-r--r--ipa-admintools/man/ipa-addgroup.151
-rw-r--r--ipa-admintools/man/ipa-addservice.159
-rw-r--r--ipa-admintools/man/ipa-adduser.184
-rw-r--r--ipa-admintools/man/ipa-defaultoptions.165
-rw-r--r--ipa-admintools/man/ipa-deldelegation.139
-rw-r--r--ipa-admintools/man/ipa-delgroup.137
-rw-r--r--ipa-admintools/man/ipa-delservice.138
-rw-r--r--ipa-admintools/man/ipa-deluser.135
-rw-r--r--ipa-admintools/man/ipa-findgroup.141
-rw-r--r--ipa-admintools/man/ipa-findservice.148
-rw-r--r--ipa-admintools/man/ipa-finduser.148
-rw-r--r--ipa-admintools/man/ipa-listdelegation.137
-rw-r--r--ipa-admintools/man/ipa-lockuser.138
-rw-r--r--ipa-admintools/man/ipa-moddelegation.150
-rw-r--r--ipa-admintools/man/ipa-modgroup.156
-rw-r--r--ipa-admintools/man/ipa-moduser.156
-rw-r--r--ipa-admintools/man/ipa-passwd.134
-rw-r--r--ipa-admintools/man/ipa-pwpolicy.154
-rw-r--r--ipa-client/AUTHORS0
-rw-r--r--ipa-client/Makefile.am100
-rw-r--r--ipa-client/NEWS0
-rw-r--r--ipa-client/README24
-rwxr-xr-xipa-client/autogen.sh196
-rw-r--r--ipa-client/configure.ac195
-rw-r--r--ipa-client/firefox/Makefile.am18
-rw-r--r--ipa-client/firefox/README23
-rw-r--r--ipa-client/firefox/globalsetup.sh52
-rw-r--r--ipa-client/firefox/ipa.cfg19
-rw-r--r--ipa-client/firefox/ipa.js34
-rw-r--r--ipa-client/firefox/usersetup.sh40
-rw-r--r--ipa-client/ipa-client.spec.in86
-rw-r--r--ipa-client/ipa-getkeytab.c836
-rw-r--r--ipa-client/ipa-install/Makefile.am13
-rw-r--r--ipa-client/ipa-install/ipa-client-install387
-rw-r--r--ipa-client/ipaclient/Makefile.am16
-rw-r--r--ipa-client/ipaclient/__init__.py21
-rw-r--r--ipa-client/ipaclient/ipachangeconf.py459
-rw-r--r--ipa-client/ipaclient/ipadiscovery.py248
-rw-r--r--ipa-client/ipaclient/ntpconf.py111
-rw-r--r--ipa-client/man/Makefile.am16
-rw-r--r--ipa-client/man/ipa-client-install.155
-rw-r--r--ipa-client/man/ipa-getkeytab.1101
-rw-r--r--ipa-client/version.m4.in1
-rw-r--r--ipa-python/MANIFEST.in3
-rw-r--r--ipa-python/Makefile28
-rw-r--r--ipa-python/README30
-rw-r--r--ipa-python/__init__.py0
-rw-r--r--ipa-python/aci.py166
-rw-r--r--ipa-python/config.py183
-rw-r--r--ipa-python/dnsclient.py443
-rw-r--r--ipa-python/entity.py202
-rw-r--r--ipa-python/group.py24
-rwxr-xr-xipa-python/ipa-python.spec.in82
-rw-r--r--ipa-python/ipa.conf3
-rw-r--r--ipa-python/ipaadminutil.py96
-rw-r--r--ipa-python/ipaclient.py471
-rw-r--r--ipa-python/ipaerror.py259
-rw-r--r--ipa-python/ipautil.py989
-rw-r--r--ipa-python/ipavalidate.py137
-rw-r--r--ipa-python/krbtransport.py51
-rw-r--r--ipa-python/radius_util.py366
-rw-r--r--ipa-python/rpcclient.py906
-rw-r--r--ipa-python/setup.py.in77
-rw-r--r--ipa-python/sysrestore.py317
-rw-r--r--ipa-python/test/test_aci.py127
-rw-r--r--ipa-python/test/test_ipautil.py309
-rw-r--r--ipa-python/test/test_ipavalidate.py97
-rw-r--r--ipa-python/user.py24
-rw-r--r--ipa-python/version.py.in25
-rw-r--r--ipa-radius-admintools/Makefile24
-rw-r--r--ipa-radius-admintools/ipa-addradiusclient197
-rw-r--r--ipa-radius-admintools/ipa-addradiusprofile196
-rw-r--r--ipa-radius-admintools/ipa-delradiusclient79
-rw-r--r--ipa-radius-admintools/ipa-delradiusprofile87
-rw-r--r--ipa-radius-admintools/ipa-findradiusclient106
-rw-r--r--ipa-radius-admintools/ipa-findradiusprofile109
-rw-r--r--ipa-radius-admintools/ipa-modradiusclient275
-rw-r--r--ipa-radius-admintools/ipa-modradiusprofile265
-rw-r--r--ipa-radius-admintools/ipa-radius-admintools.spec.in53
-rw-r--r--ipa-radius-server/Makefile23
-rw-r--r--ipa-radius-server/ipa-radius-install71
-rwxr-xr-xipa-radius-server/ipa-radius-server.spec.in61
-rw-r--r--ipa-radius-server/plugins/__init__.py1
-rw-r--r--ipa-radius-server/plugins/radiusinstance.py170
-rw-r--r--ipa-radius-server/share/radius.radiusd.conf.template285
-rw-r--r--ipa-server/AUTHORS0
-rw-r--r--ipa-server/Makefile.am91
-rw-r--r--ipa-server/NEWS0
-rw-r--r--ipa-server/README20
-rwxr-xr-xipa-server/autogen.sh196
-rw-r--r--ipa-server/configure.ac323
-rwxr-xr-xipa-server/ipa-compat-manage171
-rw-r--r--ipa-server/ipa-fix-CVE-2008-3274524
-rw-r--r--ipa-server/ipa-gui/Makefile.am66
-rw-r--r--ipa-server/ipa-gui/README.i18n10
-rw-r--r--ipa-server/ipa-gui/README.multivalue27
-rw-r--r--ipa-server/ipa-gui/README.txt4
-rw-r--r--ipa-server/ipa-gui/dev.cfg73
-rw-r--r--ipa-server/ipa-gui/i18n.patch16
-rw-r--r--ipa-server/ipa-gui/ipa_gui.egg-info/Makefile.am22
-rw-r--r--ipa-server/ipa-gui/ipa_gui.egg-info/PKG-INFO15
-rw-r--r--ipa-server/ipa-gui/ipa_gui.egg-info/SOURCES.txt30
-rw-r--r--ipa-server/ipa-gui/ipa_gui.egg-info/dependency_links.txt1
-rw-r--r--ipa-server/ipa-gui/ipa_gui.egg-info/entry_points.txt6
-rw-r--r--ipa-server/ipa-gui/ipa_gui.egg-info/not-zip-safe1
-rw-r--r--ipa-server/ipa-gui/ipa_gui.egg-info/paster_plugins.txt2
-rw-r--r--ipa-server/ipa-gui/ipa_gui.egg-info/requires.txt1
-rw-r--r--ipa-server/ipa-gui/ipa_gui.egg-info/sqlobject.txt2
-rw-r--r--ipa-server/ipa-gui/ipa_gui.egg-info/top_level.txt1
-rw-r--r--ipa-server/ipa-gui/ipa_webgui207
-rw-r--r--ipa-server/ipa-gui/ipa_webgui.cfg109
-rw-r--r--ipa-server/ipa-gui/ipa_webgui.init79
-rw-r--r--ipa-server/ipa-gui/ipagui/Makefile.am30
-rw-r--r--ipa-server/ipa-gui/ipagui/__init__.py0
-rw-r--r--ipa-server/ipa-gui/ipagui/config/Makefile.am20
-rw-r--r--ipa-server/ipa-gui/ipagui/config/__init__.py0
-rw-r--r--ipa-server/ipa-gui/ipagui/config/app.cfg104
-rw-r--r--ipa-server/ipa-gui/ipagui/config/log.cfg32
-rw-r--r--ipa-server/ipa-gui/ipagui/controllers.py135
-rw-r--r--ipa-server/ipa-gui/ipagui/forms/Makefile.am19
-rw-r--r--ipa-server/ipa-gui/ipagui/forms/__init__.py0
-rw-r--r--ipa-server/ipa-gui/ipagui/forms/delegate.py110
-rw-r--r--ipa-server/ipa-gui/ipagui/forms/group.py89
-rw-r--r--ipa-server/ipa-gui/ipagui/forms/ipapolicy.py87
-rw-r--r--ipa-server/ipa-gui/ipagui/forms/principal.py55
-rw-r--r--ipa-server/ipa-gui/ipagui/forms/user.py207
-rw-r--r--ipa-server/ipa-gui/ipagui/helpers/Makefile.am17
-rw-r--r--ipa-server/ipa-gui/ipagui/helpers/__init__.py1
-rw-r--r--ipa-server/ipa-gui/ipagui/helpers/ipahelper.py88
-rw-r--r--ipa-server/ipa-gui/ipagui/helpers/userhelper.py46
-rw-r--r--ipa-server/ipa-gui/ipagui/helpers/validators.py92
-rw-r--r--ipa-server/ipa-gui/ipagui/json.py27
-rw-r--r--ipa-server/ipa-gui/ipagui/model.py26
-rw-r--r--ipa-server/ipa-gui/ipagui/proxyprovider.py176
-rw-r--r--ipa-server/ipa-gui/ipagui/proxyvisit.py42
-rw-r--r--ipa-server/ipa-gui/ipagui/release.py16
-rw-r--r--ipa-server/ipa-gui/ipagui/static/Makefile.am12
-rw-r--r--ipa-server/ipa-gui/ipagui/static/css/Makefile.am17
-rw-r--r--ipa-server/ipa-gui/ipagui/static/css/style_freeipa.css62
-rw-r--r--ipa-server/ipa-gui/ipagui/static/css/style_platform-objects.css19
-rw-r--r--ipa-server/ipa-gui/ipagui/static/css/style_platform.css517
-rw-r--r--ipa-server/ipa-gui/ipagui/static/images/Makefile.am29
-rw-r--r--ipa-server/ipa-gui/ipagui/static/images/branding/Makefile.am15
-rw-r--r--ipa-server/ipa-gui/ipagui/static/images/branding/logo.pngbin0 -> 8560 bytes
-rw-r--r--ipa-server/ipa-gui/ipagui/static/images/down.gifbin0 -> 57 bytes
-rw-r--r--ipa-server/ipa-gui/ipagui/static/images/favicon.icobin0 -> 3638 bytes
-rw-r--r--ipa-server/ipa-gui/ipagui/static/images/header_inner.pngbin0 -> 37537 bytes
-rw-r--r--ipa-server/ipa-gui/ipagui/static/images/info.pngbin0 -> 2889 bytes
-rw-r--r--ipa-server/ipa-gui/ipagui/static/images/logo.pngbin0 -> 8560 bytes
-rw-r--r--ipa-server/ipa-gui/ipagui/static/images/objects/Makefile.am25
-rw-r--r--ipa-server/ipa-gui/ipagui/static/images/objects/object-accesscontrol.pngbin0 -> 2093 bytes
-rw-r--r--ipa-server/ipa-gui/ipagui/static/images/objects/object-channel.pngbin0 -> 702 bytes
-rw-r--r--ipa-server/ipa-gui/ipagui/static/images/objects/object-channels.pngbin0 -> 702 bytes
-rw-r--r--ipa-server/ipa-gui/ipagui/static/images/objects/object-content.pngbin0 -> 1735 bytes
-rw-r--r--ipa-server/ipa-gui/ipagui/static/images/objects/object-media.pngbin0 -> 1569 bytes
-rw-r--r--ipa-server/ipa-gui/ipagui/static/images/objects/object-overview.pngbin0 -> 1101 bytes
-rw-r--r--ipa-server/ipa-gui/ipagui/static/images/objects/object-policy.pngbin0 -> 1471 bytes
-rw-r--r--ipa-server/ipa-gui/ipagui/static/images/objects/object-system.pngbin0 -> 1591 bytes
-rw-r--r--ipa-server/ipa-gui/ipagui/static/images/objects/object-user.pngbin0 -> 1005 bytes
-rw-r--r--ipa-server/ipa-gui/ipagui/static/images/objects/object-usergroup.pngbin0 -> 1349 bytes
-rw-r--r--ipa-server/ipa-gui/ipagui/static/images/objects/object-virtualsystem.pngbin0 -> 2089 bytes
-rw-r--r--ipa-server/ipa-gui/ipagui/static/images/ok.pngbin0 -> 25753 bytes
-rw-r--r--ipa-server/ipa-gui/ipagui/static/images/template/Makefile.am21
-rw-r--r--ipa-server/ipa-gui/ipagui/static/images/template/background-content.pngbin0 -> 194 bytes
-rw-r--r--ipa-server/ipa-gui/ipagui/static/images/template/background-navbar-active.pngbin0 -> 610 bytes
-rw-r--r--ipa-server/ipa-gui/ipagui/static/images/template/background-navbar-active_fullsize.pngbin0 -> 634 bytes
-rw-r--r--ipa-server/ipa-gui/ipagui/static/images/template/background-navbar.pngbin0 -> 253 bytes
-rw-r--r--ipa-server/ipa-gui/ipagui/static/images/template/background-navbar_fullsize.pngbin0 -> 251 bytes
-rw-r--r--ipa-server/ipa-gui/ipagui/static/images/template/background-sidebar.pngbin0 -> 170 bytes
-rw-r--r--ipa-server/ipa-gui/ipagui/static/images/template/background.pngbin0 -> 184 bytes
-rw-r--r--ipa-server/ipa-gui/ipagui/static/images/tg_under_the_hood.pngbin0 -> 4010 bytes
-rw-r--r--ipa-server/ipa-gui/ipagui/static/images/under_the_hood_blue.pngbin0 -> 2667 bytes
-rw-r--r--ipa-server/ipa-gui/ipagui/static/images/up.gifbin0 -> 56 bytes
-rw-r--r--ipa-server/ipa-gui/ipagui/static/javascript/Makefile.am21
-rw-r--r--ipa-server/ipa-gui/ipagui/static/javascript/dynamicedit.js217
-rw-r--r--ipa-server/ipa-gui/ipagui/static/javascript/dynamicselect.js70
-rw-r--r--ipa-server/ipa-gui/ipagui/static/javascript/effects.js1094
-rw-r--r--ipa-server/ipa-gui/ipagui/static/javascript/ipautil.js24
-rw-r--r--ipa-server/ipa-gui/ipagui/static/javascript/prototype.js3277
-rw-r--r--ipa-server/ipa-gui/ipagui/static/javascript/scriptaculous.js58
-rw-r--r--ipa-server/ipa-gui/ipagui/static/javascript/tablekit.js848
-rw-r--r--ipa-server/ipa-gui/ipagui/subcontrollers/Makefile.am21
-rw-r--r--ipa-server/ipa-gui/ipagui/subcontrollers/__init__.py1
-rw-r--r--ipa-server/ipa-gui/ipagui/subcontrollers/delegation.py415
-rw-r--r--ipa-server/ipa-gui/ipagui/subcontrollers/group.py484
-rw-r--r--ipa-server/ipa-gui/ipagui/subcontrollers/ipacontroller.py92
-rw-r--r--ipa-server/ipa-gui/ipagui/subcontrollers/ipapolicy.py208
-rw-r--r--ipa-server/ipa-gui/ipagui/subcontrollers/policy.py49
-rw-r--r--ipa-server/ipa-gui/ipagui/subcontrollers/principal.py193
-rw-r--r--ipa-server/ipa-gui/ipagui/subcontrollers/user.py854
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/Makefile.am55
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/__init__.py0
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/delegateedit.kid33
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/delegateform.kid213
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/delegategroupsearch.kid48
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/delegatelayout.kid34
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/delegatelist.kid93
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/delegatenew.kid31
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/dynamiceditsearch.kid97
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/groupedit.kid36
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/groupeditform.kid289
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/grouplayout.kid40
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/grouplist.kid93
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/groupnew.kid32
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/groupnewform.kid149
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/groupshow.kid131
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/ipapolicyedit.kid32
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/ipapolicyeditform.kid280
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/ipapolicyshow.kid188
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/loginfailed.kid41
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/master.kid121
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/not_found.kid37
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/policyindex.kid48
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/policylayout.kid34
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/principallayout.kid36
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/principallist.kid82
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/principalnew.kid30
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/principalnewform.kid119
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/principalshow.kid70
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/unhandled_exception.kid48
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/useredit.kid57
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/usereditform.kid949
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/userlayout.kid41
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/userlist.kid118
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/usernew.kid30
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/usernewform.kid842
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/userselectsearch.kid53
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/usershow.kid399
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/welcome.kid53
-rw-r--r--ipa-server/ipa-gui/ipagui/tests/Makefile.am16
-rw-r--r--ipa-server/ipa-gui/ipagui/tests/__init__.py0
-rw-r--r--ipa-server/ipa-gui/ipagui/tests/test_controllers.py49
-rw-r--r--ipa-server/ipa-gui/ipagui/tests/test_model.py39
-rw-r--r--ipa-server/ipa-gui/locales/ja/LC_MESSAGES/messages.po757
-rw-r--r--ipa-server/ipa-gui/locales/messages.pot782
-rw-r--r--ipa-server/ipa-gui/sample-prod.cfg77
-rw-r--r--ipa-server/ipa-gui/setup.py65
-rw-r--r--ipa-server/ipa-gui/start-ipagui.py0
-rw-r--r--ipa-server/ipa-gui/test.cfg4
-rw-r--r--ipa-server/ipa-install/Makefile.am24
-rw-r--r--ipa-server/ipa-install/README67
-rw-r--r--ipa-server/ipa-install/ipa-replica-install312
-rwxr-xr-xipa-server/ipa-install/ipa-replica-manage218
-rw-r--r--ipa-server/ipa-install/ipa-replica-prepare294
-rw-r--r--ipa-server/ipa-install/ipa-server-certinstall157
-rw-r--r--ipa-server/ipa-install/ipa-server-install622
-rw-r--r--ipa-server/ipa-install/ipactl57
-rw-r--r--ipa-server/ipa-install/share/60ipaconfig.ldif42
-rw-r--r--ipa-server/ipa-install/share/60kerberos.ldif283
-rw-r--r--ipa-server/ipa-install/share/60radius.ldif559
-rw-r--r--ipa-server/ipa-install/share/60samba.ldif152
-rw-r--r--ipa-server/ipa-install/share/Makefile.am39
-rw-r--r--ipa-server/ipa-install/share/bind.named.conf.template41
-rw-r--r--ipa-server/ipa-install/share/bind.zone.db.template28
-rw-r--r--ipa-server/ipa-install/share/bootstrap-template.ldif202
-rw-r--r--ipa-server/ipa-install/share/certmap.conf.template82
-rw-r--r--ipa-server/ipa-install/share/default-aci.ldif38
-rw-r--r--ipa-server/ipa-install/share/default-keytypes.ldif25
-rw-r--r--ipa-server/ipa-install/share/dna-posix.ldif39
-rw-r--r--ipa-server/ipa-install/share/encrypted_attribute.ldif6
-rw-r--r--ipa-server/ipa-install/share/fedora-ds.init.patch12
-rw-r--r--ipa-server/ipa-install/share/indices.ldif93
-rw-r--r--ipa-server/ipa-install/share/kdc.conf.template14
-rw-r--r--ipa-server/ipa-install/share/kerberos.ldif16
-rw-r--r--ipa-server/ipa-install/share/krb.con.template2
-rw-r--r--ipa-server/ipa-install/share/krb5.conf.template42
-rw-r--r--ipa-server/ipa-install/share/krb5.ini.template19
-rw-r--r--ipa-server/ipa-install/share/krbrealm.con.template3
-rw-r--r--ipa-server/ipa-install/share/master-entry.ldif7
-rw-r--r--ipa-server/ipa-install/share/memberof-task.ldif8
-rw-r--r--ipa-server/ipa-install/share/ntp.conf.server.template50
-rw-r--r--ipa-server/ipa-install/share/ntpd.sysconfig.template8
-rw-r--r--ipa-server/ipa-install/share/preferences.html.template33
-rw-r--r--ipa-server/ipa-install/share/referint-conf.ldif11
-rw-r--r--ipa-server/ipa-install/share/schema_compat.uldif50
-rw-r--r--ipa-server/ipa-install/share/unique-attributes.ldif35
-rw-r--r--ipa-server/ipa-install/updates/Makefile.am19
-rw-r--r--ipa-server/ipa-install/updates/RFC2307bis.update65
-rw-r--r--ipa-server/ipa-install/updates/RFC4876.update146
-rw-r--r--ipa-server/ipa-install/updates/indices.update18
-rw-r--r--ipa-server/ipa-install/updates/nss_ldap.update33
-rw-r--r--ipa-server/ipa-install/updates/replication.update9
-rw-r--r--ipa-server/ipa-install/updates/winsync_index.update10
-rw-r--r--ipa-server/ipa-kpasswd/Makefile.am58
-rw-r--r--ipa-server/ipa-kpasswd/README2
-rw-r--r--ipa-server/ipa-kpasswd/ipa_kpasswd.c1388
-rw-r--r--ipa-server/ipa-kpasswd/ipa_kpasswd.init83
-rwxr-xr-xipa-server/ipa-ldap-updater126
-rw-r--r--ipa-server/ipa-server.spec.in290
-rw-r--r--ipa-server/ipa-slapi-plugins/Makefile.am16
-rw-r--r--ipa-server/ipa-slapi-plugins/README0
-rw-r--r--ipa-server/ipa-slapi-plugins/dna/Makefile.am42
-rw-r--r--ipa-server/ipa-slapi-plugins/dna/dna-conf.ldif14
-rw-r--r--ipa-server/ipa-slapi-plugins/dna/dna.c1462
-rw-r--r--ipa-server/ipa-slapi-plugins/ipa-memberof/Makefile.am43
-rw-r--r--ipa-server/ipa-slapi-plugins/ipa-memberof/ipa-memberof.c2244
-rw-r--r--ipa-server/ipa-slapi-plugins/ipa-memberof/ipa-memberof.h100
-rw-r--r--ipa-server/ipa-slapi-plugins/ipa-memberof/ipa-memberof_config.c312
-rw-r--r--ipa-server/ipa-slapi-plugins/ipa-memberof/memberof-conf.ldif14
-rw-r--r--ipa-server/ipa-slapi-plugins/ipa-pwd-extop/Makefile.am46
-rw-r--r--ipa-server/ipa-slapi-plugins/ipa-pwd-extop/README0
-rw-r--r--ipa-server/ipa-slapi-plugins/ipa-pwd-extop/ipa_pwd_extop.c4058
-rw-r--r--ipa-server/ipa-slapi-plugins/ipa-pwd-extop/pwd-extop-conf.ldif16
-rw-r--r--ipa-server/ipa-slapi-plugins/ipa-winsync/Makefile.am43
-rw-r--r--ipa-server/ipa-slapi-plugins/ipa-winsync/README0
-rw-r--r--ipa-server/ipa-slapi-plugins/ipa-winsync/ipa-winsync-conf.ldif27
-rw-r--r--ipa-server/ipa-slapi-plugins/ipa-winsync/ipa-winsync-config.c975
-rw-r--r--ipa-server/ipa-slapi-plugins/ipa-winsync/ipa-winsync.c1177
-rw-r--r--ipa-server/ipa-slapi-plugins/ipa-winsync/ipa-winsync.h160
-rw-r--r--ipa-server/ipa-upgradeconfig130
-rw-r--r--ipa-server/ipaserver/Makefile.am24
-rw-r--r--ipa-server/ipaserver/__init__.py21
-rw-r--r--ipa-server/ipaserver/bindinstance.py156
-rw-r--r--ipa-server/ipaserver/certs.py424
-rw-r--r--ipa-server/ipaserver/dsinstance.py479
-rw-r--r--ipa-server/ipaserver/httpinstance.py231
-rw-r--r--ipa-server/ipaserver/installutils.py248
-rw-r--r--ipa-server/ipaserver/ipaldap.py701
-rw-r--r--ipa-server/ipaserver/krbinstance.py428
-rwxr-xr-xipa-server/ipaserver/ldapupdate.py593
-rw-r--r--ipa-server/ipaserver/ntpinstance.py107
-rw-r--r--ipa-server/ipaserver/replication.py532
-rw-r--r--ipa-server/ipaserver/service.py169
-rw-r--r--ipa-server/man/Makefile.am27
-rw-r--r--ipa-server/man/ipa-compat-manage.145
-rw-r--r--ipa-server/man/ipa-ldap-updater.178
-rw-r--r--ipa-server/man/ipa-replica-install.141
-rw-r--r--ipa-server/man/ipa-replica-manage.170
-rw-r--r--ipa-server/man/ipa-replica-prepare.148
-rw-r--r--ipa-server/man/ipa-server-certinstall.148
-rw-r--r--ipa-server/man/ipa-server-install.181
-rw-r--r--ipa-server/man/ipa_kpasswd.836
-rw-r--r--ipa-server/man/ipa_webgui.837
-rw-r--r--ipa-server/man/ipactl.837
-rw-r--r--ipa-server/selinux/Makefile28
-rw-r--r--ipa-server/selinux/ipa-server-selinux.spec.in86
-rw-r--r--ipa-server/selinux/ipa_kpasswd/ipa_kpasswd.fc9
-rw-r--r--ipa-server/selinux/ipa_kpasswd/ipa_kpasswd.te71
-rw-r--r--ipa-server/selinux/ipa_webgui/ipa_webgui.fc11
-rw-r--r--ipa-server/selinux/ipa_webgui/ipa_webgui.te97
-rw-r--r--ipa-server/version.m4.in1
-rw-r--r--ipa-server/xmlrpc-server/Makefile.am38
-rw-r--r--ipa-server/xmlrpc-server/README0
-rw-r--r--ipa-server/xmlrpc-server/attrs.py53
-rw-r--r--ipa-server/xmlrpc-server/funcs.py2291
-rw-r--r--ipa-server/xmlrpc-server/ipa-rewrite.conf19
-rw-r--r--ipa-server/xmlrpc-server/ipa.conf109
-rw-r--r--ipa-server/xmlrpc-server/ipaxmlrpc.py394
-rw-r--r--ipa-server/xmlrpc-server/ssbrowser.html68
-rw-r--r--ipa-server/xmlrpc-server/test/Makefile.am12
-rw-r--r--ipa-server/xmlrpc-server/test/README60
-rw-r--r--ipa-server/xmlrpc-server/test/test.py41
-rw-r--r--ipa-server/xmlrpc-server/test/test_methods.py57
-rw-r--r--ipa-server/xmlrpc-server/test/test_mod_python.py52
-rw-r--r--ipa-server/xmlrpc-server/unauthorized.html28
390 files changed, 60409 insertions, 0 deletions
diff --git a/Makefile b/Makefile
new file mode 100644
index 00000000..6e69387d
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,261 @@
+include VERSION
+
+SUBDIRS=ipa-server ipa-admintools ipa-python ipa-client ipa-radius-server ipa-radius-admintools
+
+PRJ_PREFIX=ipa
+
+RPMBUILD ?= $(PWD)/rpmbuild
+TARGET ?= master
+
+# After updating the version in VERSION you should run the version-update
+# target.
+
+ifeq ($(IPA_VERSION_IS_GIT_SNAPSHOT),"yes")
+GIT_VERSION=$(shell git show --pretty=format:"%h" --stat HEAD 2>/dev/null|head -1)
+ifneq ($(GIT_VERSION),)
+IPA_VERSION=$(IPA_VERSION_MAJOR).$(IPA_VERSION_MINOR).$(IPA_VERSION_RELEASE)GIT$(GIT_VERSION)
+endif # in a git tree and git returned a version
+endif # git
+
+ifndef IPA_VERSION
+ifdef IPA_VERSION_PRE_RELEASE
+IPA_VERSION=$(IPA_VERSION_MAJOR).$(IPA_VERSION_MINOR).$(IPA_VERSION_RELEASE).pre$(IPA_VERSION_PRE_RELEASE)
+else
+ifdef IPA_VERSION_RC_RELEASE
+IPA_VERSION=$(IPA_VERSION_MAJOR).$(IPA_VERSION_MINOR).$(IPA_VERSION_RELEASE).rc$(IPA_VERSION_RC_RELEASE)
+else
+IPA_VERSION=$(IPA_VERSION_MAJOR).$(IPA_VERSION_MINOR).$(IPA_VERSION_RELEASE)
+endif # rc
+endif # pre
+endif # ipa_version
+
+SERV_TARBALL_PREFIX=$(PRJ_PREFIX)-server-$(IPA_VERSION)
+SERV_TARBALL=$(SERV_TARBALL_PREFIX).tgz
+
+ADMIN_TARBALL_PREFIX=$(PRJ_PREFIX)-admintools-$(IPA_VERSION)
+ADMIN_TARBALL=$(ADMIN_TARBALL_PREFIX).tgz
+
+PYTHON_TARBALL_PREFIX=$(PRJ_PREFIX)-python-$(IPA_VERSION)
+PYTHON_TARBALL=$(PYTHON_TARBALL_PREFIX).tgz
+
+CLI_TARBALL_PREFIX=$(PRJ_PREFIX)-client-$(IPA_VERSION)
+CLI_TARBALL=$(CLI_TARBALL_PREFIX).tgz
+
+RADIUS_SERVER_TARBALL_PREFIX=$(PRJ_PREFIX)-radius-server-$(IPA_VERSION)
+RADIUS_SERVER_TARBALL=$(RADIUS_SERVER_TARBALL_PREFIX).tgz
+
+RADIUS_ADMINTOOLS_TARBALL_PREFIX=$(PRJ_PREFIX)-radius-admintools-$(IPA_VERSION)
+RADIUS_ADMINTOOLS_TARBALL=$(RADIUS_ADMINTOOLS_TARBALL_PREFIX).tgz
+
+SERV_SELINUX_TARBALL_PREFIX=$(PRJ_PREFIX)-server-selinux-$(IPA_VERSION)
+SERV_SELINUX_TARBALL=$(SERV_SELINUX_TARBALL_PREFIX).tgz
+
+IPA_RPM_RELEASE=$(shell cat RELEASE)
+
+LIBDIR ?= /usr/lib
+
+all: bootstrap-autogen
+ @for subdir in $(SUBDIRS); do \
+ (cd $$subdir && $(MAKE) $@) || exit 1; \
+ done
+
+bootstrap-autogen: version-update
+ @echo "Building IPA $(IPA_VERSION)"
+ cd ipa-server; if [ ! -e Makefile ]; then ./autogen.sh --prefix=/usr --sysconfdir=/etc --localstatedir=/var --libdir=$(LIBDIR); fi
+ cd ipa-client; if [ ! -e Makefile ]; then ./autogen.sh --prefix=/usr --sysconfdir=/etc --localstatedir=/var --libdir=$(LIBDIR); fi
+
+autogen: version-update
+ @echo "Building IPA $(IPA_VERSION)"
+ cd ipa-server; ./autogen.sh --prefix=/usr --sysconfdir=/etc --localstatedir=/var --libdir=$(LIBDIR)
+ cd ipa-client; ./autogen.sh --prefix=/usr --sysconfdir=/etc --localstatedir=/var --libdir=$(LIBDIR)
+
+install: all
+ @for subdir in $(SUBDIRS); do \
+ (cd $$subdir && $(MAKE) $@) || exit 1; \
+ done
+
+test:
+ @for subdir in $(SUBDIRS); do \
+ (cd $$subdir && $(MAKE) $@) || exit 1; \
+ done
+
+release-update:
+ if [ ! -e RELEASE ]; then echo 0 > RELEASE; fi
+
+version-update: release-update
+ sed -e s/__VERSION__/$(IPA_VERSION)/ -e s/__RELEASE__/$(IPA_RPM_RELEASE)/ \
+ ipa-server/ipa-server.spec.in > ipa-server/ipa-server.spec
+ sed -e s/__VERSION__/$(IPA_VERSION)/ ipa-server/version.m4.in \
+ > ipa-server/version.m4
+
+ sed -e s/__VERSION__/$(IPA_VERSION)/ -e s/__RELEASE__/$(IPA_RPM_RELEASE)/ \
+ ipa-admintools/ipa-admintools.spec.in > ipa-admintools/ipa-admintools.spec
+
+ sed -e s/__VERSION__/$(IPA_VERSION)/ -e s/__RELEASE__/$(IPA_RPM_RELEASE)/ \
+ ipa-python/ipa-python.spec.in > ipa-python/ipa-python.spec
+
+ sed -e s/__VERSION__/$(IPA_VERSION)/ -e s/__RELEASE__/$(IPA_RPM_RELEASE)/ \
+ ipa-client/ipa-client.spec.in > ipa-client/ipa-client.spec
+ sed -e s/__VERSION__/$(IPA_VERSION)/ ipa-client/version.m4.in \
+ > ipa-client/version.m4
+
+ sed -e s/__VERSION__/$(IPA_VERSION)/ -e s/__RELEASE__/$(IPA_RPM_RELEASE)/ \
+ ipa-radius-server/ipa-radius-server.spec.in \
+ > ipa-radius-server/ipa-radius-server.spec
+
+ sed -e s/__VERSION__/$(IPA_VERSION)/ -e s/__RELEASE__/$(IPA_RPM_RELEASE)/ \
+ ipa-radius-admintools/ipa-radius-admintools.spec.in \
+ > ipa-radius-admintools/ipa-radius-admintools.spec
+
+ sed -e s/__VERSION__/$(IPA_VERSION)/ -e s/__RELEASE__/$(IPA_RPM_RELEASE)/ \
+ ipa-server/selinux/ipa-server-selinux.spec.in \
+ > ipa-server/selinux/ipa-server-selinux.spec
+
+ sed -e s/__VERSION__/$(IPA_VERSION)/ ipa-python/setup.py.in \
+ > ipa-python/setup.py
+ sed -e s/__VERSION__/$(IPA_VERSION)/ ipa-python/version.py.in \
+ > ipa-python/version.py
+ perl -pi -e "s:__NUM_VERSION__:$(IPA_VERSION_MAJOR)$(IPA_VERSION_MINOR)$(IPA_VERSION_RELEASE):" ipa-python/version.py
+
+archive:
+ -mkdir -p dist
+ git archive --format=tar --prefix=ipa/ $(TARGET) | (cd dist && tar xf -)
+
+local-archive:
+ -mkdir -p dist/ipa
+ @for subdir in $(SUBDIRS); do \
+ cp -pr $$subdir dist/ipa/.; \
+ done
+
+archive-cleanup:
+ rm -fr dist/ipa
+
+tarballs:
+ -mkdir -p dist/sources
+
+ # ipa-server
+ mv dist/ipa/ipa-server dist/$(SERV_TARBALL_PREFIX)
+ rm -f dist/sources/$(SERV_TARBALL)
+ cd dist/$(SERV_TARBALL_PREFIX); ./autogen.sh --prefix=/usr --sysconfdir=/etc --localstatedir=/var --libdir=$(LIBDIR); make distclean
+ cd dist; tar cfz sources/$(SERV_TARBALL) $(SERV_TARBALL_PREFIX)
+ rm -fr dist/$(SERV_TARBALL_PREFIX)
+
+ # ipa-admintools
+ mv dist/ipa/ipa-admintools dist/$(ADMIN_TARBALL_PREFIX)
+ rm -f dist/sources/$(ADMIN_TARBALL)
+ cd dist; tar cfz sources/$(ADMIN_TARBALL) $(ADMIN_TARBALL_PREFIX)
+ rm -fr dist/$(ADMIN_TARBALL_PREFIX)
+
+ # ipa-python
+ mv dist/ipa/ipa-python dist/$(PYTHON_TARBALL_PREFIX)
+ rm -f dist/sources/$(PYTHON_TARBALL)
+ cd dist; tar cfz sources/$(PYTHON_TARBALL) $(PYTHON_TARBALL_PREFIX)
+ rm -fr dist/$(PYTHON_TARBALL_PREFIX)
+
+ # ipa-client
+ mv dist/ipa/ipa-client dist/$(CLI_TARBALL_PREFIX)
+ rm -f dist/sources/$(CLI_TARBALL)
+ cd dist/$(CLI_TARBALL_PREFIX); ./autogen.sh --prefix=/usr --sysconfdir=/etc --localstatedir=/var --libdir=$(LIBDIR); make distclean
+ cd dist; tar cfz sources/$(CLI_TARBALL) $(CLI_TARBALL_PREFIX)
+ rm -fr dist/$(CLI_TARBALL_PREFIX)
+
+ # ipa-radius-server
+ mv dist/ipa/ipa-radius-server dist/$(RADIUS_SERVER_TARBALL_PREFIX)
+ rm -f dist/sources/$(RADIUS_SERVER_TARBALL)
+ cd dist; tar cfz sources/$(RADIUS_SERVER_TARBALL) $(RADIUS_SERVER_TARBALL_PREFIX)
+ rm -fr dist/$(RADIUS_SERVER_TARBALL_PREFIX)
+
+ # ipa-radius-admintools
+ mv dist/ipa/ipa-radius-admintools dist/$(RADIUS_ADMINTOOLS_TARBALL_PREFIX)
+ rm -f dist/sources/$(RADIUS_ADMINTOOLS_TARBALL)
+ cd dist; tar cfz sources/$(RADIUS_ADMINTOOLS_TARBALL) $(RADIUS_ADMINTOOLS_TARBALL_PREFIX)
+ rm -fr dist/$(RADIUS_ADMINTOOLS_TARBALL_PREFIX)
+
+ # ipa-server/selinux
+ cp dist/sources/$(SERV_TARBALL) dist/sources/$(SERV_SELINUX_TARBALL)
+
+
+rpmroot:
+ mkdir -p $(RPMBUILD)/BUILD
+ mkdir -p $(RPMBUILD)/RPMS
+ mkdir -p $(RPMBUILD)/SOURCES
+ mkdir -p $(RPMBUILD)/SPECS
+ mkdir -p $(RPMBUILD)/SRPMS
+
+rpmdistdir:
+ mkdir -p dist/rpms
+ mkdir -p dist/srpms
+
+rpm-ipa-server:
+ cp dist/sources/$(SERV_TARBALL) $(RPMBUILD)/SOURCES/.
+ rpmbuild --define "_topdir $(RPMBUILD)" -ba ipa-server/ipa-server.spec
+ cp rpmbuild/RPMS/*/$(PRJ_PREFIX)-server-$(IPA_VERSION)-*.rpm dist/rpms/
+ cp rpmbuild/SRPMS/$(PRJ_PREFIX)-server-$(IPA_VERSION)-*.src.rpm dist/srpms/
+
+rpm-ipa-admin:
+ cp dist/sources/$(ADMIN_TARBALL) $(RPMBUILD)/SOURCES/.
+ rpmbuild --define "_topdir $(RPMBUILD)" -ba ipa-admintools/ipa-admintools.spec
+ cp rpmbuild/RPMS/noarch/$(PRJ_PREFIX)-admintools-$(IPA_VERSION)-*.rpm dist/rpms/
+ cp rpmbuild/SRPMS/$(PRJ_PREFIX)-admintools-$(IPA_VERSION)-*.src.rpm dist/srpms/
+
+rpm-ipa-python:
+ cp dist/sources/$(PYTHON_TARBALL) $(RPMBUILD)/SOURCES/.
+ rpmbuild --define "_topdir $(RPMBUILD)" -ba ipa-python/ipa-python.spec
+ cp rpmbuild/RPMS/noarch/$(PRJ_PREFIX)-python-$(IPA_VERSION)-*.rpm dist/rpms/
+ cp rpmbuild/SRPMS/$(PRJ_PREFIX)-python-$(IPA_VERSION)-*.src.rpm dist/srpms/
+
+rpm-ipa-client:
+ cp dist/sources/$(CLI_TARBALL) $(RPMBUILD)/SOURCES/.
+ rpmbuild --define "_topdir $(RPMBUILD)" -ba ipa-client/ipa-client.spec
+ cp rpmbuild/RPMS/*/$(PRJ_PREFIX)-client-$(IPA_VERSION)-*.rpm dist/rpms/
+ cp rpmbuild/SRPMS/$(PRJ_PREFIX)-client-$(IPA_VERSION)-*.src.rpm dist/srpms/
+
+rpm-ipa-radius-server:
+ cp dist/sources/$(RADIUS_SERVER_TARBALL) $(RPMBUILD)/SOURCES/.
+ rpmbuild --define "_topdir $(RPMBUILD)" -ba ipa-radius-server/ipa-radius-server.spec
+ cp rpmbuild/RPMS/noarch/$(PRJ_PREFIX)-radius-server-$(IPA_VERSION)-*.rpm dist/rpms/
+ cp rpmbuild/SRPMS/$(PRJ_PREFIX)-radius-server-$(IPA_VERSION)-*.src.rpm dist/srpms/
+
+rpm-ipa-radius-admintools:
+ cp dist/sources/$(RADIUS_ADMINTOOLS_TARBALL) $(RPMBUILD)/SOURCES/.
+ rpmbuild --define "_topdir $(RPMBUILD)" -ba ipa-radius-admintools/ipa-radius-admintools.spec
+ cp rpmbuild/RPMS/noarch/$(PRJ_PREFIX)-radius-admintools-$(IPA_VERSION)-*.rpm dist/rpms/
+ cp rpmbuild/SRPMS/$(PRJ_PREFIX)-radius-admintools-$(IPA_VERSION)-*.src.rpm dist/srpms/
+
+rpm-ipa-server-selinux:
+ cp dist/sources/$(SERV_SELINUX_TARBALL) $(RPMBUILD)/SOURCES/.
+ rpmbuild --define "_topdir $(RPMBUILD)" -ba ipa-server/selinux/ipa-server-selinux.spec
+ cp rpmbuild/RPMS/*/$(PRJ_PREFIX)-server-selinux-$(IPA_VERSION)-*.rpm dist/rpms/
+ cp rpmbuild/SRPMS/$(PRJ_PREFIX)-server-selinux-$(IPA_VERSION)-*.src.rpm dist/srpms/
+
+rpms: rpmroot rpmdistdir rpm-ipa-server rpm-ipa-admin rpm-ipa-python rpm-ipa-client rpm-ipa-radius-server rpm-ipa-radius-admintools rpm-ipa-server-selinux
+
+repodata:
+ -createrepo -p dist
+
+dist: version-update archive tarballs archive-cleanup rpms repodata
+
+local-dist: autogen clean local-archive tarballs archive-cleanup rpms
+
+
+clean: version-update
+ @for subdir in $(SUBDIRS); do \
+ (cd $$subdir && $(MAKE) $@) || exit 1; \
+ done
+ rm -f *~
+
+distclean: version-update
+ @for subdir in $(SUBDIRS); do \
+ (cd $$subdir && $(MAKE) $@) || exit 1; \
+ done
+ rm -fr rpmbuild dist
+
+maintainer-clean: clean
+ rm -fr rpmbuild dist
+ cd ipa-server/selinux && $(MAKE) maintainer-clean
+ cd ipa-server && $(MAKE) maintainer-clean
+ cd ipa-client && $(MAKE) maintainer-clean
+ cd ipa-python && $(MAKE) maintainer-clean
+ cd ipa-admintools && $(MAKE) maintainer-clean
+ cd ipa-radius-admintools && $(MAKE) maintainer-clean
+ cd ipa-radius-server && $(MAKE) maintainer-clean
diff --git a/README b/README
new file mode 100644
index 00000000..86c01f15
--- /dev/null
+++ b/README
@@ -0,0 +1,60 @@
+
+ IPA Server
+
+ What is it?
+ -----------
+
+ For efficiency, compliance and risk mitigation, organizations need to
+ centrally manage and correlate vital security information including:
+
+ * Identity (machine, user, virtual machines, groups, authentication
+ credentials)
+ * Policy (configuration settings, access control information)
+ * Audit (events, logs, analysis thereof)
+
+ Since these are not new problems. there exist many approaches and
+ products focused on addressing them. However, these tend to have the
+ following weaknesses:
+
+ * Focus on solving identity management across the enterprise has meant
+ less focus on policy and audit.
+ * Vendor focus on Web identity management problems has meant less well
+ developed solutions for central management of the Linux and Unix
+ world's vital security info. Organizations are forced to maintain
+ a hodgepodge of internal and proprietary solutions at high TCO.
+ * Proprietary security products don't easily provide access to the
+ vital security information they collect or manage. This makes it
+ difficult to synchronize and analyze effectively.
+
+ The Latest Version
+ ------------------
+
+ Details of the latest version can be found on the IPA server project
+ page under <http://www.freeipa.org/>.
+
+ Documentation
+ -------------
+
+ The most up-to-date documentation can be found at
+ <http://freeipa.org/page/Documentation/>.
+
+ Licensing
+ ---------
+
+ Please see the file called LICENSE.
+
+ Contacts
+ --------
+
+ * If you want to be informed about new code releases, bug fixes,
+ security fixes, general news and information about the IPA server
+ subscribe to the freeipa-announce mailing list at
+ <https://www.redhat.com/mailman/listinfo/freeipa-interest/>.
+
+ * If you have a bug report please submit it at:
+ <https://bugzilla.redhat.com>
+
+ * If you want to participate in actively developing IPA please
+ subscribe to the freeipa-devel mailing list at
+ <https://www.redhat.com/mailman/listinfo/freeipa-devel/> or join
+ us in IRC at irc://irc.freenode.net/freeipa
diff --git a/VERSION b/VERSION
new file mode 100644
index 00000000..13f2c693
--- /dev/null
+++ b/VERSION
@@ -0,0 +1,55 @@
+########################################################
+# freeIPA Version #
+# #
+# freeIPA versions are as follows #
+# 1.0.x New production series #
+# 1.0.x{pre,rc}y Preview/Testing & RC #
+# 1.0.0GITabcdefg Build from GIT #
+# #
+########################################################
+
+########################################################
+# This are the main version numbers #
+# #
+# <MAJOR>.<MINOR>.<RELEASE> #
+# #
+# e.g. IPA_VERSION_MAJOR=1 #
+# IPA_VERSION_MINOR=0 #
+# IPA_VERSION_RELEASE=0 #
+# -> "1.0.0" #
+########################################################
+IPA_VERSION_MAJOR=1
+IPA_VERSION_MINOR=2
+IPA_VERSION_RELEASE=0
+
+########################################################
+# For 'pre' releases the version will be #
+# #
+# <MAJOR>.<MINOR>.<RELEASE>pre<PRE_RELEASE> #
+# #
+# e.g. IPA_VERSION_PRE_RELEASE=1 #
+# -> "1.0.0pre1" #
+########################################################
+IPA_VERSION_PRE_RELEASE=
+
+########################################################
+# For 'rc' releases the version will be #
+# #
+# <MAJOR>.<MINOR>.<RELEASE>rc<RC_RELEASE> #
+# #
+# e.g. IPA_VERSION_RC_RELEASE=1 #
+# -> "1.0.0rc1" #
+########################################################
+IPA_VERSION_RC_RELEASE=
+
+########################################################
+# To mark GIT snapshots this should be set to 'yes' #
+# in the development BRANCH, and set to 'no' only in #
+# the IPA_X_X_RELEASE BRANCH #
+# #
+# <MAJOR>.<MINOR>.<RELEASE>GITxxx #
+# #
+# e.g. IPA_VERSION_IS_SVN_SNAPSHOT=yes #
+# -> "1.0.0GITabcdefg" #
+########################################################
+IPA_VERSION_IS_GIT_SNAPSHOT="yes"
diff --git a/contrib/RHEL4/Makefile.am b/contrib/RHEL4/Makefile.am
new file mode 100644
index 00000000..f42303c4
--- /dev/null
+++ b/contrib/RHEL4/Makefile.am
@@ -0,0 +1,13 @@
+NULL =
+
+sbin_SCRIPTS = \
+ ipa-client-setup \
+ $(NULL)
+
+EXTRA_DIST = \
+ $(sbin_SCRIPTS) \
+ $(NULL)
+
+MAINTAINERCLEANFILES = \
+ *~ \
+ Makefile.in
diff --git a/contrib/RHEL4/configure.ac b/contrib/RHEL4/configure.ac
new file mode 100644
index 00000000..83676a37
--- /dev/null
+++ b/contrib/RHEL4/configure.ac
@@ -0,0 +1,55 @@
+AC_PREREQ(2.59)
+AC_INIT([ipa-client],
+ [0.99.0],
+ [http://www.freeipa.org/])
+
+AM_INIT_AUTOMAKE
+
+AC_SUBST(VERSION)
+
+dnl ---------------------------------------------------------------------------
+dnl - Check for Python
+dnl ---------------------------------------------------------------------------
+
+AC_MSG_NOTICE([Checking for Python])
+have_python=no
+AM_PATH_PYTHON([2.3])
+
+if test "x$PYTHON" = "x" ; then
+ AC_MSG_ERROR([Python not found])
+fi
+
+dnl ---------------------------------------------------------------------------
+dnl - Set the data install directory since we don't use pkgdatadir
+dnl ---------------------------------------------------------------------------
+
+IPA_DATA_DIR="$datadir/ipa"
+AC_SUBST(IPA_DATA_DIR)
+
+dnl ---------------------------------------------------------------------------
+dnl Finish
+dnl ---------------------------------------------------------------------------
+
+# Files
+
+AC_CONFIG_FILES([
+ Makefile
+])
+
+AC_OUTPUT
+
+echo "
+ IPA client $VERSION
+ ========================
+
+ prefix: ${prefix}
+ exec_prefix: ${exec_prefix}
+ libdir: ${libdir}
+ bindir: ${bindir}
+ sbindir: ${sbindir}
+ sysconfdir: ${sysconfdir}
+ localstatedir: ${localstatedir}
+ datadir: ${datadir}
+ source code location: ${srcdir}
+ Maintainer mode: ${USE_MAINTAINER_MODE}
+"
diff --git a/contrib/RHEL4/ipa-client-setup b/contrib/RHEL4/ipa-client-setup
new file mode 100644
index 00000000..8e66ffdc
--- /dev/null
+++ b/contrib/RHEL4/ipa-client-setup
@@ -0,0 +1,368 @@
+#! /usr/bin/python -E
+# Authors: Simo Sorce <ssorce@redhat.com>
+# Karl MacMillan <kmacmillan@mentalrootkit.com>
+#
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+VERSION = "%prog .1"
+
+import sys
+import os
+import string
+import shutil
+import socket
+import logging
+from optparse import OptionParser
+import ipachangeconf
+import ldap
+from ldap import LDAPError
+
+class ipaserver:
+
+ def __init__(self, server):
+ self.server = server
+ self.realm = None
+ self.domain = None
+ self.basedn = None
+
+ def getServerName(self):
+ return str(self.server)
+
+ def getDomainName(self):
+ return str(self.domain)
+
+ def getRealmName(self):
+ return str(self.realm)
+
+ def getBaseDN(self):
+ return str(self.basedn)
+
+ def check(self):
+
+ lret = []
+ lres = []
+ lattr = ""
+ linfo = ""
+ lrealms = []
+
+ i = 0
+
+ #now verify the server is really an IPA server
+ try:
+ logging.debug("Init ldap with: ldap://"+self.server+":389")
+ lh = ldap.initialize("ldap://"+self.server+":389")
+ lh.simple_bind_s("","")
+
+ logging.debug("Search rootdse")
+ lret = lh.search_s("", ldap.SCOPE_BASE, "(objectClass=*)")
+ for lattr in lret[0][1]:
+ if lattr.lower() == "namingcontexts":
+ self.basedn = lret[0][1][lattr][0]
+
+ logging.debug("Search for (info=*) in "+self.basedn+"(base)")
+ lret = lh.search_s(self.basedn, ldap.SCOPE_BASE, "(info=IPA*)")
+ if not lret:
+ return False
+ logging.debug("Found: "+str(lret))
+
+ for lattr in lret[0][1]:
+ if lattr.lower() == "info":
+ linfo = lret[0][1][lattr][0].lower()
+ break
+
+ if not linfo:
+ return False
+
+ #search and return known realms
+ logging.debug("Search for (objectClass=krbRealmContainer) in "+self.basedn+"(sub)")
+ lret = lh.search_s("cn=kerberos,"+self.basedn, ldap.SCOPE_SUBTREE, "(objectClass=krbRealmContainer)")
+ if not lret:
+ #something very wrong
+ return False
+ logging.debug("Found: "+str(lret))
+
+ for lres in lret:
+ for lattr in lres[1]:
+ if lattr.lower() == "cn":
+ lrealms.append(lres[1][lattr][0])
+
+
+ if len(lrealms) != 1:
+ #which one? we can't attach to a multi-realm server without DNS working
+ return False
+ else:
+ self.realm = lrealms[0]
+ self.domain = lrealms[0].lower()
+ return True
+
+ except LDAPError, err:
+ #no good
+ logging.error("Ldap Error: "+str(err))
+ return False
+
+ntp_conf = """# Permit time synchronization with our time source, but do not
+# permit the source to query or modify the service on this system.
+restrict default kod nomodify notrap nopeer noquery
+restrict -6 default kod nomodify notrap nopeer noquery
+
+# Permit all access over the loopback interface. This could
+# be tightened as well, but to do so would effect some of
+# the administrative functions.
+restrict 127.0.0.1
+restrict -6 ::1
+
+# Hosts on local network are less restricted.
+#restrict 192.168.1.0 mask 255.255.255.0 nomodify notrap
+
+# Use public servers from the pool.ntp.org project.
+# Please consider joining the pool (http://www.pool.ntp.org/join.html).
+server $SERVER
+
+#broadcast 192.168.1.255 key 42 # broadcast server
+#broadcastclient # broadcast client
+#broadcast 224.0.1.1 key 42 # multicast server
+#multicastclient 224.0.1.1 # multicast client
+#manycastserver 239.255.254.254 # manycast server
+#manycastclient 239.255.254.254 key 42 # manycast client
+
+# Undisciplined Local Clock. This is a fake driver intended for backup
+# and when no outside source of synchronized time is available.
+server 127.127.1.0 # local clock
+#fudge 127.127.1.0 stratum 10
+
+# Drift file. Put this in a directory which the daemon can write to.
+# No symbolic links allowed, either, since the daemon updates the file
+# by creating a temporary in the same directory and then rename()'ing
+# it to the file.
+driftfile /var/lib/ntp/drift
+
+# Key file containing the keys and key identifiers used when operating
+# with symmetric key cryptography.
+keys /etc/ntp/keys
+
+# Specify the key identifiers which are trusted.
+#trustedkey 4 8 42
+
+# Specify the key identifier to use with the ntpdc utility.
+#requestkey 8
+
+# Specify the key identifier to use with the ntpq utility.
+#controlkey 8
+"""
+
+ntp_sysconfig = """# Drop root to id 'ntp:ntp' by default.
+OPTIONS="-x -u ntp:ntp -p /var/run/ntpd.pid"
+
+# Set to 'yes' to sync hw clock after successful ntpdate
+SYNC_HWCLOCK=yes
+
+# Additional options for ntpdate
+NTPDATE_OPTIONS=""
+"""
+
+def config_ntp(server_fqdn):
+
+ nc = string.replace(ntp_conf, "$SERVER", server_fqdn)
+
+ shutil.copy("/etc/ntp.conf", "/etc/ntp.conf.ipasave")
+
+ fd = open("/etc/ntp.conf", "w")
+ fd.write(nc)
+ fd.close()
+
+ shutil.copy("/etc/sysconfig/ntpd", "/etc/sysconfig/ntpd.ipasave")
+
+ fd = open("/etc/sysconfig/ntpd", "w")
+ fd.write(ntp_sysconfig)
+ fd.close()
+
+ # Set the ntpd to start on boot
+ os.system("/sbin/chkconfig ntpd on")
+
+ # Restart ntpd
+ os.system("/sbin/service ntpd restart")
+
+def parse_options():
+ parser = OptionParser(version=VERSION)
+ parser.add_option("--server", dest="server", help="IPA server")
+ parser.add_option("-d", "--debug", dest="debug", action="store_true",
+ default=False, help="print debugging information")
+ parser.add_option("-U", "--unattended", dest="unattended",
+ action="store_true",
+ help="unattended installation never prompts the user")
+ parser.add_option("-N", "--no-ntp", action="store_false",
+ help="do not configure ntp", default=True, dest="conf_ntp")
+
+ options, args = parser.parse_args()
+ if not options.server:
+ parser.error("must provide an IPA server name with --server")
+
+ return options
+
+def ask_for_confirmation(message):
+ yesno = raw_input(message + " [y/N]: ")
+ if not yesno or yesno.lower()[0] != "y":
+ return False
+ print "\n"
+ return True
+
+def logging_setup(options):
+ # Always log everything (i.e., DEBUG) to the log
+ # file.
+ logger = logging.getLogger('ipa-client-setup')
+ fh = logging.FileHandler('ipaclient-install.log')
+ formatter = logging.Formatter('%(name)-12s: %(levelname)-8s %(message)s')
+ fh.setFormatter(formatter)
+ logger.addHandler(fh)
+
+ # If the debug option is set, also log debug messages to the console
+ if options.debug:
+ logger.setLevel(logging.DEBUG)
+ else:
+ # Otherwise, log critical and error messages
+ logger.setLevel(logging.ERROR)
+
+ return logger
+
+def main():
+ options = parse_options()
+ logger = logging_setup(options)
+ dnsok = True
+
+ ipasrv = ipaserver(options.server)
+
+ ret = ipasrv.check()
+ if ret == False:
+ print "Failed to verify that ["+options.server+"] is an IPA Server, aborting!"
+ return -1
+
+ print "IPA Server verified."
+ print "Realm: "+ipasrv.getRealmName()
+ print "DNS Domain: "+ipasrv.getDomainName()
+ print "IPA Server: "+ipasrv.getServerName()
+ print "BaseDN: "+ipasrv.getBaseDN()
+
+ print "\n"
+ if not options.unattended and not ask_for_confirmation("Continue to configure the system with these values?"):
+ return 1
+
+ # Configure ipa.conf
+ ipaconf = ipachangeconf.IPAChangeConf("IPA Installer")
+ ipaconf.setOptionAssignment(" = ")
+ ipaconf.setSectionNameDelimiters(("[","]"))
+
+ opts = [{'name':'comment', 'type':'comment', 'value':'File modified by ipa-client-install'},
+ {'name':'empty', 'type':'empty'}]
+
+ #[defaults]
+ defopts = [{'name':'server', 'type':'option', 'value':ipasrv.getServerName()},
+ {'name':'realm', 'type':'option', 'value':ipasrv.getRealmName()}]
+
+ opts.append({'name':'defaults', 'type':'section', 'value':defopts})
+ opts.append({'name':'empty', 'type':'empty'})
+
+ ipaconf.newConf("/etc/ipa/ipa.conf", opts)
+
+ # Configure ldap.conf
+ ldapconf = ipachangeconf.IPAChangeConf("IPA Installer")
+ ldapconf.setOptionAssignment(" ")
+
+ opts = [{'name':'comment', 'type':'comment', 'value':'File modified by ipa-client-install'},
+ {'name':'empty', 'type':'empty'},
+ {'name':'ldap_version', 'type':'option', 'value':'3'},
+ {'name':'base', 'type':'option', 'value':ipasrv.getBaseDN()},
+ {'name':'empty', 'type':'empty'},
+ {'name':'nss_base_passwd', 'type':'option', 'value':'cn=users,cn=accounts,'+ipasrv.getBaseDN()+'?sub'},
+ {'name':'nss_base_group', 'type':'option', 'value':'cn=users,cn=accounts,'+ipasrv.getBaseDN()+'?sub'},
+ {'name':'nss_schema', 'type':'option', 'value':'rfc2307bis'},
+ {'name':'nss_map_attribute', 'type':'option', 'value':'uniqueMember member'},
+ {'name':'nss_initgroups_ignoreusers', 'type':'option', 'value':'root,dirsrv'},
+ {'name':'empty', 'type':'empty'},
+ {'name':'nss_reconnect_maxsleeptime', 'type':'option', 'value':'8'},
+ {'name':'nss_reconnect_sleeptime', 'type':'option', 'value':'1'},
+ {'name':'bind_timelimit', 'type':'option', 'value':'5'},
+ {'name':'timelimit', 'type':'option', 'value':'15'},
+ {'name':'empty', 'type':'empty'},
+ {'name':'uri', 'type':'option', 'value':'ldap://'+ipasrv.getServerName()},
+ {'name':'empty', 'type':'empty'}]
+ try:
+ ldapconf.newConf("/etc/ldap.conf", opts)
+ except Exception, e:
+ print "Configuration failed: " + str(e)
+ return 1
+
+ if not "" == ipasrv.getRealmName():
+ #Configure krb5.conf
+ krbconf = ipachangeconf.IPAChangeConf("IPA Installer")
+ krbconf.setOptionAssignment(" = ")
+ krbconf.setSectionNameDelimiters(("[","]"))
+ krbconf.setSubSectionDelimiters(("{","}"))
+ krbconf.setIndent((""," "," "))
+
+ opts = [{'name':'comment', 'type':'comment', 'value':'File modified by ipa-client-install'},
+ {'name':'empty', 'type':'empty'}]
+
+ #[libdefaults]
+ libopts = [{'name':'default_realm', 'type':'option', 'value':ipasrv.getRealmName()}]
+ libopts.append({'name':'dns_lookup_realm', 'type':'option', 'value':'false'})
+ libopts.append({'name':'dns_lookup_kdc', 'type':'option', 'value':'false'})
+ libopts.append({'name':'ticket_lifetime', 'type':'option', 'value':'24h'})
+ libopts.append({'name':'forwardable', 'type':'option', 'value':'yes'})
+
+ opts.append({'name':'libdefaults', 'type':'section', 'value':libopts})
+ opts.append({'name':'empty', 'type':'empty'})
+
+ #[realms]
+ kropts =[{'name':'kdc', 'type':'option', 'value':ipasrv.getServerName()+':88'},
+ {'name':'admin_server', 'type':'option', 'value':ipasrv.getServerName()+':749'},
+ {'name':'default_domain', 'type':'option', 'value':ipasrv.getDomainName()}]
+ ropts = [{'name':ipasrv.getRealmName(), 'type':'subsection', 'value':kropts}]
+ opts.append({'name':'realms', 'type':'section', 'value':ropts})
+ opts.append({'name':'empty', 'type':'empty'})
+
+ #[domain_realm]
+ dropts = [{'name':'.'+ipasrv.getDomainName(), 'type':'option', 'value':ipasrv.getRealmName()},
+ {'name':ipasrv.getDomainName(), 'type':'option', 'value':ipasrv.getRealmName()}]
+ opts.append({'name':'domain_realm', 'type':'section', 'value':dropts})
+ opts.append({'name':'empty', 'type':'empty'})
+
+ #[appdefaults]
+ pamopts = [{'name':'debug', 'type':'option', 'value':'false'},
+ {'name':'ticket_lifetime', 'type':'option', 'value':'36000'},
+ {'name':'renew_lifetime', 'type':'option', 'value':'36000'},
+ {'name':'forwardable', 'type':'option', 'value':'true'},
+ {'name':'krb4_convert', 'type':'option', 'value':'false'}]
+ appopts = [{'name':'pam', 'type':'subsection', 'value':pamopts}]
+ opts.append({'name':'appdefaults', 'type':'section', 'value':appopts})
+
+ krbconf.newConf("/etc/krb5.conf", opts);
+
+ #Modify nsswitch to add nss_ldap
+ os.system("/usr/sbin/authconfig --enableldap --kickstart")
+
+ #Modify pam to add pam_krb5
+ os.system("/usr/sbin/authconfig --enablekrb5 --kickstart")
+
+ if options.conf_ntp:
+ config_ntp(ipasrv.getServerName())
+
+ print "Client configuration complete."
+
+ return 0
+
+sys.exit(main())
diff --git a/contrib/RHEL4/ipa-client.spec b/contrib/RHEL4/ipa-client.spec
new file mode 100644
index 00000000..d2173c73
--- /dev/null
+++ b/contrib/RHEL4/ipa-client.spec
@@ -0,0 +1,54 @@
+Name: ipa-client
+Version: 1.0.0
+Release: 1%{?dist}
+Summary: IPA client Setup script for RHEL-4
+
+Group: System Environment/Base
+License: GPLv2
+URL: http://www.freeipa.org
+Source0: %{name}-%{version}.tgz
+BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n)
+BuildArch: noarch
+#BuildRequires: python-devel
+
+Requires: python
+Requires: python-ldap
+
+%{!?python_sitelib: %define python_sitelib %(%{__python} -c "from distutils.sysconfig import get_python_lib; print get_python_lib()")}
+
+%description
+IPA is a server for identity, policy, and audit.
+The client package provide install and configuration scripts for RHEL-4 clients.
+
+%prep
+%setup -q
+%configure --prefix=/usr
+
+%build
+
+make
+
+%install
+rm -rf %{buildroot}
+%{__python} setup.py install --no-compile --root=%{buildroot}
+%makeinstall \
+ SBINDIR=$RPM_BUILD_ROOT%{_sbindir}
+mkdir -p $RPM_BUILD_ROOT/%{_sysconfdir}/ipa
+install -m644 ipa.conf $RPM_BUILD_ROOT%{_sysconfdir}/ipa/ipa.conf
+
+%clean
+rm -rf %{buildroot}
+
+%files
+%defattr(-,root,root,-)
+%{_sbindir}/ipa-client-setup
+%{python_sitelib}/ipachangeconf.py*
+%config(noreplace) %{_sysconfdir}/ipa/ipa.conf
+
+%changelog
+* Thu Apr 3 2008 Rob Crittenden <rcritten@redhat.com> - 1.0.0-1
+- Version bump for release
+
+* Mon Mar 25 2008 Simo Sorce <ssorce@redhat.com> - 0.99.0-1
+- First RHEL-4 release
+
diff --git a/contrib/RHEL4/ipa.conf b/contrib/RHEL4/ipa.conf
new file mode 100644
index 00000000..516f764d
--- /dev/null
+++ b/contrib/RHEL4/ipa.conf
@@ -0,0 +1,3 @@
+[defaults]
+# realm = EXAMPLE.COM
+# server = ipa.example.com
diff --git a/contrib/RHEL4/ipachangeconf.py b/contrib/RHEL4/ipachangeconf.py
new file mode 100644
index 00000000..34c08d10
--- /dev/null
+++ b/contrib/RHEL4/ipachangeconf.py
@@ -0,0 +1,459 @@
+#
+# ipachangeconf - configuration file manipulation classes and functions
+# partially based on authconfig code
+# Copyright (c) 1999-2007 Red Hat, Inc.
+# Author: Simo Sorce <ssorce@redhat.com>
+#
+# This is free software; you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by
+# the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+#
+
+import fcntl
+import os
+import string
+import time
+import shutil
+
+def openLocked(filename, perms):
+ fd = -1
+ try:
+ fd = os.open(filename, os.O_RDWR | os.O_CREAT, perms)
+
+ fcntl.lockf(fd, fcntl.LOCK_EX)
+ except OSError, (errno, strerr):
+ if fd != -1:
+ try:
+ os.close(fd)
+ except OSError:
+ pass
+ raise IOError(errno, strerr)
+ return os.fdopen(fd, "r+")
+
+
+ #TODO: add subsection as a concept
+ # (ex. REALM.NAME = { foo = x bar = y } )
+ #TODO: put section delimiters as separating element of the list
+ # so that we can process multiple sections in one go
+ #TODO: add a comment all but provided options as a section option
+class IPAChangeConf:
+
+ def __init__(self, name):
+ self.progname = name
+ self.indent = ("","","")
+ self.assign = (" = ","=")
+ self.dassign = self.assign[0]
+ self.comment = ("#",)
+ self.dcomment = self.comment[0]
+ self.eol = ("\n",)
+ self.deol = self.eol[0]
+ self.sectnamdel = ("[","]")
+ self.subsectdel = ("{","}")
+
+ def setProgName(self, name):
+ self.progname = name
+
+ def setIndent(self, indent):
+ if type(indent) is tuple:
+ self.indent = indent
+ elif type(indent) is str:
+ self.indent = (indent, )
+ else:
+ raise ValueError, 'Indent must be a list of strings'
+
+ def setOptionAssignment(self, assign):
+ if type(assign) is tuple:
+ self.assign = assign
+ else:
+ self.assign = (assign, )
+ self.dassign = self.assign[0]
+
+ def setCommentPrefix(self, comment):
+ if type(comment) is tuple:
+ self.comment = comment
+ else:
+ self.comment = (comment, )
+ self.dcomment = self.comment[0]
+
+ def setEndLine(self, eol):
+ if type(eol) is tuple:
+ self.eol = eol
+ else:
+ self.eol = (eol, )
+ self.deol = self.eol[0]
+
+ def setSectionNameDelimiters(self, delims):
+ self.sectnamdel = delims
+
+ def setSubSectionDelimiters(self, delims):
+ self.subsectdel = delims
+
+ def matchComment(self, line):
+ for v in self.comment:
+ if line.lstrip().startswith(v):
+ return line.lstrip()[len(v):]
+ return False
+
+ def matchEmpty(self, line):
+ if line.strip() == "":
+ return True
+ return False
+
+ def matchSection(self, line):
+ cl = "".join(line.strip().split()).lower()
+ if len(self.sectnamdel) != 2:
+ return False
+ if not cl.startswith(self.sectnamdel[0]):
+ return False
+ if not cl.endswith(self.sectnamdel[1]):
+ return False
+ return cl[len(self.sectnamdel[0]):-len(self.sectnamdel[1])]
+
+ def matchSubSection(self, line):
+ if self.matchComment(line):
+ return False
+
+ parts = line.split(self.dassign, 1)
+ if len(parts) < 2:
+ return False
+
+ if parts[1].strip() == self.subsectdel[0]:
+ return parts[0].strip()
+
+ return False
+
+ def matchSubSectionEnd(self, line):
+ if self.matchComment(line):
+ return False
+
+ if line.strip() == self.subsectdel[1]:
+ return True
+
+ return False
+
+ def getSectionLine(self, section):
+ if len(self.sectnamdel) != 2:
+ return section
+ return self.sectnamdel[0]+section+self.sectnamdel[1]+self.deol
+
+ def dump(self, options, level=0):
+ output = ""
+ if level >= len(self.indent):
+ level = len(self.indent)-1
+
+ for o in options:
+ if o['type'] == "section":
+ output += self.sectnamdel[0]+o['name']+self.sectnamdel[1]+self.deol
+ output += self.dump(o['value'], level+1)
+ continue
+ if o['type'] == "subsection":
+ output += self.indent[level]+o['name']+self.dassign+self.subsectdel[0]+self.deol
+ output += self.dump(o['value'], level+1)
+ output += self.indent[level]+self.subsectdel[1]+self.deol
+ continue
+ if o['type'] == "option":
+ output += self.indent[level]+o['name']+self.dassign+o['value']+self.deol
+ continue
+ if o['type'] == "comment":
+ output += self.dcomment+o['value']+self.deol
+ continue
+ if o['type'] == "empty":
+ output += self.deol
+ continue
+ raise SyntaxError, 'Unknown type: ['+o['type']+']'
+
+ return output
+
+ def parseLine(self, line):
+
+ if self.matchEmpty(line):
+ return {'name':'empty', 'type':'empty'}
+
+ value = self.matchComment(line)
+ if value:
+ return {'name':'comment', 'type':'comment', 'value':value.rstrip()}
+
+ parts = line.split(self.dassign, 1)
+ if len(parts) < 2:
+ raise SyntaxError, 'Syntax Error: Unknown line format'
+
+ return {'name':parts[0].strip(), 'type':'option', 'value':parts[1].rstrip()}
+
+ def findOpts(self, opts, type, name, exclude_sections=False):
+
+ num = 0
+ for o in opts:
+ if o['type'] == type and o['name'] == name:
+ return (num, o)
+ if exclude_sections and (o['type'] == "section" or o['type'] == "subsection"):
+ return (num, None)
+ num += 1
+ return (num, None)
+
+ def commentOpts(self, inopts, level = 0):
+
+ opts = []
+
+ if level >= len(self.indent):
+ level = len(self.indent)-1
+
+ for o in inopts:
+ if o['type'] == 'section':
+ no = self.commentOpts(o['value'], level+1)
+ val = self.dcomment+self.sectnamdel[0]+o['name']+self.sectnamdel[1]
+ opts.append({'name':'comment', 'type':'comment', 'value':val})
+ for n in no:
+ opts.append(n)
+ continue
+ if o['type'] == 'subsection':
+ no = self.commentOpts(o['value'], level+1)
+ val = self.indent[level]+o['name']+self.dassign+self.subsectdel[0]
+ opts.append({'name':'comment', 'type':'comment', 'value':val})
+ for n in no:
+ opts.append(n)
+ val = self.indent[level]+self.subsectdel[1]
+ opts.append({'name':'comment', 'type':'comment', 'value':val})
+ continue
+ if o['type'] == 'option':
+ val = self.indent[level]+o['name']+self.dassign+o['value']
+ opts.append({'name':'comment', 'type':'comment', 'value':val})
+ continue
+ if o['type'] == 'comment':
+ opts.append(o)
+ continue
+ if o['type'] == 'empty':
+ opts.append({'name':'comment', 'type':'comment', 'value':''})
+ continue
+ raise SyntaxError, 'Unknown type: ['+o['type']+']'
+
+ return opts
+
+ def mergeOld(self, oldopts, newopts):
+
+ opts = []
+
+ for o in oldopts:
+ if o['type'] == "section" or o['type'] == "subsection":
+ (num, no) = self.findOpts(newopts, o['type'], o['name'])
+ if not no:
+ opts.append(o)
+ continue
+ if no['action'] == "set":
+ mo = self.mergeOld(o['value'], no['value'])
+ opts.append({'name':o['name'], 'type':o['type'], 'value':mo})
+ continue
+ if no['action'] == "comment":
+ co = self.commentOpts(o['value'])
+ for c in co:
+ opts.append(c)
+ continue
+ if no['action'] == "remove":
+ continue
+ raise SyntaxError, 'Unknown action: ['+no['action']+']'
+
+ if o['type'] == "comment" or o['type'] == "empty":
+ opts.append(o)
+ continue
+
+ if o['type'] == "option":
+ (num, no) = self.findOpts(newopts, 'option', o['name'], True)
+ if not no:
+ opts.append(o)
+ continue
+ if no['action'] == 'comment' or no['action'] == 'remove':
+ if no['value'] != None and o['value'] != no['value']:
+ opts.append(o)
+ continue
+ if no['action'] == 'comment':
+ opts.append({'name':'comment', 'type':'comment',
+ 'value':self.dcomment+o['name']+self.dassign+o['value']})
+ continue
+ if no['action'] == 'set':
+ opts.append(no)
+ continue
+ raise SyntaxError, 'Unknown action: ['+o['action']+']'
+
+ raise SyntaxError, 'Unknown type: ['+o['type']+']'
+
+ return opts
+
+ def mergeNew(self, opts, newopts):
+
+ cline = 0
+
+ for no in newopts:
+
+ if no['type'] == "section" or no['type'] == "subsection":
+ (num, o) = self.findOpts(opts, no['type'], no['name'])
+ if not o:
+ if no['action'] == 'set':
+ opts.append(no)
+ continue
+ if no['action'] == "set":
+ self.mergeNew(o['value'], no['value'])
+ continue
+ cline = num+1
+ continue
+
+ if no['type'] == "option":
+ (num, o) = self.findOpts(opts, no['type'], no['name'], True)
+ if not o:
+ if no['action'] == 'set':
+ opts.append(no)
+ continue
+ cline = num+1
+ continue
+
+ if no['type'] == "comment" or no['type'] == "empty":
+ opts.insert(cline, no)
+ cline += 1
+ continue
+
+ raise SyntaxError, 'Unknown type: ['+no['type']+']'
+
+
+ def merge(self, oldopts, newopts):
+
+ #Use a two pass strategy
+ #First we create a new opts tree from oldopts removing/commenting
+ # the options as indicated by the contents of newopts
+ #Second we fill in the new opts tree with options as indicated
+ # in the newopts tree (this is becaus eentire (sub)sections may
+ # exist in the newopts that do not exist in oldopts)
+
+ opts = self.mergeOld(oldopts, newopts)
+ self.mergeNew(opts, newopts)
+ return opts
+
+ #TODO: Make parse() recursive?
+ def parse(self, f):
+
+ opts = []
+ sectopts = []
+ section = None
+ subsectopts = []
+ subsection = None
+ curopts = opts
+ fatheropts = opts
+
+ # Read in the old file.
+ for line in f:
+
+ # It's a section start.
+ value = self.matchSection(line)
+ if value:
+ if section is not None:
+ opts.append({'name':section, 'type':'section', 'value':sectopts})
+ sectopts = []
+ curopts = sectopts
+ fatheropts = sectopts
+ section = value
+ continue
+
+ # It's a subsection start.
+ value = self.matchSubSection(line)
+ if value:
+ if subsection is not None:
+ raise SyntaxError, 'nested subsections are not supported yet'
+ subsectopts = []
+ curopts = subsectopts
+ subsection = value
+ continue
+
+ value = self.matchSubSectionEnd(line)
+ if value:
+ if subsection is None:
+ raise SyntaxError, 'Unmatched end subsection terminator found'
+ fatheropts.append({'name':subsection, 'type':'subsection', 'value':subsectopts})
+ subsection = None
+ curopts = fatheropts
+ continue
+
+ # Copy anything else as is.
+ curopts.append(self.parseLine(line))
+
+ #Add last section if any
+ if len(sectopts) is not 0:
+ opts.append({'name':section, 'type':'section', 'value':sectopts})
+
+ return opts
+
+ # Write settings to configuration file
+ # file is a path
+ # options is a set of dictionaries in the form:
+ # [{'name': 'foo', 'value': 'bar', 'action': 'set/comment'}]
+ # section is a section name like 'global'
+ def changeConf(self, file, newopts):
+ autosection = False
+ savedsection = None
+ done = False
+ output = ""
+ f = None
+ try:
+ #Do not catch an unexisting file error, we want to fail in that case
+ shutil.copy2(file, file+".ipabkp")
+
+ f = openLocked(file, 0644)
+
+ oldopts = self.parse(f)
+
+ options = self.merge(oldopts, newopts)
+
+ output = self.dump(options)
+
+ # Write it out and close it.
+ f.seek(0)
+ f.truncate(0)
+ f.write(output)
+ finally:
+ try:
+ if f:
+ f.close()
+ except IOError:
+ pass
+ return True
+
+ # Write settings to new file, backup old
+ # file is a path
+ # options is a set of dictionaries in the form:
+ # [{'name': 'foo', 'value': 'bar', 'action': 'set/comment'}]
+ # section is a section name like 'global'
+ def newConf(self, file, options):
+ autosection = False
+ savedsection = None
+ done = False
+ output = ""
+ f = None
+ try:
+ try:
+ shutil.copy2(file, file+".ipabkp")
+ except IOError, err:
+ if err.errno == 2:
+ # The orign file did not exist
+ pass
+
+ f = openLocked(file, 0644)
+
+ # Trunkate
+ f.seek(0)
+ f.truncate(0)
+
+ output = self.dump(options)
+
+ f.write(output)
+ finally:
+ try:
+ if f:
+ f.close()
+ except IOError:
+ pass
+ return True
diff --git a/contrib/RHEL4/setup.py b/contrib/RHEL4/setup.py
new file mode 100644
index 00000000..ef0f36b8
--- /dev/null
+++ b/contrib/RHEL4/setup.py
@@ -0,0 +1,75 @@
+#!/usr/bin/env python
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+"""FreeIPA python support library
+
+FreeIPA is a server for identity, policy, and audit.
+"""
+
+DOCLINES = __doc__.split("\n")
+
+import os
+import sys
+import distutils.sysconfig
+
+CLASSIFIERS = """\
+Development Status :: 4 - Beta
+Intended Audience :: System Environment/Base
+License :: GPL
+Programming Language :: Python
+Operating System :: POSIX
+Operating System :: Unix
+"""
+
+# BEFORE importing distutils, remove MANIFEST. distutils doesn't properly
+# update it when the contents of directories change.
+if os.path.exists('MANIFEST'): os.remove('MANIFEST')
+
+def setup_package():
+
+ from distutils.core import setup
+
+ old_path = os.getcwd()
+ local_path = os.path.dirname(os.path.abspath(sys.argv[0]))
+ os.chdir(local_path)
+ sys.path.insert(0,local_path)
+
+ try:
+ setup(
+ name = "ipa-client",
+ version = "0.99.0",
+ license = "GPL",
+ author = "Simo Sorce",
+ author_email = "ssorce@redhat.com",
+ maintainer = "freeIPA Developers",
+ maintainer_email = "freeipa-devel@redhat.com",
+ url = "http://www.freeipa.org/",
+ description = DOCLINES[0],
+ long_description = "\n".join(DOCLINES[2:]),
+ download_url = "http://www.freeipa.org/page/Downloads",
+ classifiers=filter(None, CLASSIFIERS.split('\n')),
+ platforms = ["Linux"],
+ py_modules=['ipachangeconf']
+ )
+ finally:
+ del sys.path[0]
+ os.chdir(old_path)
+ return
+
+if __name__ == '__main__':
+ setup_package()
diff --git a/ipa-admintools/Makefile b/ipa-admintools/Makefile
new file mode 100644
index 00000000..43899ef2
--- /dev/null
+++ b/ipa-admintools/Makefile
@@ -0,0 +1,47 @@
+SUBDIRS=man
+SBINDIR = $(DESTDIR)/usr/sbin
+
+
+all: ;
+ @for subdir in $(SUBDIRS); do \
+ (cd $$subdir && $(MAKE) $@) || exit 1; \
+ done
+
+install:
+ install -m 755 ipa-adduser $(SBINDIR)
+ install -m 755 ipa-finduser $(SBINDIR)
+ install -m 755 ipa-moduser $(SBINDIR)
+ install -m 755 ipa-deluser $(SBINDIR)
+ install -m 755 ipa-lockuser $(SBINDIR)
+ install -m 755 ipa-addgroup $(SBINDIR)
+ install -m 755 ipa-delgroup $(SBINDIR)
+ install -m 755 ipa-findgroup $(SBINDIR)
+ install -m 755 ipa-modgroup $(SBINDIR)
+ install -m 755 ipa-passwd $(SBINDIR)
+ install -m 755 ipa-pwpolicy $(SBINDIR)
+ install -m 755 ipa-addservice $(SBINDIR)
+ install -m 755 ipa-delservice $(SBINDIR)
+ install -m 755 ipa-findservice $(SBINDIR)
+ install -m 755 ipa-adddelegation $(SBINDIR)
+ install -m 755 ipa-deldelegation $(SBINDIR)
+ install -m 755 ipa-listdelegation $(SBINDIR)
+ install -m 755 ipa-moddelegation $(SBINDIR)
+ install -m 755 ipa-defaultoptions $(SBINDIR)
+ install -m 755 ipa-change-master-key $(SBINDIR)
+
+ @for subdir in $(SUBDIRS); do \
+ (cd $$subdir && $(MAKE) $@) || exit 1; \
+ done
+
+install-man:
+ install -m 644
+
+clean:
+ rm -f *~ *.pyc
+
+distclean: clean
+ rm -f ipa-admintools.spec
+
+maintainer-clean: distclean
+
+test:
diff --git a/ipa-admintools/README b/ipa-admintools/README
new file mode 100644
index 00000000..6fc9db87
--- /dev/null
+++ b/ipa-admintools/README
@@ -0,0 +1,13 @@
+These tools are designed for administrators to work from a command-line,
+use in scripts, etc.
+
+The design goal is to provide 100% of capabilities that the UI has, something
+which is often not true.
+
+Developers
+----------
+
+These scripts use an XML-RPC interface to communicate with the IPA server.
+Please use only this API and avoid the temptation to communicate directly
+with the LDAP server. It is our philosophy to have a robust, standard
+interface for doing all IPA administrative work.
diff --git a/ipa-admintools/ipa-adddelegation b/ipa-admintools/ipa-adddelegation
new file mode 100644
index 00000000..4493390f
--- /dev/null
+++ b/ipa-admintools/ipa-adddelegation
@@ -0,0 +1,201 @@
+#! /usr/bin/python -E
+# Authors: Rob Crittenden <rcritten@redhat.com>
+#
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+import sys
+try:
+ from optparse import OptionParser
+ import ipa
+ import ipa.user
+ import ipa.ipaclient as ipaclient
+ import ipa.config
+ import ipa.aci
+ import ipa.ipaadminutil as ipaadminutil
+ import ipa.ipautil as ipautil
+
+ import xmlrpclib
+ import kerberos
+ import krbV
+ import ldap
+ import errno
+ import socket
+except ImportError:
+ print >> sys.stderr, """\
+There was a problem importing one of the required Python modules. The
+error was:
+
+ %s
+""" % sys.exc_value
+ sys.exit(1)
+
+def parse_options():
+ usage = "%prog -l|--list\n"
+ usage += "%prog -a|--attributes attr1,attr2,..,attrn -s|--source STRING -t|--target STRING [-v|--verbose] name"
+ parser = OptionParser(usage=usage, formatter=ipa.config.IPAFormatter())
+ parser.add_option("-a", "--attributes", dest="attributes",
+ help="The attributes the source group may change in the target group")
+ parser.add_option("-s", "--source", dest="source",
+ help="The source group name")
+ parser.add_option("-t", "--target", dest="target",
+ help="The target group name")
+ parser.add_option("-l", "--list", dest="list", action="store_true",
+ help="List common attributes (this is not an exhaustive list)")
+ parser.add_option("-v", "--verbose", action="store_true", dest="verbose",
+ help="Verbose output of the XML-RPC connection")
+
+ ipa.config.add_standard_options(parser)
+ options, args = parser.parse_args()
+
+ if options.list:
+ ipa.config.verify_args(parser, args)
+ else:
+ ipa.config.verify_args(parser, args, "name")
+ if not options.attributes or not options.source or not options.target:
+ parser.error("need attributes and both source and target groups")
+
+ ipa.config.init_config(options)
+
+ return options, args
+
+def main():
+
+ options, args = parse_options()
+
+ if options.list:
+ client = ipaclient.IPAClient(verbose=options.verbose)
+ l = client.get_all_attrs()
+
+ for x in l:
+ print x
+ return 0
+
+ client = ipaclient.IPAClient(verbose=options.verbose)
+
+ source_grp = client.find_groups(options.source)
+ counter = source_grp[0]
+ source_grp = source_grp[1:]
+ groupindex = -1
+ if counter == 0:
+ print "No entries found for %s" % options.source
+ return 2
+ elif counter == -1:
+ print "These results are truncated."
+ print "Please refine your search and try again."
+ return 3
+
+ if counter > 1:
+ print "\nMultiple entries for the source group found."
+ groupindex = ipaadminutil.select_group(counter, source_grp)
+ if groupindex == "q":
+ return 0
+
+ if groupindex >= 0:
+ source_grp = [source_grp[groupindex]]
+
+ target_grp = client.find_groups(options.target)
+ counter = target_grp[0]
+ target_grp = target_grp[1:]
+ groupindex = -1
+ if counter == 0:
+ print "No entries found for %s" % options.target
+ return 2
+ elif counter == -1:
+ print "These results are truncated."
+ print "Please refine your search and try again."
+ return 3
+
+ if counter > 1:
+ print "\nMultiple entries for the target group found."
+ groupindex = ipaadminutil.select_group(counter, target_grp)
+ if groupindex == "q":
+ return 0
+
+ if groupindex >= 0:
+ target_grp = [target_grp[groupindex]]
+
+ attr_list = options.attributes.split(',')
+
+ new_aci = ipa.aci.ACI()
+ new_aci.name = args[0]
+ new_aci.source_group = source_grp[0].dn
+ new_aci.dest_group = target_grp[0].dn
+ new_aci.attrs = attr_list
+
+ aci_entry = client.get_aci_entry(['*', 'aci'])
+
+ # Look for an existing ACI of the same name
+ aci_str_list = aci_entry.getValues('aci')
+ if aci_str_list is None:
+ aci_str_list = []
+ if not(isinstance(aci_str_list,list) or isinstance(aci_str_list,tuple)):
+ aci_str_list = [aci_str_list]
+
+ for aci_str in aci_str_list:
+ try:
+ old_aci = ipa.aci.ACI(aci_str)
+ if old_aci.name == new_aci.name:
+ print "A delegation of that name already exists"
+ return 2
+ except SyntaxError:
+ # ignore aci_str's that ACI can't parse
+ pass
+
+ aci_entry = client.get_aci_entry(['dn'])
+ aci_entry.setValue('aci', new_aci.export_to_string())
+
+ client.update_entry(aci_entry)
+
+ # Now add to the editors group so they can make changes in the UI
+ try:
+ group = client.get_entry_by_cn("editors")
+ client.add_group_to_group(new_aci.source_group, group.dn)
+ except ipa.ipaerror.exception_for(ipa.ipaerror.LDAP_EMPTY_MODLIST):
+ # This is ok, ignore it
+ pass
+
+ print "Delegation %s successfully added" % args[0]
+ return 0
+
+try:
+ if __name__ == "__main__":
+ sys.exit(main())
+except SystemExit, e:
+ sys.exit(e)
+except KeyboardInterrupt, e:
+ sys.exit(1)
+except xmlrpclib.Fault, fault:
+ if fault.faultCode == errno.ECONNREFUSED:
+ print "The IPA XML-RPC service is not responding."
+ else:
+ print fault.faultString
+ sys.exit(1)
+except kerberos.GSSError, e:
+ print "Could not initialize GSSAPI: %s/%s" % (ipautil.get_gsserror(e))
+ sys.exit(1)
+except xmlrpclib.ProtocolError, e:
+ print "Unable to connect to IPA server: %s" % (e.errmsg)
+ sys.exit(1)
+except ipa.ipaerror.IPAError, e:
+ print "%s" % (e.message)
+ sys.exit(1)
+except socket.error, e:
+ print e[1]
+ print "Re-run with -v flag for more details."
+except Exception, e:
+ print "%s" % str(e)
+ sys.exit(1)
diff --git a/ipa-admintools/ipa-addgroup b/ipa-admintools/ipa-addgroup
new file mode 100644
index 00000000..09ae9a67
--- /dev/null
+++ b/ipa-admintools/ipa-addgroup
@@ -0,0 +1,162 @@
+#! /usr/bin/python -E
+# Authors: Rob Crittenden <rcritten@redhat.com>
+#
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+import sys
+try:
+ from optparse import OptionParser
+ import ipa
+ import ipa.group
+ import ipa.ipaclient as ipaclient
+ import ipa.ipavalidate as ipavalidate
+ import ipa.ipautil as ipautil
+ import ipa.config
+ import ipa.ipaerror
+ import ipa.ipaadminutil as ipaadminutil
+
+ import xmlrpclib
+ import kerberos
+ import ldap
+ import errno
+ import socket
+except ImportError:
+ print >> sys.stderr, """\
+There was a problem importing one of the required Python modules. The
+error was:
+
+ %s
+""" % sys.exc_value
+ sys.exit(1)
+
+def set_add_usage(which):
+ print "%s option usage: --%s NAME=VALUE" % (which, which)
+
+def parse_options():
+ usage = "%prog [options] [group]"
+ parser = OptionParser(usage=usage)
+ parser.add_option("-d", "--description", dest="desc",
+ help="A description of this group")
+ parser.add_option("-g", "--gid", dest="gid",
+ help="The gid to use for this group. If not included one is automatically set.")
+ parser.add_option("-v", "--verbose", action="store_true", dest="verbose",
+ help="Verbose output of the XML-RPC connection")
+ parser.add_option("--addattr", dest="addattr",
+ help="Adds an attribute or values to that attribute, attr=value",
+ action="append")
+ parser.add_option("--setattr", dest="setattr",
+ help="Set an attribute, dropping any existing values that may exist",
+ action="append")
+
+ ipa.config.add_standard_options(parser)
+ options, args = parser.parse_args()
+
+ if len(args) > 1:
+ parser.error("too many arguments")
+
+ ipa.config.init_config(options)
+
+ return options, args
+
+def main():
+ cn = ""
+ desc = ""
+
+ group=ipa.group.Group()
+ options, args = parse_options()
+
+ if len(args) != 1:
+ cn = ipautil.user_input_name("Group name")
+ else:
+ cn = args[0]
+ try:
+ ipaadminutil.check_name(cn)
+ except ValueError, e:
+ print "Group name " + str(e)
+ return 1
+
+ if not options.desc:
+ desc = ipautil.user_input("Description", allow_empty = False)
+ else:
+ desc = options.desc
+ if not ipavalidate.String(desc, notEmpty=True):
+ print "Please enter a value"
+ return 1
+
+ if options.gid:
+ group.setValue('gidnumber', options.gid)
+
+ group.setValue('cn', cn)
+ group.setValue('description', desc)
+
+ if options.setattr:
+ for s in options.setattr:
+ s = s.split('=', 1)
+ if len(s) != 2:
+ set_add_usage("set")
+ sys.exit(1)
+ (attr,value) = s
+ group.setValue(attr, value)
+
+ if options.addattr:
+ for a in options.addattr:
+ a = a.split('=', 1)
+ if len(a) != 2:
+ set_add_usage("add")
+ sys.exit(1)
+ (attr,value) = a
+ cvalue = group.getValue(attr)
+ if cvalue:
+ if isinstance(cvalue,str):
+ cvalue = [cvalue]
+ value = cvalue + [value]
+ group.setValue(attr, value)
+
+ client = ipaclient.IPAClient(verbose=options.verbose)
+ client.add_group(group)
+ print cn + " successfully added"
+
+ return 0
+
+try:
+ if __name__ == "__main__":
+ sys.exit(main())
+except SystemExit, e:
+ sys.exit(e)
+except KeyboardInterrupt, e:
+ sys.exit(1)
+except xmlrpclib.Fault, fault:
+ if fault.faultCode == errno.ECONNREFUSED:
+ print "The IPA XML-RPC service is not responding."
+ else:
+ print fault.faultString
+ sys.exit(1)
+except kerberos.GSSError, e:
+ print "Could not initialize GSSAPI: %s/%s" % (ipautil.get_gsserror(e))
+ sys.exit(1)
+except xmlrpclib.ProtocolError, e:
+ print "Unable to connect to IPA server: %s" % (e.errmsg)
+ sys.exit(1)
+except ipa.ipaerror.IPAError, e:
+ print "%s" % (e.message)
+ sys.exit(1)
+except socket.error, e:
+ print e[1]
+ print "Re-run with -v flag for more details."
+except Exception, e:
+ print "%s" % str(e)
+ sys.exit(1)
diff --git a/ipa-admintools/ipa-addservice b/ipa-admintools/ipa-addservice
new file mode 100644
index 00000000..15105bc0
--- /dev/null
+++ b/ipa-admintools/ipa-addservice
@@ -0,0 +1,104 @@
+#! /usr/bin/python -E
+# Authors: Karl MacMillan <kmacmill@redhat.com>
+#
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+import sys
+try:
+ from optparse import OptionParser
+ import ipa
+ import ipa.user
+ import ipa.ipaclient as ipaclient
+ import ipa.ipautil as ipautil
+ import ipa.config
+
+ import base64
+
+ import xmlrpclib
+ import kerberos
+ import krbV
+ import ldap
+ import getpass
+ import errno
+ import socket
+except ImportError:
+ print >> sys.stderr, """\
+There was a problem importing one of the required Python modules. The
+error was:
+
+ %s
+""" % sys.exc_value
+ sys.exit(1)
+
+def parse_options():
+ usage = "%prog [options] principal"
+ parser = OptionParser(usage=usage)
+
+ parser.add_option("--force", action="store_true", default=False,
+ help="Force a service principal name")
+ parser.add_option("-v", "--verbose", action="store_true", dest="verbose",
+ help="Verbose output of the XML-RPC connection")
+
+ ipa.config.add_standard_options(parser)
+ options, args = parser.parse_args()
+ ipa.config.verify_args(parser, args, "principal")
+ ipa.config.init_config(options)
+
+ return options, args
+
+def main():
+ # The following fields are required
+ princ_name = ""
+
+ options, args = parse_options()
+
+ princ_name = args[0]
+
+ client = ipaclient.IPAClient(verbose=options.verbose)
+
+ client.add_service_principal(princ_name, "%d" % options.force)
+
+ return 0
+
+try:
+ if __name__ == "__main__":
+ sys.exit(main())
+except SystemExit, e:
+ sys.exit(e)
+except KeyboardInterrupt, e:
+ sys.exit(1)
+except xmlrpclib.Fault, fault:
+ if fault.faultCode == errno.ECONNREFUSED:
+ print "The IPA XML-RPC service is not responding."
+ else:
+ print fault.faultString
+ sys.exit(1)
+except kerberos.GSSError, e:
+ print "Could not initialize GSSAPI: %s/%s" % (ipautil.get_gsserror(e))
+ sys.exit(1)
+except xmlrpclib.ProtocolError, e:
+ print "Unable to connect to IPA server: %s" % (e.errmsg)
+ sys.exit(1)
+except ipa.ipaerror.IPAError, e:
+ print "%s" % (e.message)
+ sys.exit(1)
+except socket.error, e:
+ print e[1]
+ print "Re-run with -v flag for more details."
+except Exception, e:
+ print "%s" % str(e)
+ sys.exit(1)
diff --git a/ipa-admintools/ipa-adduser b/ipa-admintools/ipa-adduser
new file mode 100644
index 00000000..cf1f4324
--- /dev/null
+++ b/ipa-admintools/ipa-adduser
@@ -0,0 +1,290 @@
+#! /usr/bin/python -E
+# Authors: Rob Crittenden <rcritten@redhat.com>
+#
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+import sys
+try:
+ from optparse import OptionParser
+ import ipa
+ import ipa.user
+ import ipa.ipaclient as ipaclient
+ import ipa.ipavalidate as ipavalidate
+ import ipa.ipautil as ipautil
+ import ipa.config
+ import ipa.ipaadminutil as ipaadminutil
+
+ import xmlrpclib
+ import kerberos
+ import krbV
+ import ldap
+ import getpass
+ import errno
+ import socket
+except ImportError:
+ print >> sys.stderr, """\
+There was a problem importing one of the required Python modules. The
+error was:
+
+ %s
+""" % sys.exc_value
+ sys.exit(1)
+
+def set_add_usage(which):
+ print "%s option usage: --%s NAME=VALUE" % (which, which)
+
+def parse_options():
+ usage = "%prog [options] [user]"
+ parser = OptionParser(usage=usage)
+ parser.add_option("-c", "--gecos", dest="gecos",
+ help="Set the GECOS field")
+ parser.add_option("-d", "--directory", dest="directory",
+ help="Set the User's home directory")
+ parser.add_option("-f", "--firstname", dest="gn",
+ help="User's first name")
+ parser.add_option("-l", "--lastname", dest="sn",
+ help="User's last name")
+ parser.add_option("-p", "--password", dest="password",
+ help="Set user's password")
+ parser.add_option("-P", dest="password_prompt", action="store_true",
+ help="Prompt on the command-line for the user's password")
+ parser.add_option("-s", "--shell", dest="shell",
+ help="Set user's login shell to shell")
+ parser.add_option("-G", "--groups", dest="groups",
+ help="Add account to one or more groups (comma-separated)")
+ parser.add_option("-k", "--krb-principal", dest="principal",
+ help="Set user's Kerberos Principal Name")
+ parser.add_option("-M", "--mailAddress", dest="mail",
+ help="Set user's e-mail address")
+ parser.add_option("-v", "--verbose", action="store_true", dest="verbose",
+ help="Verbose output of the XML-RPC connection")
+ parser.add_option("--addattr", dest="addattr",
+ help="Adds an attribute or values to that attribute, attr=value",
+ action="append")
+ parser.add_option("--setattr", dest="setattr",
+ help="Set an attribute, dropping any existing values that may exist",
+ action="append")
+
+ ipa.config.add_standard_options(parser)
+ options, args = parser.parse_args()
+
+ if len(args) > 1:
+ parser.error("too many arguments")
+
+ ipa.config.init_config(options)
+
+ return options, args
+
+def main():
+ # The following fields are required
+ givenname = ""
+ lastname = ""
+ username = ""
+ principal = ""
+ password = ""
+ mail = ""
+ gecos = ""
+ directory = ""
+ shell = ""
+ groups = ""
+
+ match = False
+
+ all_interactive = False
+
+ user=ipa.user.User()
+ options, args = parse_options()
+
+ if len(args) != 1:
+ all_interactive = True
+
+ if not options.gn:
+ givenname = ipautil.user_input("First name", allow_empty = False)
+ else:
+ givenname = options.gn
+ if not ipavalidate.String(givenname, notEmpty=True):
+ print "Please enter a value"
+ return 1
+
+ if not options.sn:
+ lastname = ipautil.user_input("Last name", allow_empty = False)
+ else:
+ lastname = options.sn
+ if not ipavalidate.String(lastname, notEmpty=True):
+ print "Please enter a value"
+ return 1
+
+ if len(args) != 1:
+ username = ipautil.user_input_name("Login name")
+ else:
+ username = args[0]
+ try:
+ ipaadminutil.check_name(username)
+ except ValueError, e:
+ print "Login name " + str(e)
+ return 1
+
+ if options.password_prompt:
+ while match != True:
+ password = getpass.getpass(" Password: ")
+ confirm = getpass.getpass(" Password (again): ")
+ if password != confirm:
+ print "Passwords do not match"
+ match = False
+ else:
+ match = True
+ if len(password) < 1:
+ print "Password cannot be empty"
+ match = False
+ else:
+ password = options.password
+
+ if options.mail:
+ mail = options.mail
+ if not ipavalidate.Email(mail):
+ print "The email provided seem not a valid email."
+ return 1
+
+ # Ask the questions we don't normally force. We don't require answers
+ # for these.
+ if all_interactive is True:
+ if not options.gecos:
+ gecos = ipautil.user_input("gecos")
+ if not options.directory:
+ directory = ipautil.user_input_path("Home directory", "/home/" + username, allow_empty = True)
+ if not options.shell:
+ shell = ipautil.user_input("Shell", "/bin/sh", allow_empty = False)
+
+ else:
+ gecos = options.gecos
+ directory = options.directory
+ shell = options.shell
+ groups = options.groups
+
+ if options.principal:
+ principal = options.principal
+ else:
+ ctx = krbV.default_context()
+ principal = username + "@" + ctx.default_realm
+
+ user.setValue('givenname', givenname)
+ user.setValue('sn', lastname)
+ user.setValue('uid', username)
+ user.setValue('krbprincipalname', principal)
+ if mail:
+ user.setValue('mail', mail)
+ if gecos:
+ user.setValue('gecos', gecos)
+ if directory:
+ user.setValue('homedirectory', directory)
+ if shell:
+ user.setValue('loginshell', shell)
+
+ if options.setattr:
+ for s in options.setattr:
+ s = s.split('=', 1)
+ if len(s) != 2:
+ set_add_usage("set")
+ sys.exit(1)
+ (attr,value) = s
+ user.setValue(attr, value)
+
+ if options.addattr:
+ for a in options.addattr:
+ a = a.split('=', 1)
+ if len(a) != 2:
+ set_add_usage("add")
+ sys.exit(1)
+ (attr,value) = a
+ cvalue = user.getValue(attr)
+ if cvalue:
+ if isinstance(cvalue,str):
+ cvalue = [cvalue]
+ value = cvalue + [value]
+ user.setValue(attr, value)
+
+ client = ipaclient.IPAClient(verbose=options.verbose)
+
+ # get group dns and verify they exist
+ groups_to_add = []
+ if groups:
+ for group in groups.split(','):
+ group_dn = get_group_dn(client, group)
+ if not group_dn:
+ print "group %s doesn't exist" % group
+ return 1
+ groups_to_add.append(group_dn)
+
+ # add the user
+ client.add_user(user)
+
+ # add the user to all the groups
+ for group in groups_to_add:
+ client.add_user_to_group(username, group)
+
+ # Set the User's password
+ if password is not None:
+ try:
+ client.modifyPassword(principal, '', password)
+ except ipa.ipaerror.IPAError, e:
+ print "User added but setting the password failed."
+ print "%s" % (e.message)
+ return 1
+
+ print username + " successfully added"
+ return 0
+
+def get_group_dn(client, group_name):
+ if not group_name:
+ return None
+
+ found = client.find_groups(group_name)
+ if len(found) < 2:
+ return None
+ for group in found[1:]:
+ if group.cn == group_name:
+ return group.dn
+ return None
+
+try:
+ if __name__ == "__main__":
+ sys.exit(main())
+except SystemExit, e:
+ sys.exit(e)
+except KeyboardInterrupt, e:
+ sys.exit(1)
+except xmlrpclib.Fault, fault:
+ if fault.faultCode == errno.ECONNREFUSED:
+ print "The IPA XML-RPC service is not responding."
+ else:
+ print fault.faultString
+ sys.exit(1)
+except kerberos.GSSError, e:
+ print "Could not initialize GSSAPI: %s/%s" % (ipautil.get_gsserror(e))
+ sys.exit(1)
+except xmlrpclib.ProtocolError, e:
+ print "Unable to connect to IPA server: %s" % (e.errmsg)
+ sys.exit(1)
+except ipa.ipaerror.IPAError, e:
+ print "%s" % (e.message)
+ sys.exit(1)
+except socket.error, e:
+ print e[1]
+ print "Re-run with -v flag for more details."
+except Exception, e:
+ print "%s" % str(e)
+ sys.exit(1)
diff --git a/ipa-admintools/ipa-admintools.spec.in b/ipa-admintools/ipa-admintools.spec.in
new file mode 100644
index 00000000..ea03923a
--- /dev/null
+++ b/ipa-admintools/ipa-admintools.spec.in
@@ -0,0 +1,87 @@
+Name: ipa-admintools
+Version: __VERSION__
+Release: __RELEASE__%{?dist}
+Summary: IPA admin tools
+
+Group: System Environment/Base
+License: GPLv2
+URL: http://www.freeipa.org
+Source0: %{name}-%{version}.tgz
+BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n)
+BuildArch: noarch
+
+Requires: python
+Requires: python-krbV
+Requires: ipa-python
+Requires: python-ldap
+Requires: python-configobj
+
+%description
+User, group, delegation and policy administration tools for IPA. IPA is a
+server for identity, policy, and audit.
+
+%prep
+%setup -q
+
+%install
+rm -rf %{buildroot}
+mkdir -p %{buildroot}%{_sbindir}
+
+make install DESTDIR=%{buildroot}
+
+
+%clean
+rm -rf %{buildroot}
+
+
+%files
+%defattr(-,root,root,-)
+%{_sbindir}/ipa*
+%{_mandir}/man1/*
+
+%changelog
+* Fri May 23 2008 Rob Crittenden <rcritten@redhat.com> - 1.0.0-2
+- Add Requires for python-ldap and python-configobj
+
+* Thu Apr 3 2008 Rob Crittenden <rcritten@redhat.com> - 1.0.0-1
+- Version bump for release
+
+* Thu Feb 21 2008 Rob Crittenden <rcritten@redhat.com> - 0.99.0-1
+- Version bump for release
+
+* Thu Jan 31 2008 Rob Crittenden <rcritten@redhat.com> - 0.6.0-3
+- Marked with wrong license. IPA is GPLv2.
+
+* Thu Jan 17 2008 Rob Crittenden <rcritten@redhat.com> - 0.6.0-2
+- Fixed License in specfile
+
+* Fri Dec 21 2007 Karl MacMillan <kmacmill@redhat.com> - 0.6.0-1
+- Version bump for release.
+
+* Wed Nov 21 2007 Karl MacMillan <kmacmill@redhat.com> - 0.5.0-1
+- Version bump for release and rpm name change
+
+* Thu Nov 1 2007 Karl MacMillan <kmacmill@redhat.com> - 0.4.1-1
+- Version bump for release
+
+* Thu Oct 11 2007 Karl MacMillan <kmacmill@redhat.com> - 0.4.0-2
+- Package man files
+
+* Tue Oct 2 2007 Karl MacMillan <kmacmill@redhat.com> - 0.4.0-1
+- Milestone 4
+
+* Mon Sep 10 2007 Karl MacMillan <kmacmill@redhat.com> - 0.3.0-1
+- Milestone 3
+
+* Fri Aug 17 2007 Karl MacMillan <kmacmill@redhat.com> - 0.2.0-4
+- Package additional utilities.
+
+* Mon Aug 5 2007 Rob Crittenden <rcritten@redhat.com> - 0.1.0-3
+- Abstracted client class to work directly or over RPC
+
+* Wed Aug 1 2007 Rob Crittenden <rcritten@redhat.com> - 0.1.0-2
+- Update tools to do kerberos
+- Add User class
+
+* Fri Jul 27 2007 Karl MacMillan <kmacmill@localhost.localdomain> - 0.1.0-1
+- Initial rpm version
diff --git a/ipa-admintools/ipa-change-master-key b/ipa-admintools/ipa-change-master-key
new file mode 100644
index 00000000..a4e94399
--- /dev/null
+++ b/ipa-admintools/ipa-change-master-key
@@ -0,0 +1,387 @@
+#! /usr/bin/python -E
+# Authors: Simo Sorce <ssorce@redhat.com>
+#
+# Copyright (C) 2007 Simo Sorce <ssorce@redhat.com>
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 or later
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+etckrb5conf = "/etc/krb5.conf"
+krb5dir = "/var/kerberos/krb5kdc"
+cachedir = "/var/cache/ipa"
+libdir = "/var/lib/ipa"
+basedir = libdir+"/mkey"
+ourkrb5conf = basedir+"/krb5.conf"
+ldappwdfile = basedir+"/ldappwd"
+
+password = ""
+
+import sys
+try:
+ from optparse import OptionParser
+ import ipa
+ import ipa.config
+ import ipa.ipautil
+ from ipaclient import ipachangeconf
+ from ipaserver import ipaldap
+
+ import krbV
+
+ import ldap
+ from ldap import LDAPError
+ from ldap import ldapobject
+
+ from pyasn1.type import univ, namedtype
+ import pyasn1.codec.ber.encoder
+ import pyasn1.codec.ber.decoder
+ import struct
+ import base64
+
+ import random
+ import time
+ import os
+ import shutil
+ import getpass
+except ImportError:
+ print >> sys.stderr, """\
+There was a problem importing one of the required Python modules. The
+error was:
+
+ %s
+""" % sys.exc_value
+ sys.exit(1)
+
+def parse_options():
+ parser = OptionParser("%prog [-q|--quiet] [-p DM_PASSWORD]")
+ parser.add_option("-p", "--dm-password", dest="dm_password",
+ help="The Directory Manager password")
+ parser.add_option("-q", "--quiet", action="store_true", dest="quiet",
+ help="Keep quiet")
+
+ ipa.config.add_standard_options(parser)
+ options, args = parser.parse_args()
+
+ ipa.config.verify_args(parser, args)
+ ipa.config.init_config(options)
+
+ return options, args
+
+# We support only des3 encoded stash files for now
+def generate_new_stash_file(file):
+
+ odd_parity_bytes_pool = ['\x01', '\x02', '\x04', '\x07', '\x08', '\x0b',
+ '\r', '\x0e', '\x10', '\x13', '\x15', '\x16', '\x19', '\x1a', '\x1c',
+ '\x1f', ' ', '#', '%', '&', ')', '*', ',', '/', '1', '2', '4', '7', '8',
+ ';', '=', '>', '@', 'C', 'E', 'F', 'I', 'J', 'L', 'O', 'Q', 'R', 'T',
+ 'W', 'X', '[', ']', '^', 'a', 'b', 'd', 'g', 'h', 'k', 'm', 'n', 'p',
+ 's', 'u', 'v', 'y', 'z', '|', '\x7f', '\x80', '\x83', '\x85', '\x86',
+ '\x89', '\x8a', '\x8c', '\x8f', '\x91', '\x92', '\x94', '\x97', '\x98',
+ '\x9b', '\x9d', '\x9e', '\xa1', '\xa2', '\xa4', '\xa7', '\xa8', '\xab',
+ '\xad', '\xae', '\xb0', '\xb3', '\xb5', '\xb6', '\xb9', '\xba', '\xbc',
+ '\xbf', '\xc1', '\xc2', '\xc4', '\xc7', '\xc8', '\xcb', '\xcd', '\xce',
+ '\xd0', '\xd3', '\xd5', '\xd6', '\xd9', '\xda', '\xdc', '\xdf', '\xe0',
+ '\xe3', '\xe5', '\xe6', '\xe9', '\xea', '\xec', '\xef', '\xf1', '\xf2',
+ '\xf4', '\xf7', '\xf8', '\xfb', '\xfd', '\xfe']
+
+ pool_len = len(odd_parity_bytes_pool)
+ keytype = 16 # des3
+ keydata = ""
+
+ r = random.SystemRandom()
+ for k in range(24):
+ keydata += r.choice(odd_parity_bytes_pool)
+
+ format = '=hi%ss' % len(keydata)
+ s = struct.pack(format, keytype, len(keydata), keydata)
+ try:
+ fd = open(file, "w")
+ fd.write(s)
+ except os.error, e:
+ logging.critical("failed to write stash file")
+ raise e
+
+# clean up procedures
+def cleanup(password):
+ try:
+ os.stat(basedir)
+ except:
+ return None
+ try:
+ # always remove ldappwdfile as it contains the Directory Manager password
+ os.remove(ldappwdfile)
+ except:
+ pass
+
+ # tar and encrypt the working dir so that we do not leave sensitive data
+ # around unproteceted
+ curtime = time.strftime("%Y%m%d%H%M%S",time.gmtime())
+ tarfile = libdir+"/ipa-change-mkey-"+curtime+".tar"
+ gpgfile = tarfile+".gpg"
+ args = ['/bin/tar', '-C', libdir, '-cf', tarfile, 'mkey']
+ ipa.ipautil.run(args)
+ ipa.ipautil.encrypt_file(tarfile, gpgfile, password, cachedir)
+ os.remove(tarfile)
+ shutil.rmtree(basedir, ignore_errors=True)
+
+ return "The temporary working directory with backup dump files has been securely archived and gpg-encrypted as "+gpgfile+" using the Directory Manager password."
+
+def main():
+
+ global password
+
+ options, args = parse_options()
+
+ krbctx = krbV.default_context()
+
+ realm = krbctx.default_realm
+ suffix = ipa.ipautil.realm_to_suffix(realm)
+
+ backupfile = basedir+"/backup.dump"
+ convertfile = basedir+"/convert.dump"
+ oldstashfile = krb5dir+"/.k5."+realm
+ newstashfile = basedir+"/.new.mkey"
+ bkpstashfile = basedir+"/.k5."+realm
+
+ if os.getuid() != 0:
+ print "ERROR: This command must be run as root"
+ sys.exit(1)
+
+ print "DANGER: This is a dangerous operation, make sure you backup all your IPA data before running the tool"
+ print "This command will restart your Directory and KDC Servers."
+
+ #TODO: ask for confirmation
+ if not ipa.ipautil.user_input("Do you want to proceed and change the Kerberos Master key?", False):
+ print ""
+ print "Aborting..."
+ return 1
+
+ password = options.dm_password
+ if not password:
+ password = getpass.getpass("Directory Manager password: ")
+
+ # get a connection to the DS
+ try:
+ conn = ipaldap.IPAdmin(ipa.config.config.default_server[0])
+ conn.do_simple_bind(bindpw=password)
+ except Exception, e:
+ print "ERROR: Could not connect to the Directory Server on "+ipa.config.config.default_server[0]+" ("+str(e)+")"
+ return 1
+
+ # Wipe basedir and recreate it
+ shutil.rmtree(basedir, ignore_errors=True)
+ os.mkdir(basedir, 0700)
+
+ generate_new_stash_file(newstashfile)
+
+ # Generate conf files
+ try:
+ shutil.copyfile(etckrb5conf, ourkrb5conf)
+
+ krbconf = ipachangeconf.IPAChangeConf("IPA Installer")
+ krbconf.setOptionAssignment(" = ")
+ krbconf.setSectionNameDelimiters(("[","]"))
+ krbconf.setSubSectionDelimiters(("{","}"))
+ krbconf.setIndent((""," "," "))
+
+ #OPTS
+ opts = [{'name':'ldap_kadmind_dn', 'type':'option', 'action':'set', 'value':'cn=Directory Manager'},
+ {'name':'ldap_service_password_file', 'type':'option', 'action':'set', 'value':ldappwdfile}]
+
+ #REALM
+ realmopts = [{'name':realm, 'type':'subsection', 'action':'set', 'value':opts}]
+
+ #DBMODULES
+ dbopts = [{'name':'dbmodules', 'type':'section', 'action':'set', 'value':realmopts}]
+
+ krbconf.changeConf(ourkrb5conf, dbopts);
+
+ hexpwd = ""
+ for x in password:
+ hexpwd += (hex(ord(x))[2:])
+ pwd_fd = open(ldappwdfile, "w")
+ pwd_fd.write("cn=Directory Manager#{HEX}"+hexpwd+"\n")
+ pwd_fd.close()
+ os.chmod(ldappwdfile, 0600)
+
+ except Exception, e:
+ print "Failed to create custom configuration files ("+str(e)+") aborting..."
+ return 1
+
+ #Set environment vars so that the modified krb5.conf is used
+ os.environ['KRB5_CONFIG'] = ourkrb5conf
+
+ #Backup the kerberos key material for recovery if needed
+ args = ["/usr/kerberos/sbin/kdb5_util", "dump", "-verbose", backupfile]
+ print "Performing safety backup of the key material"
+ try:
+ output = ipa.ipautil.run(args)
+ except ipa.ipautil.CalledProcessError, e:
+ print "Failed to backup key material ("+str(e)+"), aborting ..."
+ return 1
+
+ if not options.quiet:
+ princlist = output[1].split('\n')
+ print "Principals stored into the backup file "+backupfile+":"
+ for p in princlist:
+ print p
+ print ""
+
+ #Convert the kerberos keys to the new master key
+ args = ["/usr/kerberos/sbin/kdb5_util", "dump", "-verbose", "-new_mkey_file", newstashfile, convertfile]
+ print "Converting key material to new master key"
+ try:
+ output = ipa.ipautil.run(args)
+ except ipa.ipautil.CalledProcessError, e:
+ print "Failed to convert key material, aborting ..."
+ return 1
+
+ savedprinclist = output[1].split('\n')
+
+ if not options.quiet:
+ princlist = output[1].split('\n')
+ print "Principals dumped for conversion:"
+ for p in princlist:
+ print p
+ print ""
+
+ #Stop the KDC
+ args = ["/etc/init.d/krb5kdc", "stop"]
+ try:
+ output = ipa.ipautil.run(args)
+ if output[0]:
+ print output[0]
+ if output[1]:
+ print output[1]
+ except ipa.ipautil.CalledProcessError, e:
+ print "WARNING: Failed to restart the KDC ("+str(e)+")"
+ print "You will have to manually restart the KDC when the operation is completed"
+
+ #Change the mkey into ldap
+ try:
+ stash = open(newstashfile, "r")
+ keytype = struct.unpack('h', stash.read(2))[0]
+ keylen = struct.unpack('i', stash.read(4))[0]
+ keydata = stash.read(keylen)
+
+ #encode it in the asn.1 attribute
+ MasterKey = univ.Sequence()
+ MasterKey.setComponentByPosition(0, univ.Integer(keytype))
+ MasterKey.setComponentByPosition(1, univ.OctetString(keydata))
+ krbMKey = univ.Sequence()
+ krbMKey.setComponentByPosition(0, univ.Integer(0)) #we have no kvno
+ krbMKey.setComponentByPosition(1, MasterKey)
+ asn1key = pyasn1.codec.ber.encoder.encode(krbMKey)
+
+ dn = "cn="+realm+",cn=kerberos,"+suffix
+ mod = [(ldap.MOD_REPLACE, 'krbMKey', str(asn1key))]
+ conn.modify_s(dn, mod)
+ except Exception, e:
+ print "ERROR: Failed to upload the Master Key from the Stash file: "+newstashfile+" ("+str(e)+")"
+ return 1
+
+ #Backup old stash file and substitute with new
+ try:
+ shutil.move(oldstashfile, bkpstashfile)
+ shutil.copyfile(newstashfile, oldstashfile)
+ except Exception, e:
+ print "ERROR: An error occurred while installing the new stash file("+str(e)+")"
+ print "The KDC may fail to start if the correct stash file is not in place"
+ print "Verify that "+newstashfile+" has been correctly installed into "+oldstashfile
+ print "A backup copy of the old stash file should be saved in "+bkpstashfile
+
+ #Finally upload the converted principals
+ args = ["/usr/kerberos/sbin/kdb5_util", "load", "-verbose", "-update", convertfile]
+ print "Uploading converted key material"
+ try:
+ output = ipa.ipautil.run(args)
+ except ipa.ipautil.CalledProcessError, e:
+ print "Failed to upload key material ("+e+"), aborting ..."
+ return 1
+
+ if not options.quiet:
+ princlist = output[1].split('\n')
+ print "Principals converted and uploaded:"
+ for p in princlist:
+ print p
+ print ""
+
+ uploadedprinclist = output[1].split('\n')
+
+ #Check for differences and report
+ d = []
+ for p in savedprinclist:
+ if uploadedprinclist.count(p) == 0:
+ d.append(p)
+ if len(d) != 0:
+ print "WARNING: Not all dumped principals have been updated"
+ print "Principals not Updated:"
+ for p in d:
+ print p
+
+ #Remove custom environ
+ del os.environ['KRB5_CONFIG']
+
+ #Restart Directory Server (the pwd plugin need to read the new mkey)
+ args = ["/etc/init.d/dirsrv", "restart"]
+ try:
+ output = ipa.ipautil.run(args)
+ if output[0]:
+ print output[0]
+ if output[1]:
+ print output[1]
+ except ipa.ipautil.CalledProcessError, e:
+ print "WARNING: Failed to restart the Directory Server ("+str(e)+")"
+ print "Please manually restart the DS with 'service dirsrv restart'"
+
+ #Restart the KDC
+ args = ["/etc/init.d/krb5kdc", "start"]
+ try:
+ output = ipa.ipautil.run(args)
+ if output[0]:
+ print output[0]
+ if output[1]:
+ print output[1]
+ except ipa.ipautil.CalledProcessError, e:
+ print "WARNING: Failed to restart the KDC ("+str(e)+")"
+ print "Please manually restart the kdc with 'service krb5kdc start'"
+
+ print "Master Password successfully changed"
+ print "You MUST now copy the stash file "+oldstashfile+" to all the replicas and restart them!"
+ print ""
+
+ return 0
+
+if __name__ == "__main__":
+ ret = 0
+ try:
+ ret = main()
+ except SystemExit, e:
+ ret = e
+ except KeyboardInterrupt, e:
+ ret = 1
+ except Exception, e:
+ print "%s" % str(e)
+ ret = 1
+
+ try:
+ msg = cleanup(password)
+ if msg:
+ print msg
+ except Exception, e:
+ print "Failed to clean up the temporary location for the dump files and generate and encrypted archive with error:"
+ print e
+ print "Please securely archive/encrypt "+basedir
+
+ sys.exit(ret)
diff --git a/ipa-admintools/ipa-defaultoptions b/ipa-admintools/ipa-defaultoptions
new file mode 100644
index 00000000..3cc94d4c
--- /dev/null
+++ b/ipa-admintools/ipa-defaultoptions
@@ -0,0 +1,181 @@
+#! /usr/bin/python -E
+# Authors: Rob Crittenden <rcritten@redhat.com>
+#
+# Copyright (C) 2008 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+import sys
+try:
+ from optparse import OptionParser
+ import ipa
+ import ipa.entity
+ import ipa.ipaclient as ipaclient
+ import ipa.config
+
+ import xmlrpclib
+ import kerberos
+ import errno
+ import validate
+ import socket
+except ImportError:
+ print >> sys.stderr, """\
+There was a problem importing one of the required Python modules. The
+error was:
+
+ %s
+""" % sys.exc_value
+ sys.exit(1)
+
+def parse_options():
+ usage = "%prog [options]\n"
+ usage += "%prog --show"
+ parser = OptionParser(usage=usage, formatter=ipa.config.IPAFormatter())
+ parser.add_option("--maxusername", dest="maxusername",
+ help="Max. Length of a username")
+ parser.add_option("--homedir", dest="homedir",
+ help="Default location of home directories")
+ parser.add_option("--defaultshell", dest="defaultshell",
+ help="Default shell for new users")
+ parser.add_option("--defaultgroup", dest="defaultgroup",
+ help="Default group for new users")
+ parser.add_option("--emaildomain", dest="emaildomain",
+ help="Default e-mail domain")
+ parser.add_option("--searchtimelimit", dest="searchtimelimit",
+ help="Max. amount of time (sec.) for a search (-1 is unlimited)")
+ parser.add_option("--searchrecordslimit", dest="searchrecordslimit",
+ help="Max. number of records to search (-1 is unlimited)")
+ parser.add_option("--usersearch", dest="usersearch",
+ help="A comma-separated list of fields to search when searching for users")
+ parser.add_option("--groupsearch", dest="groupsearch",
+ help="A comma-separated list of fields to search when searching for groups")
+ parser.add_option("--show", dest="show", action="store_true",
+ help="Show the current configuration")
+ parser.add_option("-v", "--verbose", action="store_true", dest="verbose",
+ help="Verbose output of the XML-RPC connection")
+
+ ipa.config.add_standard_options(parser)
+ options, args = parser.parse_args()
+ ipa.config.verify_args(parser, args)
+
+ if not options.show and not options.maxusername and not options.homedir and not options.defaultshell and not options.defaultgroup and not options.emaildomain and not options.searchtimelimit and not options.searchrecordslimit and not options.usersearch and not options.groupsearch:
+ parser.error("nothing to do")
+
+ ipa.config.init_config(options)
+
+ return options, args
+
+def show_config(client):
+ policy = client.get_ipa_config()
+ print "Search Configuration"
+ print " Search Time Limit (sec.): %s" % policy.getValues('ipaSearchTimeLimit')
+ print " Search Records Limit: %s" % policy.getValues('ipaSearchRecordsLimit')
+ print " User Search Fields: %s" % policy.getValues('ipaUserSearchFields')
+ print " Group Search Fields: %s" % policy.getValues('ipaGroupSearchFields')
+
+ print ""
+
+ print "User Settings"
+ print " Max. Username Length: %s" % policy.getValues('ipaMaxUsernameLength')
+ print " Root for Home Directories: %s" % policy.getValues('ipaHomesRootDir')
+ print " Default Shell: %s" % policy.getValues('ipaDefaultLoginShell')
+ print " Default User Group: %s" % policy.getValues('ipaDefaultPrimaryGroup')
+ print "Default E-mail Domain: %s" % policy.getValues('ipaDefaultEmailDomain')
+
+def update_policy(client, options):
+ current = client.get_ipa_config()
+
+ new = ipa.entity.Entity(current.toDict())
+
+ try:
+ if options.maxusername:
+ validate.is_integer(options.maxusername, min=1)
+ new.setValue('ipamaxusernamelength', options.maxusername)
+ if options.homedir:
+ validate.is_string(options.homedir)
+ new.setValue('ipahomesrootdir', options.homedir)
+ if options.defaultshell:
+ validate.is_string(options.defaultshell)
+ new.setValue('ipadefaultloginshell', options.history)
+ if options.defaultgroup:
+ new.setValue('ipadefaultprimarygroup', options.defaultgroup)
+ if options.emaildomain:
+ new.setValue('ipadefaultemaildomain', options.emaildomain)
+ if options.searchtimelimit:
+ validate.is_integer(options.searchtimelimit, min=-1)
+ new.setValue('ipasearchtimelimit', options.searchtimelimit)
+ if options.searchrecordslimit:
+ validate.is_integer(options.searchrecordslimit, min=-1)
+ new.setValue('ipasearchrecordslimit', options.searchrecordslimit)
+ if options.usersearch:
+ new.setValue('ipausersearchfields', options.usersearch)
+ if options.groupsearch:
+ new.setValue('ipagroupsearchfields', options.groupsearch)
+ except validate.VdtTypeError, e:
+ print "%s" % str(e)
+ return 1
+ except validate.VdtValueTooSmallError, e:
+ print "%s" % str(e)
+ return 1
+
+ client.update_ipa_config(new)
+
+ if options.usersearch or options.groupsearch:
+ print "WARNING: Be sure that the attributes in User and Group search are indexed in the Directory Server or you may suffer a performance loss."
+
+def main():
+ options, args = parse_options()
+
+ client = ipaclient.IPAClient(verbose=options.verbose)
+
+ if options.show:
+ show_config(client)
+ return 0
+
+ if update_policy(client, options):
+ return 1
+
+ print "Update successful."
+
+ return 0
+
+try:
+ if __name__ == "__main__":
+ sys.exit(main())
+except SystemExit, e:
+ sys.exit(e)
+except KeyboardInterrupt, e:
+ sys.exit(1)
+except xmlrpclib.Fault, fault:
+ if fault.faultCode == errno.ECONNREFUSED:
+ print "The IPA XML-RPC service is not responding."
+ else:
+ print fault.faultString
+ sys.exit(1)
+except kerberos.GSSError, e:
+ print "Could not initialize GSSAPI: %s/%s" % (e[0][0], e[0][1])
+ sys.exit(1)
+except xmlrpclib.ProtocolError, e:
+ print "Unable to connect to IPA server: %s" % (e.errmsg)
+ sys.exit(1)
+except ipa.ipaerror.IPAError, e:
+ print "%s" % (e.message)
+ sys.exit(1)
+except socket.error, e:
+ print e[1]
+ print "Re-run with -v flag for more details."
+except Exception, e:
+ print "%s" % str(e)
+ sys.exit(1)
diff --git a/ipa-admintools/ipa-deldelegation b/ipa-admintools/ipa-deldelegation
new file mode 100644
index 00000000..f11cf4b4
--- /dev/null
+++ b/ipa-admintools/ipa-deldelegation
@@ -0,0 +1,143 @@
+#! /usr/bin/python -E
+# Authors: Rob Crittenden <rcritten@redhat.com>
+#
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+import sys
+try:
+ from optparse import OptionParser
+ import ipa.ipaclient as ipaclient
+ import ipa.config
+ import ipa.ipautil as ipautil
+
+ import xmlrpclib
+ import kerberos
+ import copy
+ import errno
+ import socket
+
+ import ipa.aci
+ from ipa import ipaerror
+except ImportError:
+ print >> sys.stderr, """\
+There was a problem importing one of the required Python modules. The
+error was:
+
+ %s
+""" % sys.exc_value
+ sys.exit(1)
+
+aci_fields = ['*', 'aci']
+
+def parse_options():
+ usage = "%prog [-v|--verbose] name"
+ parser = OptionParser(usage=usage)
+ parser.add_option("-v", "--verbose", action="store_true", dest="verbose",
+ help="Verbose output of the XML-RPC connection")
+
+ ipa.config.add_standard_options(parser)
+ options, args = parser.parse_args()
+ ipa.config.verify_args(parser, args, "name")
+ ipa.config.init_config(options)
+
+ return options, args
+
+def main():
+
+ options, args = parse_options()
+
+ client = ipaclient.IPAClient(verbose=options.verbose)
+ aci_entry = client.get_aci_entry(aci_fields)
+
+ aci_str_list = aci_entry.getValues('aci')
+ if aci_str_list is None:
+ aci_str_list = []
+ if not(isinstance(aci_str_list,list) or isinstance(aci_str_list,tuple)):
+ aci_str_list = [aci_str_list]
+
+ acistr = None
+ aci_list = []
+ for aci_str in aci_str_list:
+ try:
+ aci = ipa.aci.ACI(aci_str)
+ if aci.name == args[0]:
+ acistr = aci_str
+ source_group = aci.source_group
+ else:
+ aci_list.append(aci)
+ except SyntaxError:
+ # ignore aci_str's that ACI can't parse
+ pass
+
+ if acistr is None:
+ print "No delegation '%s' found." % args[0]
+ return 2
+
+ old_aci_index = aci_str_list.index(acistr)
+
+ new_aci_str_list = copy.deepcopy(aci_str_list)
+ del new_aci_str_list[old_aci_index]
+ aci_entry.setValue('aci', new_aci_str_list)
+
+ client.update_entry(aci_entry)
+
+ last = True
+ # If this is the last delegation for a group, remove it from editors
+ for a in aci_list:
+ if source_group == a.source_group:
+ last = False
+ break
+
+ if last:
+ group = client.get_entry_by_cn("editors")
+ client.remove_member_from_group(source_group, group.dn)
+
+ print "Delegation removed."
+
+ return 0
+
+try:
+ if __name__ == "__main__":
+ sys.exit(main())
+except SystemExit, e:
+ sys.exit(e)
+except KeyboardInterrupt, e:
+ sys.exit(1)
+except xmlrpclib.Fault, fault:
+ if fault.faultCode == errno.ECONNREFUSED:
+ print "The IPA XML-RPC service is not responding."
+ else:
+ print fault.faultString
+ sys.exit(1)
+except (SyntaxError, ipaerror.IPAError), e:
+ print "Delegation deletion failed: " + str(e)
+ sys.exit(1)
+except kerberos.GSSError, e:
+ print "Could not initialize GSSAPI: %s/%s" % (ipautil.get_gsserror(e))
+ sys.exit(1)
+except xmlrpclib.ProtocolError, e:
+ print "Unable to connect to IPA server: %s" % (e.errmsg)
+ sys.exit(1)
+except ipa.ipaerror.IPAError, e:
+ print "%s" % (e.message)
+ sys.exit(1)
+except socket.error, e:
+ print e[1]
+ print "Re-run with -v flag for more details."
+except Exception, e:
+ print "%s" % str(e)
+ sys.exit(1)
diff --git a/ipa-admintools/ipa-delgroup b/ipa-admintools/ipa-delgroup
new file mode 100644
index 00000000..62ae3962
--- /dev/null
+++ b/ipa-admintools/ipa-delgroup
@@ -0,0 +1,109 @@
+#! /usr/bin/python -E
+# Authors: Rob Crittenden <rcritten@redhat.com>
+#
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+import sys
+try:
+ from optparse import OptionParser
+ import ipa
+ import ipa.ipaclient as ipaclient
+ import ipa.config
+ import ipa.ipautil as ipautil
+ import errno
+ import socket
+ import ldap
+
+ import xmlrpclib
+ import kerberos
+except ImportError:
+ print >> sys.stderr, """\
+There was a problem importing one of the required Python modules. The
+error was:
+
+ %s
+""" % sys.exc_value
+ sys.exit(1)
+
+def parse_options():
+ usage = "%prog [-v|--verbose] group"
+ parser = OptionParser(usage=usage)
+ parser.add_option("-v", "--verbose", action="store_true", dest="verbose",
+ help="Verbose output of the XML-RPC connection")
+
+ ipa.config.add_standard_options(parser)
+ options, args = parser.parse_args()
+ ipa.config.verify_args(parser, args, "group")
+ ipa.config.init_config(options)
+
+ return options, args
+
+def main():
+ options, args = parse_options()
+
+ client = ipaclient.IPAClient(verbose=options.verbose)
+ groups = client.find_groups(args[0], ['cn','description','gidnumber','nsAccountLock'])
+
+ counter = groups[0]
+ groups = groups[1:]
+ to_delete = None
+
+ for i in range(counter):
+ dn_list = ldap.explode_dn(groups[i].dn.lower())
+ if "cn=%s" % args[0].lower() in dn_list:
+ to_delete = groups[i]
+
+ if to_delete is None:
+ print "Group '%s' not found." % args[0]
+ return 2
+
+ ret = client.delete_group(to_delete.dn)
+ if (ret == "Success"):
+ print args[0] + " successfully deleted"
+ else:
+ print args[0] + " " + ret
+
+ return 0
+
+try:
+ if __name__ == "__main__":
+ sys.exit(main())
+except SystemExit, e:
+ sys.exit(e)
+except KeyboardInterrupt, e:
+ sys.exit(1)
+except xmlrpclib.Fault, fault:
+ if fault.faultCode == errno.ECONNREFUSED:
+ print "The IPA XML-RPC service is not responding."
+ else:
+ print fault.faultString
+ sys.exit(1)
+except kerberos.GSSError, e:
+ print "Could not initialize GSSAPI: %s/%s" % (ipautil.get_gsserror(e))
+ sys.exit(1)
+except xmlrpclib.ProtocolError, e:
+ print "Unable to connect to IPA server: %s" % (e.errmsg)
+ sys.exit(1)
+except ipa.ipaerror.IPAError, e:
+ print "%s" % (e.message)
+ sys.exit(1)
+except socket.error, e:
+ print e[1]
+ print "Re-run with -v flag for more details."
+except Exception, e:
+ print "%s" % str(e)
+ sys.exit(1)
diff --git a/ipa-admintools/ipa-delservice b/ipa-admintools/ipa-delservice
new file mode 100644
index 00000000..c295edc7
--- /dev/null
+++ b/ipa-admintools/ipa-delservice
@@ -0,0 +1,110 @@
+#! /usr/bin/python -E
+# Authors: Rob Crittenden <rcritten@redhat.com>
+#
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+import sys
+try:
+ from optparse import OptionParser
+ import ipa
+ import ipa.ipaclient as ipaclient
+ import ipa.config
+ import ipa.ipautil as ipautil
+
+ import xmlrpclib
+ import kerberos
+ import krbV
+ import ldap
+ import errno
+ import socket
+except ImportError:
+ print >> sys.stderr, """\
+There was a problem importing one of the required Python modules. The
+error was:
+
+ %s
+""" % sys.exc_value
+ sys.exit(1)
+
+def parse_options():
+ usage = "%prog [-v|--verbose] principal"
+ parser = OptionParser(usage=usage)
+ parser.add_option("-v", "--verbose", action="store_true", dest="verbose",
+ help="Verbose output of the XML-RPC connection")
+
+ ipa.config.add_standard_options(parser)
+ options, args = parser.parse_args()
+ ipa.config.verify_args(parser, args, "principal")
+ ipa.config.init_config(options)
+
+ return options, args
+
+def main():
+ # The following fields are required
+ princ_name = ""
+
+ options, args = parse_options()
+
+ princ_name = args[0]
+
+ client = ipaclient.IPAClient(verbose=options.verbose)
+
+ hosts = client.find_service_principal(args[0], sattrs=None)
+ counter = hosts[0]
+ hosts = hosts[1:]
+
+ if counter == 0:
+ print "Service Principal '%s' not found." % args[0]
+ return 2
+ if counter != 1:
+ print "An exact match was not found. Found %d principals for %s" % (counter, args[0])
+ return 2
+
+ client.delete_service_principal(hosts[0].dn)
+
+ print "Successfully deleted"
+
+ return 0
+
+try:
+ if __name__ == "__main__":
+ sys.exit(main())
+except SystemExit, e:
+ sys.exit(e)
+except KeyboardInterrupt, e:
+ sys.exit(1)
+except xmlrpclib.Fault, fault:
+ if fault.faultCode == errno.ECONNREFUSED:
+ print "The IPA XML-RPC service is not responding."
+ else:
+ print fault.faultString
+ sys.exit(1)
+except kerberos.GSSError, e:
+ print "Could not initialize GSSAPI: %s/%s" % (ipautil.get_gsserror(e))
+ sys.exit(1)
+except xmlrpclib.ProtocolError, e:
+ print "Unable to connect to IPA server: %s" % (e.errmsg)
+ sys.exit(1)
+except ipa.ipaerror.IPAError, e:
+ print "%s" % (e.message)
+ sys.exit(1)
+except socket.error, e:
+ print e[1]
+ print "Re-run with -v flag for more details."
+except Exception, e:
+ print "%s" % str(e)
+ sys.exit(1)
diff --git a/ipa-admintools/ipa-deluser b/ipa-admintools/ipa-deluser
new file mode 100644
index 00000000..4b0db7e6
--- /dev/null
+++ b/ipa-admintools/ipa-deluser
@@ -0,0 +1,90 @@
+#! /usr/bin/python -E
+# Authors: Rob Crittenden <rcritten@redhat.com>
+#
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+import sys
+try:
+ from optparse import OptionParser
+ import ipa
+ import ipa.ipaclient as ipaclient
+ import ipa.ipautil as ipautil
+ import ipa.config
+ import errno
+ import socket
+
+ import xmlrpclib
+ import kerberos
+except ImportError:
+ print >> sys.stderr, """\
+There was a problem importing one of the required Python modules. The
+error was:
+
+ %s
+""" % sys.exc_value
+ sys.exit(1)
+
+def parse_options():
+ usage = "%prog [-v|--verbose] user"
+ parser = OptionParser(usage=usage)
+ parser.add_option("-v", "--verbose", action="store_true", dest="verbose",
+ help="Verbose output of the XML-RPC connection")
+
+ ipa.config.add_standard_options(parser)
+ options, args = parser.parse_args()
+ ipa.config.verify_args(parser, args, "user")
+ ipa.config.init_config(options)
+
+ return options, args
+
+def main():
+ options, args = parse_options()
+
+ client = ipaclient.IPAClient(verbose=options.verbose)
+ ret = client.delete_user(args[0])
+ print args[0] + " successfully deleted"
+
+ return 0
+
+try:
+ if __name__ == "__main__":
+ sys.exit(main())
+except SystemExit, e:
+ sys.exit(e)
+except KeyboardInterrupt, e:
+ sys.exit(1)
+except xmlrpclib.Fault, fault:
+ if fault.faultCode == errno.ECONNREFUSED:
+ print "The IPA XML-RPC service is not responding."
+ else:
+ print fault.faultString
+ sys.exit(1)
+except kerberos.GSSError, e:
+ print "Could not initialize GSSAPI: %s/%s" % (ipautil.get_gsserror(e))
+ sys.exit(1)
+except xmlrpclib.ProtocolError, e:
+ print "Unable to connect to IPA server: %s" % (e.errmsg)
+ sys.exit(1)
+except ipa.ipaerror.IPAError, e:
+ print "%s" % (e.message)
+ sys.exit(1)
+except socket.error, e:
+ print e[1]
+ print "Re-run with -v flag for more details."
+except Exception, e:
+ print "%s" % str(e)
+ sys.exit(1)
diff --git a/ipa-admintools/ipa-findgroup b/ipa-admintools/ipa-findgroup
new file mode 100644
index 00000000..d9a9acc3
--- /dev/null
+++ b/ipa-admintools/ipa-findgroup
@@ -0,0 +1,157 @@
+#! /usr/bin/python -E
+# Authors: Rob Crittenden <rcritten@redhat.com>
+#
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+import sys
+try:
+ from optparse import OptionParser
+ import ipa.ipaclient as ipaclient
+ import ipa.ipaadminutil as ipaadminutil
+ import ipa.ipautil as ipautil
+ import ipa.config
+
+ import errno
+ import sys
+ import xmlrpclib
+ import kerberos
+ import socket
+except ImportError:
+ print >> sys.stderr, """\
+There was a problem importing one of the required Python modules. The
+error was:
+
+ %s
+""" % sys.exc_value
+ sys.exit(1)
+
+def parse_options():
+ usage = "%prog [options] group"
+ parser = OptionParser(usage=usage)
+
+ parser.add_option("-a", "--all", action="store_true", dest="all",
+ help="Show all group attributes")
+ parser.add_option("-n", "--notranslate", action="store_true",
+ dest="notranslate",
+ help="Don't translate LDAP attributes into readable labels")
+ parser.add_option("-v", "--verbose", action="store_true", dest="verbose",
+ help="Verbose output of the XML-RPC connection")
+
+ ipa.config.add_standard_options(parser)
+ options, args = parser.parse_args()
+ ipa.config.verify_args(parser, args, "group")
+ ipa.config.init_config(options)
+
+ return options, args
+
+def main():
+ group={}
+ options, args = parse_options()
+
+ client = ipaclient.IPAClient(verbose=options.verbose)
+ if options.all is None:
+ groups = client.find_groups(args[0], ['cn','description','gidnumber','nsAccountLock'])
+ else:
+ groups = client.find_groups(args[0], sattrs=['*','nsAccountLock'])
+
+ counter = groups[0]
+ groups = groups[1:]
+ groupindex = -1
+ if counter == 0:
+ print "No entries found for", args[0]
+ return 2
+ elif counter == -1:
+ print "These results are truncated."
+ print "Please refine your search and try again."
+
+ if counter > 1:
+ try:
+ groupindex = ipaadminutil.select_group(counter, groups)
+ except KeyboardInterrupt:
+ return 1
+ if groupindex == "q":
+ return 0
+
+ if groupindex >= 0:
+ groups = [groups[groupindex]]
+
+ for ent in groups:
+ try:
+ members = client.group_members(ent.dn, ['dn','cn'], 0)
+ except ipa.ipaerror.IPAError, e:
+ print "Error getting members for " + ent.dn
+ print str(e)
+ continue
+ attr = ent.attrList()
+ if options.notranslate:
+ labels = {}
+ for a in attr:
+ labels[a] = a
+ else:
+ labels = client.attrs_to_labels(attr)
+
+ print "dn: " + ent.dn
+
+ for a in attr:
+ value = ent.getValues(a)
+ if isinstance(value,str):
+ print labels[a] + ": " + value
+ else:
+ print labels[a] + ": "
+ for l in value:
+ print "\t" + l
+
+ counter = members[0]
+ members = members[1:]
+
+ if counter > 0:
+ print "Members:"
+ for m in members:
+ print " " + m.getValue('cn') + ": " + m.dn
+ # blank line between results
+ print
+
+ return 0
+
+try:
+ if __name__ == "__main__":
+ sys.exit(main())
+except SystemExit, e:
+ sys.exit(e)
+except KeyboardInterrupt, e:
+ sys.exit(1)
+except xmlrpclib.Fault, fault:
+ if fault.faultCode == errno.ECONNREFUSED:
+ print "The IPA XML-RPC service is not responding."
+ else:
+ print fault.faultString
+ sys.exit(1)
+except kerberos.GSSError, e:
+ print "Could not initialize GSSAPI: %s/%s" % (ipautil.get_gsserror(e))
+ sys.exit(1)
+except xmlrpclib.ProtocolError, e:
+ print "Unable to connect to IPA server: %s" % (e.errmsg)
+ sys.exit(1)
+except ipa.ipaerror.IPAError, e:
+ print "%s" % (e.message)
+ sys.exit(1)
+except socket.error, e:
+ print e[1]
+ print "Re-run with -v flag for more details."
+except Exception, e:
+ print "%s" % str(e)
+ sys.exit(1)
diff --git a/ipa-admintools/ipa-findservice b/ipa-admintools/ipa-findservice
new file mode 100644
index 00000000..c9aa4c05
--- /dev/null
+++ b/ipa-admintools/ipa-findservice
@@ -0,0 +1,105 @@
+#! /usr/bin/python -E
+# Authors: Rob Crittenden <rcritten@redhat.com>
+#
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+import sys
+try:
+ from optparse import OptionParser
+ import ipa.ipaclient as ipaclient
+ import ipa.config
+ import ipa.ipautil as ipautil
+ import ipa.ipaadminutil as ipaadminutil
+
+ import errno
+ import sys
+ import xmlrpclib
+ import kerberos
+ import socket
+except ImportError:
+ print >> sys.stderr, """\
+There was a problem importing one of the required Python modules. The
+error was:
+
+ %s
+""" % sys.exc_value
+ sys.exit(1)
+
+def parse_options():
+ usage = "%prog [-v|--verbose] host"
+ parser = OptionParser(usage=usage)
+
+ parser.add_option("-v", "--verbose", action="store_true", dest="verbose",
+ help="Verbose output of the XML-RPC connection")
+
+ ipa.config.add_standard_options(parser)
+ options, args = parser.parse_args()
+ ipa.config.verify_args(parser, args, "host")
+ ipa.config.init_config(options)
+
+ return options, args
+
+def main():
+ user={}
+ options, args = parse_options()
+
+ client = ipaclient.IPAClient(verbose=options.verbose)
+ hosts = client.find_service_principal(args[0], sattrs=None)
+
+ counter = hosts[0]
+ hosts = hosts[1:]
+ userindex = 0
+ if counter == 0:
+ print "No entries found for", args[0]
+ return 2
+ elif counter == -1:
+ print "These results are truncated."
+ print "Please refine your search and try again."
+
+ for ent in hosts:
+ print ent.krbprincipalname
+
+ return 0
+
+try:
+ if __name__ == "__main__":
+ sys.exit(main())
+except SystemExit, e:
+ sys.exit(e)
+except KeyboardInterrupt, e:
+ sys.exit(1)
+except xmlrpclib.Fault, fault:
+ if fault.faultCode == errno.ECONNREFUSED:
+ print "The IPA XML-RPC service is not responding."
+ else:
+ print fault.faultString
+ sys.exit(1)
+except kerberos.GSSError, e:
+ print "Could not initialize GSSAPI: %s/%s" % (ipautil.get_gsserror(e))
+ sys.exit(1)
+except xmlrpclib.ProtocolError, e:
+ print "Unable to connect to IPA server: %s" % (e.errmsg)
+ sys.exit(1)
+except ipa.ipaerror.IPAError, e:
+ print "%s" % (e.message)
+ sys.exit(1)
+except socket.error, e:
+ print e[1]
+ print "Re-run with -v flag for more details."
+except Exception, e:
+ print "%s" % str(e)
+ sys.exit(1)
diff --git a/ipa-admintools/ipa-finduser b/ipa-admintools/ipa-finduser
new file mode 100644
index 00000000..75df9493
--- /dev/null
+++ b/ipa-admintools/ipa-finduser
@@ -0,0 +1,177 @@
+#! /usr/bin/python -E
+# Authors: Rob Crittenden <rcritten@redhat.com>
+#
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+import sys
+try:
+ from optparse import OptionParser
+ import ipa.ipaclient as ipaclient
+ import ipa.config
+ import ipa.ipautil as ipautil
+ import ipa.ipaadminutil as ipaadminutil
+ import base64
+
+ import errno
+ import sys
+ import xmlrpclib
+ import kerberos
+ import socket
+except ImportError:
+ print >> sys.stderr, """\
+There was a problem importing one of the required Python modules. The
+error was:
+
+ %s
+""" % sys.exc_value
+ sys.exit(1)
+
+def parse_options():
+ usage = "%prog [-a|--all] [-n|--notranslate] [-v|--verbose] user"
+ parser = OptionParser(usage=usage)
+
+ parser.add_option("-a", "--all", action="store_true", dest="all",
+ help="Display all attributes")
+ parser.add_option("-n", "--notranslate", action="store_true",
+ dest="notranslate",
+ help="Don't translate LDAP attributes into readable labels")
+ parser.add_option("-v", "--verbose", action="store_true", dest="verbose",
+ help="Verbose output of the XML-RPC connection")
+
+ ipa.config.add_standard_options(parser)
+ options, args = parser.parse_args()
+ ipa.config.verify_args(parser, args, "user")
+ ipa.config.init_config(options)
+
+ return options, args
+
+def wrap_binary_data(data):
+ """Converts all binary data strings into base64-encoded objects for display
+ """
+ if isinstance(data, str):
+ if ipautil.needs_base64(data):
+ return base64.encodestring(data)
+ else:
+ return data
+ elif isinstance(data, list) or isinstance(data,tuple):
+ retval = []
+ for value in data:
+ retval.append(wrap_binary_data(value))
+ return retval
+ elif isinstance(data, dict):
+ retval = {}
+ for (k,v) in data.iteritems():
+ retval[k] = wrap_binary_data(v)
+ return retval
+ else:
+ return data
+
+def main():
+ user={}
+ options, args = parse_options()
+
+ client = ipaclient.IPAClient(verbose=options.verbose)
+ if options.all is None:
+ users = client.find_users(args[0], sattrs=['uid','givenname','sn','homeDirectory','loginshell'])
+ else:
+ users = client.find_users(args[0], sattrs=None)
+
+ counter = users[0]
+ users = users[1:]
+ userindex = 0
+ if counter == 0:
+ print "No entries found for", args[0]
+ return 2
+ elif counter == -1:
+ print "These results are truncated."
+ print "Please refine your search and try again."
+
+ if counter > 1:
+ try:
+ userindex = ipaadminutil.select_user(counter, users)
+ except KeyboardInterrupt:
+ return 1
+ if userindex == "q":
+ return 0
+
+ if userindex >= 0:
+ users = [users[userindex]]
+
+ for ent in users:
+ attr = ent.attrList()
+ attr.sort()
+
+ # Always have sn following givenname
+ try:
+ l = attr.index('givenname')
+ attr.remove('sn')
+ attr.insert(l+1, 'sn')
+ except ValueError:
+ pass
+
+ if options.notranslate:
+ labels = {}
+ for a in attr:
+ labels[a] = a
+ else:
+ labels = client.attrs_to_labels(attr)
+
+ if options.all is True:
+ print "dn: " + ent.dn
+
+ for a in attr:
+ value = ent.getValues(a)
+ if isinstance(value,str):
+ print labels[a] + ": " + str(wrap_binary_data(value)).rstrip()
+ else:
+ print labels[a] + ": "
+ for l in value:
+ print "\t" + wrap_binary_data(l)
+ # blank line between results
+ print
+
+ return 0
+
+try:
+ if __name__ == "__main__":
+ sys.exit(main())
+except SystemExit, e:
+ sys.exit(e)
+except KeyboardInterrupt, e:
+ sys.exit(1)
+except xmlrpclib.Fault, fault:
+ if fault.faultCode == errno.ECONNREFUSED:
+ print "The IPA XML-RPC service is not responding."
+ else:
+ print fault.faultString
+ sys.exit(1)
+except kerberos.GSSError, e:
+ print "Could not initialize GSSAPI: %s/%s" % (ipautil.get_gsserror(e))
+ sys.exit(1)
+except xmlrpclib.ProtocolError, e:
+ print "Unable to connect to IPA server: %s" % (e.errmsg)
+ sys.exit(1)
+except ipa.ipaerror.IPAError, e:
+ print "%s" % (e.message)
+ sys.exit(1)
+except socket.error, e:
+ print e[1]
+ print "Re-run with -v flag for more details."
+except Exception, e:
+ print "%s" % str(e)
+ sys.exit(1)
diff --git a/ipa-admintools/ipa-listdelegation b/ipa-admintools/ipa-listdelegation
new file mode 100644
index 00000000..46ddf3a3
--- /dev/null
+++ b/ipa-admintools/ipa-listdelegation
@@ -0,0 +1,140 @@
+#! /usr/bin/python -E
+# Authors: Rob Crittenden <rcritten@redhat.com>
+#
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+import sys
+try:
+ from optparse import OptionParser
+ import ipa.ipaclient as ipaclient
+ import ipa.config
+ import ipa.ipautil as ipautil
+
+ import operator
+ import xmlrpclib
+ import kerberos
+ import errno
+ import socket
+
+ import ipa.aci
+ from ipa import ipaerror
+except ImportError:
+ print >> sys.stderr, """\
+There was a problem importing one of the required Python modules. The
+error was:
+
+ %s
+""" % sys.exc_value
+ sys.exit(1)
+
+aci_fields = ['*', 'aci']
+
+def parse_options():
+ parser = OptionParser()
+ parser.add_option("-s", "--source", dest="source",
+ help="Source group of delegation")
+ parser.add_option("-n", "--name", dest="name",
+ help="Name of delegation")
+ parser.add_option("-t", "--target", dest="target",
+ help="Target group of delegation")
+ parser.add_option("-v", "--verbose", action="store_true", dest="verbose",
+ help="Verbose output of the XML-RPC connection")
+
+ ipa.config.add_standard_options(parser)
+ options, args = parser.parse_args()
+ ipa.config.verify_args(parser, args)
+ ipa.config.init_config(options)
+
+ return options, args
+
+def main():
+
+ options, args = parse_options()
+
+ all = True
+ if options.name or options.source or options.target:
+ all = False
+
+ client = ipaclient.IPAClient(verbose=options.verbose)
+ aci_entry = client.get_aci_entry(aci_fields)
+
+ aci_str_list = aci_entry.getValues('aci')
+ if aci_str_list is None:
+ aci_str_list = []
+ if not(isinstance(aci_str_list,list) or isinstance(aci_str_list,tuple)):
+ aci_str_list = [aci_str_list]
+
+ aci_list = []
+ for aci_str in aci_str_list:
+ try:
+ aci = ipa.aci.ACI(aci_str)
+ aci_list.append(aci)
+ except SyntaxError:
+ # ignore aci_str's that ACI can't parse
+ pass
+
+ group_dn_to_cn = ipa.aci.extract_group_cns(aci_list, client)
+
+ found = False
+ # the operator.itemgetter(0) lets us sort by the name field
+ for a in sorted(aci_list, key=operator.itemgetter(0)):
+ labels = client.attrs_to_labels(a.attrs)
+ if (all or options.name == a.name or
+ options.source == group_dn_to_cn[a.source_group] or
+ options.target == group_dn_to_cn[a.dest_group]):
+ print "Delegation Name: " + a.name
+ print "Group " + group_dn_to_cn[a.source_group]
+ print " can modify these attributes: "
+ for l in labels:
+ print "\t" + labels[l]
+ print " for group " + group_dn_to_cn[a.dest_group]
+ print
+ found = True
+
+ if found:
+ return 0
+ else:
+ return 2
+
+try:
+ if __name__ == "__main__":
+ sys.exit(main())
+except SystemExit, e:
+ sys.exit(e)
+except KeyboardInterrupt, e:
+ sys.exit(1)
+except xmlrpclib.Fault, fault:
+ if fault.faultCode == errno.ECONNREFUSED:
+ print "The IPA XML-RPC service is not responding."
+ else:
+ print fault.faultString
+ sys.exit(1)
+except kerberos.GSSError, e:
+ print "Could not initialize GSSAPI: %s/%s" % (ipautil.get_gsserror(e))
+ sys.exit(1)
+except xmlrpclib.ProtocolError, e:
+ print "Unable to connect to IPA server: %s" % (e.errmsg)
+ sys.exit(1)
+except ipaerror.IPAError, e:
+ print("Delegation list failed: " + str(e))
+ sys.exit(1)
+except socket.error, e:
+ print e[1]
+ print "Re-run with -v flag for more details."
+except Exception, e:
+ print "%s" % str(e)
+ sys.exit(1)
diff --git a/ipa-admintools/ipa-lockuser b/ipa-admintools/ipa-lockuser
new file mode 100644
index 00000000..85f2d5bf
--- /dev/null
+++ b/ipa-admintools/ipa-lockuser
@@ -0,0 +1,109 @@
+#! /usr/bin/python -E
+# Authors: Rob Crittenden <rcritten@redhat.com>
+#
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+import sys
+try:
+ from optparse import OptionParser
+ import ipa
+ import ipa.ipaclient as ipaclient
+ import ipa.config
+ import ipa.ipautil as ipautil
+ import errno
+ import socket
+
+ import xmlrpclib
+ import kerberos
+except ImportError:
+ print >> sys.stderr, """\
+There was a problem importing one of the required Python modules. The
+error was:
+
+ %s
+""" % sys.exc_value
+ sys.exit(1)
+
+def parse_options():
+ usage = "%prog [-u|--unlock] [-v|--verbose] user"
+ parser = OptionParser(usage=usage)
+ parser.add_option("-u", "--unlock", action="store_true", dest="unlock",
+ help="Unlock a user's account")
+ parser.add_option("-v", "--verbose", action="store_true", dest="verbose",
+ help="Verbose output of the XML-RPC connection")
+
+ ipa.config.add_standard_options(parser)
+ options, args = parser.parse_args()
+ ipa.config.verify_args(parser, args, "user")
+ ipa.config.init_config(options)
+
+ return options, args
+
+def main():
+ options, args = parse_options()
+
+ msg = "inactivated"
+ client = ipaclient.IPAClient(verbose=options.verbose)
+ if options.unlock:
+ try:
+ ret = client.mark_user_active(args[0])
+ msg = "unlocked"
+ except ipa.ipaerror.exception_for(ipa.ipaerror.LDAP_EMPTY_MODLIST):
+ print "User is already marked active"
+ return 0
+ except:
+ raise
+ else:
+ try:
+ ret = client.mark_user_inactive(args[0])
+ except ipa.ipaerror.exception_for(ipa.ipaerror.LDAP_EMPTY_MODLIST):
+ print "User is already marked inactive"
+ return 0
+ except:
+ raise
+ print args[0] + " successfully %s" % msg
+
+ return 0
+
+try:
+ if __name__ == "__main__":
+ sys.exit(main())
+except SystemExit, e:
+ sys.exit(e)
+except KeyboardInterrupt, e:
+ sys.exit(1)
+except xmlrpclib.Fault, fault:
+ if fault.faultCode == errno.ECONNREFUSED:
+ print "The IPA XML-RPC service is not responding."
+ else:
+ print fault.faultString
+ sys.exit(1)
+except kerberos.GSSError, e:
+ print "Could not initialize GSSAPI: %s/%s" % (ipautil.get_gsserror(e))
+ sys.exit(1)
+except xmlrpclib.ProtocolError, e:
+ print "Unable to connect to IPA server: %s" % (e.errmsg)
+ sys.exit(1)
+except ipa.ipaerror.IPAError, e:
+ print "%s" % (e.message)
+ sys.exit(1)
+except socket.error, e:
+ print e[1]
+ print "Re-run with -v flag for more details."
+except Exception, e:
+ print "%s" % str(e)
+ sys.exit(1)
diff --git a/ipa-admintools/ipa-moddelegation b/ipa-admintools/ipa-moddelegation
new file mode 100644
index 00000000..86e12c17
--- /dev/null
+++ b/ipa-admintools/ipa-moddelegation
@@ -0,0 +1,241 @@
+#! /usr/bin/python -E
+# Authors: Rob Crittenden <rcritten@redhat.com>
+#
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+import sys
+try:
+ from optparse import OptionParser
+ import ipa
+ import ipa.ipaclient as ipaclient
+ import ipa.ipaadminutil as ipaadminutil
+ import ipa.config
+ import ipa.aci
+ import ipa.ipautil as ipautil
+
+ import xmlrpclib
+ import kerberos
+ import krbV
+ import ldap
+ import copy
+ import errno
+ import socket
+except ImportError:
+ print >> sys.stderr, """\
+There was a problem importing one of the required Python modules. The
+error was:
+
+ %s
+""" % sys.exc_value
+ sys.exit(1)
+
+aci_fields = ['*', 'aci']
+
+def parse_options():
+ usage = "%prog -l|--list\n"
+ usage += "%prog [-a|--attributes attr1,attr2,..,attrn] [-s|--source STRING] [-t|--target STRING] [-v|--verbose] name"
+ parser = OptionParser(usage=usage, formatter=ipa.config.IPAFormatter())
+ parser.add_option("-a", "--attributes", dest="attributes",
+ help="The attributes the source group may change in the target group")
+ parser.add_option("-l", "--list", dest="list", action="store_true",
+ help="List common attributes (this is not an exhaustive list)")
+ parser.add_option("-s", "--source", dest="source",
+ help="The source group name")
+ parser.add_option("-t", "--target", dest="target",
+ help="The target group name")
+ parser.add_option("-v", "--verbose", action="store_true", dest="verbose",
+ help="Verbose output of the XML-RPC connection")
+
+ ipa.config.add_standard_options(parser)
+ options, args = parser.parse_args()
+
+ if not options.list:
+ if not options.attributes and not options.source and not options.target:
+ parser.error("need at least one option of -a, -s, or -t")
+ ipa.config.verify_args(parser, args, "name")
+
+ ipa.config.init_config(options)
+
+ return options, args
+
+def main():
+
+ options, args = parse_options()
+
+ if options.list:
+ client = ipaclient.IPAClient(verbose=options.verbose)
+ l = client.get_all_attrs()
+
+ for x in l:
+ print x
+ return 0
+
+ client = ipaclient.IPAClient(verbose=options.verbose)
+
+ # first do some sanity checking
+
+ if options.source:
+ source_grp = client.find_groups(options.source)
+ counter = source_grp[0]
+ source_grp = source_grp[1:]
+ groupindex = -1
+ if counter == 0:
+ print "No entries found for %s" % options.source
+ return 2
+ elif counter == -1:
+ print "These results are truncated."
+ print "Please refine your search and try again."
+ return 3
+
+ if counter > 1:
+ print "\nMultiple entries for the source group found."
+ groupindex = ipaadminutil.select_group(counter, source_grp)
+ if groupindex == "q":
+ return 0
+
+ if groupindex >= 0:
+ source_grp = [source_grp[groupindex]]
+
+ if options.target:
+ target_grp = client.find_groups(options.target)
+ counter = target_grp[0]
+ target_grp = target_grp[1:]
+ groupindex = -1
+ if counter == 0:
+ print "No entries found for %s" % options.target
+ return 2
+ elif counter == -1:
+ print "These results are truncated."
+ print "Please refine your search and try again."
+ return 3
+
+ if counter > 1:
+ print "\nMultiple entries for the target group found."
+ groupindex = ipaadminutil.select_group(counter, target_grp)
+ if groupindex == "q":
+ return 0
+
+ if groupindex >= 0:
+ target_grp = [target_grp[groupindex]]
+
+ if options.attributes:
+ attr_list = options.attributes.split(',')
+
+ # find the old aci
+
+ aci_entry = client.get_aci_entry(aci_fields)
+
+ aci_str_list = aci_entry.getValues('aci')
+ if aci_str_list is None:
+ aci_str_list = []
+ if not(isinstance(aci_str_list,list) or isinstance(aci_str_list,tuple)):
+ aci_str_list = [aci_str_list]
+
+ old_aci = None
+ acistr = None
+ aci_list = []
+ for aci_str in aci_str_list:
+ try:
+ old_aci = ipa.aci.ACI(aci_str)
+ if old_aci.name == args[0]:
+ acistr = aci_str
+ orig_group = old_aci.source_group
+ else:
+ aci_list.append(old_aci)
+ except SyntaxError:
+ # ignore aci_str's that ACI can't parse
+ pass
+
+ if acistr is None:
+ print "No delegation %s found." % args[0]
+ return 2
+
+ old_aci_index = aci_str_list.index(acistr)
+
+ new_aci = ipa.aci.ACI()
+ new_aci.name = args[0]
+ if options.source:
+ new_aci.source_group = source_grp[0].dn
+ else:
+ new_aci.source_group = old_aci.source_group
+ if options.target:
+ new_aci.dest_group = target_grp[0].dn
+ else:
+ new_aci.dest_group = old_aci.dest_group
+ if options.attributes:
+ new_aci.attrs = attr_list
+ else:
+ new_aci.attrs = old_aci.attrs
+ new_aci_str = new_aci.export_to_string()
+
+ new_aci_str_list = copy.deepcopy(aci_str_list)
+ new_aci_str_list[old_aci_index] = new_aci_str
+ aci_entry.setValue('aci', new_aci_str_list)
+
+ client.update_entry(aci_entry)
+
+ if options.source:
+ last = True
+ # If this is the last delegation for a group, remove it from editors
+ for a in aci_list:
+ if orig_group == a.source_group:
+ last = False
+ break
+
+ if last:
+ group = client.get_entry_by_cn("editors")
+ client.remove_member_from_group(orig_group, group.dn)
+
+ # Now add to the editors group so they can make changes in the UI
+ try:
+ group = client.get_entry_by_cn("editors")
+ client.add_group_to_group(new_aci.source_group, group.dn)
+ except ipa.ipaerror.exception_for(ipa.ipaerror.LDAP_EMPTY_MODLIST):
+ # This is ok, ignore it
+ pass
+
+ print "Delegation %s successfully updated" % args[0]
+ return 0
+
+try:
+ if __name__ == "__main__":
+ sys.exit(main())
+except SystemExit, e:
+ sys.exit(e)
+except KeyboardInterrupt, e:
+ sys.exit(1)
+except xmlrpclib.Fault, fault:
+ if fault.faultCode == errno.ECONNREFUSED:
+ print "The IPA XML-RPC service is not responding."
+ else:
+ print fault.faultString
+ sys.exit(1)
+except kerberos.GSSError, e:
+ print "Could not initialize GSSAPI: %s/%s" % (ipautil.get_gsserror(e))
+ sys.exit(1)
+except xmlrpclib.ProtocolError, e:
+ print "Unable to connect to IPA server: %s" % (e.errmsg)
+ sys.exit(1)
+except ipa.ipaerror.IPAError, e:
+ print "%s" % (e.message)
+ sys.exit(1)
+except socket.error, e:
+ print e[1]
+ print "Re-run with -v flag for more details."
+except Exception, e:
+ print "%s" % str(e)
+ sys.exit(1)
diff --git a/ipa-admintools/ipa-modgroup b/ipa-admintools/ipa-modgroup
new file mode 100644
index 00000000..6f5d7290
--- /dev/null
+++ b/ipa-admintools/ipa-modgroup
@@ -0,0 +1,234 @@
+#! /usr/bin/python -E
+# Authors: Rob Crittenden <rcritten@redhat.com>
+#
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+import sys
+try:
+ from optparse import OptionParser
+ import ipa
+ import ipa.group
+ import ipa.ipaclient as ipaclient
+ import ipa.config
+ import ipa.ipaerror
+
+ import xmlrpclib
+ import kerberos
+ import ldap
+ import errno
+ import socket
+except ImportError:
+ print >> sys.stderr, """\
+There was a problem importing one of the required Python modules. The
+error was:
+
+ %s
+""" % sys.exc_value
+ sys.exit(1)
+
+def set_add_usage(which):
+ print "%s option usage: --%s NAME=VALUE" % (which, which)
+
+def parse_options():
+ usage = "%prog [-l|--list]\n"
+ usage += "%prog [-a|--add] [-r|--remove] [-v|--verbose] user group\n"
+ usage += "%prog [-g|--groupadd] [-e|--groupdel] [-v|--verbose] group group\n"
+ usage += "%prog [-d|--desc description STRING] [--addattr attribute=value] [--delattr attribute] [--setattr attribute=value] [-v|--verbose] group"
+ parser = OptionParser(usage=usage, formatter=ipa.config.IPAFormatter())
+ parser.add_option("-a", "--add", dest="add", action="store_true",
+ help="Add a user to the group")
+ parser.add_option("-r", "--remove", dest="remove", action="store_true",
+ help="Remove a user from the group")
+ parser.add_option("-g", "--groupadd", dest="groupadd", action="store_true",
+ help="Add a group to the group")
+ parser.add_option("-e", "--groupdel", dest="groupdel", action="store_true",
+ help="Remove a group from the group")
+ parser.add_option("-d", "--description", dest="desc",
+ help="Modify the description of the group")
+ parser.add_option("--addattr", dest="addattr",
+ help="Adds an attribute or values to that attribute, attr=value",
+ action="append")
+ parser.add_option("--delattr", dest="delattr",
+ help="Remove an attribute", action="append")
+ parser.add_option("--setattr", dest="setattr",
+ help="Set an attribute, dropping any existing values that may exist",
+ action="append")
+ parser.add_option("-l", "--list", dest="list", action="store_true",
+ help="List common attributes (this is not an exhaustive list)")
+ parser.add_option("-v", "--verbose", action="store_true", dest="verbose",
+ help="Verbose output of the XML-RPC connection")
+
+ ipa.config.add_standard_options(parser)
+ options, args = parser.parse_args()
+
+ if ((not options.add and not options.remove) and
+ (not options.groupadd and not options.groupdel) and
+ (not options.desc and not options.addattr and
+ not options.delattr and not options.setattr and not options.list)):
+ parser.error("need to specify at least one operation")
+
+ if not options.list:
+ if options.add or options.remove:
+ ipa.config.verify_args(parser, args, "user group")
+ elif options.groupadd or options.groupdel:
+ ipa.config.verify_args(parser, args, "group group")
+ elif options.desc or options.addattr or options.delattr or options.setattr:
+ ipa.config.verify_args(parser, args, "group")
+
+ ipa.config.init_config(options)
+
+ return options, args
+
+def get_group(client, options, group_cn):
+ try:
+ attrs = ['*']
+
+ # in case any attributes being modified are operational such as
+ # nsaccountlock. Any attribute to be deleted needs to be included
+ # in the original record so it can be seen as being removed.
+ if options.delattr:
+ for d in options.delattr:
+ attrs.append(d)
+ group = client.get_entry_by_cn(group_cn, sattrs=attrs)
+
+ except ipa.ipaerror.IPAError, e:
+ print "%s" % e.message
+ return None
+
+ return group
+
+def main():
+ group=ipa.group.Group()
+ options, args = parse_options()
+
+ if options.list:
+ client = ipaclient.IPAClient(verbose=options.verbose)
+ list = client.get_all_attrs()
+
+ for x in list:
+ print x
+ return 0
+
+ client = ipaclient.IPAClient(verbose=options.verbose)
+ if options.add:
+ group = get_group(client, options, args[1])
+ if group is None:
+ return 1
+ users = args[0].split(',')
+ for user in users:
+ client.add_user_to_group(user, group.dn)
+ print user + " successfully added to " + args[1]
+ elif options.remove:
+ group = get_group(client, options, args[1])
+ if group is None:
+ return 1
+ users = args[0].split(',')
+ for user in users:
+ client.remove_user_from_group(user, group.dn)
+ print user + " successfully removed"
+ elif options.groupadd:
+ group = get_group(client, options, args[1])
+ if group is None:
+ return 1
+ groups = args[0].split(',')
+ for g in groups:
+ tgroup = get_group(client, options, g)
+ if tgroup is not None:
+ client.add_group_to_group(tgroup.dn, group.dn)
+ print g + " successfully added to " + args[1]
+ else:
+ print "Group %s not found" % g
+ elif options.groupdel:
+ group = get_group(client, options, args[1])
+ if group is None:
+ return 1
+ groups = args[0].split(',')
+ for g in groups:
+ tgroup = get_group(client, options, g)
+ if tgroup is not None:
+ client.remove_member_from_group(tgroup.dn, group.dn)
+ print g + " successfully removed " + args[1]
+ else:
+ print "Group %s not found" % g
+ else:
+ group = get_group(client, options, args[0])
+ if group is None:
+ return 1
+
+ if options.desc:
+ group.setValue('description', options.desc)
+
+ if options.delattr:
+ for d in options.delattr:
+ group.delValue(d)
+
+ if options.setattr:
+ for s in options.setattr:
+ s = s.split('=', 1)
+ if len(s) != 2:
+ set_add_usage("set")
+ sys.exit(1)
+ (attr,value) = s
+ group.setValue(attr, value)
+
+ if options.addattr:
+ for a in options.addattr:
+ a = a.split('=', 1)
+ if len(a) != 2:
+ set_add_usage("add")
+ sys.exit(1)
+ (attr,value) = a
+ cvalue = group.getValue(attr)
+ if cvalue:
+ if isinstance(cvalue,str):
+ cvalue = [cvalue]
+ value = cvalue + [value]
+ group.setValue(attr, value)
+
+ client.update_group(group)
+ print args[0] + " successfully updated"
+
+ return 0
+
+try:
+ if __name__ == "__main__":
+ sys.exit(main())
+except SystemExit, e:
+ sys.exit(e)
+except KeyboardInterrupt, e:
+ sys.exit(1)
+except xmlrpclib.Fault, fault:
+ if fault.faultCode == errno.ECONNREFUSED:
+ print "The IPA XML-RPC service is not responding."
+ else:
+ print fault.faultString
+ sys.exit(1)
+except kerberos.GSSError, e:
+ print "Could not initialize GSSAPI: %s/%s" % (ipautil.get_gsserror(e))
+ sys.exit(1)
+except xmlrpclib.ProtocolError, e:
+ print "Unable to connect to IPA server: %s" % (e.errmsg)
+ sys.exit(1)
+except ipa.ipaerror.IPAError, e:
+ print "%s" % (e.message)
+ sys.exit(1)
+except socket.error, e:
+ print e[1]
+ print "Re-run with -v flag for more details."
+except Exception, e:
+ print "%s" % str(e)
+ sys.exit(1)
diff --git a/ipa-admintools/ipa-moduser b/ipa-admintools/ipa-moduser
new file mode 100644
index 00000000..ec483a5d
--- /dev/null
+++ b/ipa-admintools/ipa-moduser
@@ -0,0 +1,256 @@
+#! /usr/bin/python -E
+# Authors: Rob Crittenden <rcritten@redhat.com>
+#
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+import sys
+try:
+ from optparse import OptionParser
+ import ipa
+ import ipa.user
+ import ipa.ipaclient as ipaclient
+ import ipa.ipavalidate as ipavalidate
+ import ipa.ipautil as ipautil
+ import ipa.config
+
+ import xmlrpclib
+ import kerberos
+ import ldap
+ import errno
+ import socket
+except ImportError:
+ print >> sys.stderr, """\
+There was a problem importing one of the required Python modules. The
+error was:
+
+ %s
+""" % sys.exc_value
+ sys.exit(1)
+
+def set_add_usage(which):
+ print "%s option usage: --%s NAME=VALUE" % (which, which)
+
+def parse_options():
+ usage = "%prog --list\n"
+ usage = "%prog [options] user"
+ parser = OptionParser(usage=usage, formatter=ipa.config.IPAFormatter())
+ parser.add_option("-a", "--activate", dest="activate", action="store_true",
+ help="Activate the user")
+ parser.add_option("-c", "--gecos", dest="gecos",
+ help="Set the GECOS field")
+ parser.add_option("-d", "--directory", dest="directory",
+ help="Set the User's home directory")
+ parser.add_option("-f", "--firstname", dest="gn",
+ help="User's first name")
+ parser.add_option("-l", "--lastname", dest="sn",
+ help="User's last name")
+ parser.add_option("-s", "--shell", dest="shell",
+ help="Set user's login shell to shell")
+ parser.add_option("--addattr", dest="addattr",
+ help="Adds an attribute or values to that attribute, attr=value",
+ action="append")
+ parser.add_option("--delattr", dest="delattr",
+ help="Remove an attribute", action="append")
+ parser.add_option("--setattr", dest="setattr",
+ help="Set an attribute, dropping any existing values that may exist",
+ action="append")
+ parser.add_option("--list", dest="list", action="store_true",
+ help="List common attributes (this is not an exhaustive list)")
+ parser.add_option("-M", "--mailAddress", dest="mail",
+ help="Set user's e-mail address")
+ parser.add_option("-v", "--verbose", action="store_true", dest="verbose",
+ help="Verbose output of the XML-RPC connection")
+
+ ipa.config.add_standard_options(parser)
+ options, args = parser.parse_args()
+
+ if not options.list:
+ ipa.config.verify_args(parser, args, "user")
+
+ ipa.config.init_config(options)
+
+ return options, args
+
+def main():
+ # The following fields are required
+ givenname = ""
+ lastname = ""
+ username = ""
+ mail = ""
+ gecos = ""
+ directory = ""
+ groups = ""
+ shell = ""
+
+ match = False
+
+ options, args = parse_options()
+
+ if options.list:
+ client = ipaclient.IPAClient(verbose=options.verbose)
+ list = client.get_all_attrs()
+
+ for x in list:
+ print x
+ return 0
+
+ username = args[0]
+
+ client = ipaclient.IPAClient(verbose=options.verbose)
+ try:
+ attrs = ['*']
+
+ # in case any attributes being modified are operational such as
+ # nsaccountlock. Any attribute to be deleted needs to be included
+ # in the original record so it can be seen as being removed.
+ if options.delattr:
+ for d in options.delattr:
+ attrs.append(d)
+ user = client.get_user_by_uid(username, sattrs=attrs)
+ except ipa.ipaerror.exception_for(ipa.ipaerror.LDAP_NOT_FOUND):
+ print "User %s not found" % username
+ return 1
+ except:
+ raise
+
+ # If any options are set we use just those. Otherwise ask for all of them.
+ if options.gn or options.sn or options.directory or options.gecos or options.mail or options.shell or options.addattr or options.delattr or options.setattr or options.activate:
+ givenname = options.gn
+ lastname = options.sn
+ gecos = options.gecos
+ directory = options.directory
+ mail = options.mail
+ shell = options.shell
+ else:
+ if not options.gn:
+ givenname = ipautil.user_input("First name", user.getValue('givenname'), allow_empty = False)
+ else:
+ givenname = options.gn
+ if (not ipavalidate.String(givenname, notEmpty=True)):
+ print "Please enter a value"
+ return 1
+
+ if not options.sn:
+ lastname = ipautil.user_input("Last name", user.getValue('sn'), allow_empty = False)
+ else:
+ lastname = options.sn
+ if (not ipavalidate.String(lastname, notEmpty=True)):
+ print "Please enter a value"
+ return 1
+
+ if not options.mail:
+ mail = ipautil.user_input_email("E-mail address", user.getValue('mail'), allow_empty = True)
+ else:
+ mail = options.mail
+ if (not ipavalidate.Email(mail)):
+ print "E-mail must include a user and domain name"
+ return 1
+
+ # Ask the questions we don't normally force. We don't require answers
+ # for these.
+ if not options.gecos:
+ gecos = ipautil.user_input("gecos", user.getValue('gecos'))
+
+ if not options.directory:
+ directory = ipautil.user_input_path("Home directory", user.getValue('homeDirectory'))
+ if not options.shell:
+ shell = ipautil.user_input("Shell", user.getValue('loginshell'), allow_empty = False)
+
+ if givenname:
+ user.setValue('givenname', givenname)
+ if lastname:
+ user.setValue('sn', lastname)
+ if mail:
+ user.setValue('mail', mail)
+
+ if gecos:
+ user.setValue('gecos', gecos)
+ if directory:
+ user.setValue('homedirectory', directory)
+ if shell:
+ user.setValue('loginshell', shell)
+
+ if options.delattr:
+ for d in options.delattr:
+ user.delValue(d)
+
+ if options.setattr:
+ for s in options.setattr:
+ s = s.split('=', 1)
+ if len(s) != 2:
+ set_add_usage("set")
+ sys.exit(1)
+ (attr,value) = s
+ user.setValue(attr, value)
+
+ if options.addattr:
+ for a in options.addattr:
+ a = a.split('=', 1)
+ if len(a) != 2:
+ set_add_usage("add")
+ sys.exit(1)
+ (attr,value) = a
+ cvalue = user.getValues(attr)
+ if cvalue:
+ if isinstance(cvalue,str):
+ cvalue = [cvalue]
+ value = cvalue + [value]
+ user.setValue(attr, value)
+
+ if options.activate:
+ try:
+ client.mark_user_active(user.getValues('uid'))
+ print "User activated successfully."
+ except ipa.ipaerror.exception_for(ipa.ipaerror.LDAP_EMPTY_MODLIST):
+ print "User is already marked active"
+ return 0
+ except:
+ raise
+
+ client.update_user(user)
+
+ print username + " successfully updated"
+ return 0
+
+try:
+ if __name__ == "__main__":
+ sys.exit(main())
+except SystemExit, e:
+ sys.exit(e)
+except KeyboardInterrupt, e:
+ sys.exit(1)
+except xmlrpclib.Fault, fault:
+ if fault.faultCode == errno.ECONNREFUSED:
+ print "The IPA XML-RPC service is not responding."
+ else:
+ print fault.faultString
+ sys.exit(1)
+except kerberos.GSSError, e:
+ print "Could not initialize GSSAPI: %s/%s" % (e[0][0], e[0][1])
+ sys.exit(1)
+except xmlrpclib.ProtocolError, e:
+ print "Unable to connect to IPA server: %s" % (e.errmsg)
+ sys.exit(1)
+except ipa.ipaerror.IPAError, e:
+ print "%s" % (e.message)
+ sys.exit(1)
+except socket.error, e:
+ print e[1]
+ print "Re-run with -v flag for more details."
+except Exception, e:
+ print "%s" % str(e)
+ sys.exit(1)
diff --git a/ipa-admintools/ipa-passwd b/ipa-admintools/ipa-passwd
new file mode 100644
index 00000000..616e6cac
--- /dev/null
+++ b/ipa-admintools/ipa-passwd
@@ -0,0 +1,146 @@
+#! /usr/bin/python -E
+# Authors: Rob Crittenden <rcritten@redhat.com>
+#
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Tempal Place, Suite 330, Boston, MA 02111-1307 USA
+#
+import sys
+try:
+ from optparse import OptionParser
+ import ipa
+ import ipa.ipaclient as ipaclient
+ import ipa.config
+
+ import xmlrpclib
+ import kerberos
+ import krbV
+ import ldap
+ import getpass
+ import errno
+ import socket
+except ImportError:
+ print >> sys.stderr, """\
+There was a problem importing one of the required Python modules. The
+error was:
+
+ %s
+""" % sys.exc_value
+ sys.exit(1)
+
+def parse_options():
+ usage = "ipa-passwd [-v|--verbose] [user]"
+ parser = OptionParser(usage=usage)
+ parser.add_option("-v", "--verbose", action="store_true", dest="verbose",
+ help="Verbose output of the XML-RPC connection")
+
+ ipa.config.add_standard_options(parser)
+ options, args = parser.parse_args()
+ if len(args) > 1:
+ parser.error("too many arguments")
+ ipa.config.init_config(options)
+
+ return options, args
+
+def get_principal(krbctx):
+ try:
+ ccache = krbctx.default_ccache()
+ cprinc = ccache.principal()
+ except krbV.Krb5Error, e:
+ #TODO: do a kinit
+ print "Unable to get kerberos principal: %s" % e[1]
+ return None
+
+ return cprinc.name
+
+def main():
+ match = False
+ username = None
+ principal = None
+
+ options, args = parse_options()
+
+ krbctx = krbV.default_context()
+
+ if len(args) == 1:
+ username = args[0]
+ else:
+ principal = get_principal(krbctx)
+ if principal is None:
+ return 1
+
+ if not principal:
+ u = username.split('@')
+ if len(u) > 2 or len(u) == 0:
+ print "Invalid user name (%s)" % username
+ if len(u) == 1:
+ principal = username+"@"+krbctx.default_realm
+ else:
+ principal = username
+
+ print "Changing password for %s" % principal
+
+ try:
+ while (match != True):
+ # No syntax checking of the password is required because that is
+ # done on the server side
+ password = getpass.getpass(" New Password: ")
+ confirm = getpass.getpass(" Confirm Password: ")
+ if (password != confirm):
+ print "Passwords do not match"
+ match = False
+ elif (len(password) < 1):
+ print "Password cannot be empty"
+ match = False
+ else:
+ match = True
+ except KeyboardInterrupt:
+ print ""
+ print "Password change cancelled"
+ return 1
+
+ client = ipaclient.IPAClient(verbose=options.verbose)
+ client.modifyPassword(principal, '', password)
+
+ return 0
+
+try:
+ if __name__ == "__main__":
+ sys.exit(main())
+except SystemExit, e:
+ sys.exit(e)
+except KeyboardInterrupt, e:
+ sys.exit(1)
+except xmlrpclib.Fault, fault:
+ if fault.faultCode == errno.ECONNREFUSED:
+ print "The IPA XML-RPC service is not responding."
+ else:
+ print fault.faultString
+ sys.exit(1)
+except kerberos.GSSError, e:
+ print "Could not initialize GSSAPI: %s/%s" % (e[0][0], e[0][1])
+ sys.exit(1)
+except xmlrpclib.ProtocolError, e:
+ print "Unable to connect to IPA server: %s" % (e.errmsg)
+ sys.exit(1)
+except ipa.ipaerror.IPAError, e:
+ print "%s" % (e.message)
+ sys.exit(1)
+except socket.error, e:
+ print e[1]
+ print "Re-run with -v flag for more details."
+except Exception, e:
+ print "%s" % str(e)
+ sys.exit(1)
diff --git a/ipa-admintools/ipa-pwpolicy b/ipa-admintools/ipa-pwpolicy
new file mode 100644
index 00000000..91b0f3ec
--- /dev/null
+++ b/ipa-admintools/ipa-pwpolicy
@@ -0,0 +1,153 @@
+#! /usr/bin/python -E
+# Authors: Rob Crittenden <rcritten@redhat.com>
+#
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+import sys
+try:
+ from optparse import OptionParser
+ import ipa
+ import ipa.entity
+ import ipa.ipaclient as ipaclient
+ import ipa.config
+
+ import xmlrpclib
+ import kerberos
+ import errno
+ import validate
+ import socket
+except ImportError:
+ print >> sys.stderr, """\
+There was a problem importing one of the required Python modules. The
+error was:
+
+ %s
+""" % sys.exc_value
+ sys.exit(1)
+
+def parse_options():
+ usage = "ipa-pwpolicy [--maxlife days] [--minlife hours] [--history number] [--minclasses number] [--minlength number] [-v|--verbose]\n"
+ usage += "ipa-pwpolicy --show"
+ parser = OptionParser(usage=usage, formatter=ipa.config.IPAFormatter())
+ parser.add_option("--maxlife", dest="maxlife",
+ help="Max. Password Lifetime (days)")
+ parser.add_option("--minlife", dest="minlife",
+ help="Min. Password Lifetime (hours)")
+ parser.add_option("--history", dest="history",
+ help="Password History Size")
+ parser.add_option("--minclasses", dest="minclasses",
+ help="Min. Number of Character Classes")
+ parser.add_option("--minlength", dest="minlength",
+ help="Min. Length of Password")
+ parser.add_option("--show", dest="show", action="store_true",
+ help="Show the current password policy")
+ parser.add_option("--usage", action="store_true",
+ help="Program usage")
+ parser.add_option("-v", "--verbose", action="store_true", dest="verbose",
+ help="Verbose output of the XML-RPC connection")
+
+ ipa.config.add_standard_options(parser)
+ options, args = parser.parse_args()
+
+ if not options.show:
+ if not options.maxlife and not options.minlife and not options.history and not options.minclasses and not options.minlength:
+ parser.error("need at least one option of --maxlife, --minlife, --history, --minclasses or --minlength")
+
+ ipa.config.init_config(options)
+
+ return options, args
+
+def show_policy(client):
+ policy = client.get_password_policy()
+ print "Password Policy"
+ print "Min. Password Lifetime (hours): %s" % policy.getValues('krbminpwdlife')
+ print "Max. Password Lifetime (days): %s" % policy.getValues('krbmaxpwdlife')
+ print "Min. Number of Character Classes: %s" % policy.getValues('krbpwdmindiffchars')
+ print "Min. Length of Password: %s" % policy.getValues('krbpwdminlength')
+ print "Password History Size: %s" % policy.getValues('krbpwdhistorylength')
+
+def update_policy(client, options):
+ current = client.get_password_policy()
+
+ new = ipa.entity.Entity(current.toDict())
+
+ try:
+ if options.maxlife:
+ validate.is_integer(options.maxlife, min=0)
+ new.setValue('krbmaxpwdlife', options.maxlife)
+ if options.minlife:
+ validate.is_integer(options.minlife, min=0)
+ new.setValue('krbminpwdlife', options.minlife)
+ if options.history:
+ validate.is_integer(options.history, min=0)
+ new.setValue('krbpwdhistorylength', options.history)
+ if options.minclasses:
+ validate.is_integer(options.minclasses, min=0)
+ new.setValue('krbpwdmindiffchars', options.minclasses)
+ if options.minlength:
+ validate.is_integer(options.minlength, min=0)
+ new.setValue('krbpwdminlength', options.minlength)
+ except (validate.VdtTypeError, validate.VdtValueTooSmallError), e:
+ print e
+ return 1
+
+ if int(new.getValue('krbminpwdlife')) > int(new.getValue('krbmaxpwdlife')) * 24:
+ print "Maximum password life must be greater than minimum"
+ return 1
+
+ client.update_password_policy(new)
+ return 0
+
+def main():
+ options, args = parse_options()
+
+ client = ipaclient.IPAClient(verbose=options.verbose)
+
+ if options.show:
+ show_policy(client)
+ return 0
+
+ return update_policy(client, options)
+
+try:
+ if __name__ == "__main__":
+ sys.exit(main())
+except SystemExit, e:
+ sys.exit(e)
+except KeyboardInterrupt, e:
+ sys.exit(1)
+except xmlrpclib.Fault, fault:
+ if fault.faultCode == errno.ECONNREFUSED:
+ print "The IPA XML-RPC service is not responding."
+ else:
+ print fault.faultString
+ sys.exit(1)
+except kerberos.GSSError, e:
+ print "Could not initialize GSSAPI: %s/%s" % (e[0][0], e[0][1])
+ sys.exit(1)
+except xmlrpclib.ProtocolError, e:
+ print "Unable to connect to IPA server: %s" % (e.errmsg)
+ sys.exit(1)
+except ipa.ipaerror.IPAError, e:
+ print "%s" % (e.message)
+ sys.exit(1)
+except socket.error, e:
+ print e[1]
+ print "Re-run with -v flag for more details."
+except Exception, e:
+ print "%s" % str(e)
+ sys.exit(1)
diff --git a/ipa-admintools/man/Makefile b/ipa-admintools/man/Makefile
new file mode 100644
index 00000000..506dbb43
--- /dev/null
+++ b/ipa-admintools/man/Makefile
@@ -0,0 +1,32 @@
+MANDIR = $(DESTDIR)/usr/share/man
+
+MANFILES=\
+ ipa-adddelegation.1 \
+ ipa-addgroup.1 \
+ ipa-addservice.1 \
+ ipa-adduser.1 \
+ ipa-deldelegation.1 \
+ ipa-delgroup.1 \
+ ipa-delservice.1 \
+ ipa-deluser.1 \
+ ipa-findgroup.1 \
+ ipa-findservice.1 \
+ ipa-finduser.1 \
+ ipa-modgroup.1 \
+ ipa-listdelegation.1 \
+ ipa-lockuser.1 \
+ ipa-moddelegation.1 \
+ ipa-passwd.1 \
+ ipa-pwpolicy.1 \
+ ipa-moduser.1 \
+ ipa-defaultoptions.1
+
+all: ;
+
+install:
+ mkdir -p $(MANDIR)/man1
+ @for i in $(MANFILES) ; do install -m 644 $$i $(MANDIR)/man1 ; gzip -f $(MANDIR)/man1/$$i ; done
+
+clean:
+
+test:
diff --git a/ipa-admintools/man/ipa-adddelegation.1 b/ipa-admintools/man/ipa-adddelegation.1
new file mode 100644
index 00000000..4db109f6
--- /dev/null
+++ b/ipa-admintools/man/ipa-adddelegation.1
@@ -0,0 +1,62 @@
+.\" A man page for ipa-adddelegation
+.\" Copyright (C) 2007 Red Hat, Inc.
+.\"
+.\" This is free software; you can redistribute it and/or modify it under
+.\" the terms of the GNU Library General Public License as published by
+.\" the Free Software Foundation; version 2 only
+.\"
+.\" This program is distributed in the hope that it will be useful, but
+.\" WITHOUT ANY WARRANTY; without even the implied warranty of
+.\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+.\" General Public License for more details.
+.\"
+.\" You should have received a copy of the GNU Library General Public
+.\" License along with this program; if not, write to the Free Software
+.\" Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+.\"
+.\" Author: Rob Crittenden <rcritten@redhat.com>
+.\"
+.TH "ipa-adddelegation" "1" "Oct 24 2007" "freeipa" ""
+.SH "NAME"
+ipa\-adddelegation \- Add a delegation
+
+.SH "SYNOPSIS"
+ipa\-adddelegation [\fIOPTION\fR]... \fIname\fR
+
+.SH "DESCRIPTION"
+Adds a delegation named \fIname\fR.
+
+A delegation is used to grant write access to certain attributes from one group to another.
+
+For example, a secretary group may be granted write access to modify the phone attribute of all users in a manager's group.
+.SH "OPTIONS"
+.TP
+\fB\-a\fR, \fB\-\-attributes\fR=\fIATTRIBUTES\fR
+A comma\-separated list of the \f[SM]attributes\fR that may be written by the source group.
+.TP
+\fB\-l\fR
+Provide a list of common attribute names. This is not an exhaustive list.
+.TP
+\fB\-s\fR, \fB\-\-source\fR=\fISOURCE\fR
+The name of the group that is being granted write permission.
+.TP
+\fB\-t\fR, \fB\-\-target\fR=\fITARGET\fR
+The name of the group that will be written to.
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+Display the XML\-RPC request and response for more verbose debugging output
+.PP
+All arguments except \-v/\-\-verbose are mandatory.
+.SH "EXAMPLES"
+.TP
+ipa\-adddelegation \-a telephonenumber,facsimiletelephonenumber,mobile \-s secretaries \-t everyone phones
+
+Create a delegation named \fBphones\fR that will let anyone in the \fIsecretaries\fR group edit the phone numbers of anyone in the group \fIeveryone\fR.
+.SH "EXIT STATUS"
+0 if the delegation was added successfully
+
+1 if an error occurred
+
+2 if no not exactly one matching source group was found (0 or more than one)
+
+3 if no not exactly one matching target group was found (0 or more than one
diff --git a/ipa-admintools/man/ipa-addgroup.1 b/ipa-admintools/man/ipa-addgroup.1
new file mode 100644
index 00000000..0e61c619
--- /dev/null
+++ b/ipa-admintools/man/ipa-addgroup.1
@@ -0,0 +1,51 @@
+.\" A man page for ipa-addgroup
+.\" Copyright (C) 2007 Red Hat, Inc.
+.\"
+.\" This is free software; you can redistribute it and/or modify it under
+.\" the terms of the GNU Library General Public License as published by
+.\" the Free Software Foundation; version 2 only
+.\"
+.\" This program is distributed in the hope that it will be useful, but
+.\" WITHOUT ANY WARRANTY; without even the implied warranty of
+.\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+.\" General Public License for more details.
+.\"
+.\" You should have received a copy of the GNU Library General Public
+.\" License along with this program; if not, write to the Free Software
+.\" Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+.\"
+.\" Author: Rob Crittenden <rcritten@redhat.com>
+.\"
+.TH "ipa-addgroup" "1" "Oct 10 2007" "freeipa" ""
+.SH "NAME"
+ipa\-addgroup \- Add a group
+
+.SH "SYNOPSIS"
+ipa\-addgroup [\fIOPTION\fR]... \fIgroup\fR
+
+.SH "DESCRIPTION"
+Adds a group with name \fIgroup\fR.
+.SH "OPTIONS"
+.TP
+\fB\-d\fR, \fB\-\-description\fR=\fIdescription\fR
+Set the description of the group to \fIdescription\fR.
+.TP
+\fB\-g\fR, \fB\-\-gid\fR=\fIgid\fR
+Set the gid for this group to \fIgid\fR.
+If this option is not present, one is created automatically
+by \fBfreeIPA\fR.
+.TP
+\fB\-\-addattr\fR \fIattr=value\fR
+Adds \fIvalue\fR to attribute \fIattr\fR. Attributes set this way are done after other options. If an attribute is listed more than once or already exists in the entry, it is considered a multi\-valued attribute and a list of the values is created.
+.TP
+\fB\-\-setattr\fR \fIattr=value\fR
+Set attribute \fIattr\fR to \fIvalue\fR. Any existing value will be replaced with \fIvalue\fR.
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+Display the XML\-RPC request and response for more verbose debugging output.
+.PP
+The group name and description are mandatory fields. If either of these are not included on the command line you will be asked interactively.
+
+If no options are passed then all questions are asked.
+.SH "EXIT STATUS"
+The exit status is 0 on success, nonzero on error.
diff --git a/ipa-admintools/man/ipa-addservice.1 b/ipa-admintools/man/ipa-addservice.1
new file mode 100644
index 00000000..54550572
--- /dev/null
+++ b/ipa-admintools/man/ipa-addservice.1
@@ -0,0 +1,59 @@
+.\" A man page for ipa-addservice
+.\" Copyright (C) 2007 Red Hat, Inc.
+.\"
+.\" This is free software; you can redistribute it and/or modify it under
+.\" the terms of the GNU Library General Public License as published by
+.\" the Free Software Foundation; version 2 only
+.\"
+.\" This program is distributed in the hope that it will be useful, but
+.\" WITHOUT ANY WARRANTY; without even the implied warranty of
+.\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+.\" General Public License for more details.
+.\"
+.\" You should have received a copy of the GNU Library General Public
+.\" License along with this program; if not, write to the Free Software
+.\" Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+.\"
+.\" Author: Rob Crittenden <rcritten@redhat.com>
+.\"
+.TH "ipa-addservice" "1" "Jan 4 2008" "freeipa" ""
+.SH "NAME"
+ipa\-addservice \- Add a service principal
+
+.SH "SYNOPSIS"
+ipa\-addservice [\fIOPTION\fR]... \fIprincipal\fR
+.SH "DESCRIPTION"
+Adds a service principal \fIprincipal\fR.
+
+The principal takes the form of:
+
+service/fully\-qualified\-hostname
+
+The list of possible services is too extensive to list here but a short list is:
+
+cifs
+dns
+host
+HTTP
+ldap
+nfs
+
+The IPA server automatically appends the Kerberos realm for which it is configured. You cannot specify a different realm.
+
+The hostname must resolve to a DNS A record in order to ensure that it will work with Kerberos. Use the \-\-force flag to force the creation of a principal.
+.SH "OPTIONS"
+.TP
+\fB\-\-force\fR
+Force the creation of the given principal name.
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+Display the XML\-RPC request and response for more verbose debugging output.
+.SH "EXAMPLES"
+.TP
+ipa\-addservice HTTP/www.example.com
+Add a service principal for a web server
+.TP
+ipa\-addservice host/ipa.example.com
+Add a service principal for the host (for ssh, for example)
+.SH "EXIT STATUS"
+The exit status is 0 on success, nonzero on error.
diff --git a/ipa-admintools/man/ipa-adduser.1 b/ipa-admintools/man/ipa-adduser.1
new file mode 100644
index 00000000..0b8ad3e3
--- /dev/null
+++ b/ipa-admintools/man/ipa-adduser.1
@@ -0,0 +1,84 @@
+.\" A man page for ipa-adduser
+.\" Copyright (C) 2007 Red Hat, Inc.
+.\"
+.\" This is free software; you can redistribute it and/or modify it under
+.\" the terms of the GNU Library General Public License as published by
+.\" the Free Software Foundation; version 2 only
+.\"
+.\" This program is distributed in the hope that it will be useful, but
+.\" WITHOUT ANY WARRANTY; without even the implied warranty of
+.\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+.\" General Public License for more details.
+.\"
+.\" You should have received a copy of the GNU Library General Public
+.\" License along with this program; if not, write to the Free Software
+.\" Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+.\"
+.\" Author: Rob Crittenden <rcritten@redhat.com>
+.\"
+.TH "ipa-adduser" "1" "Oct 10 2007" "freeipa" ""
+.SH "NAME"
+ipa\-adduser \- Add a user
+
+.SH "SYNOPSIS"
+ipa\-adduser [\fIOPTION\fR]... \fIuser\fR
+
+.SH "DESCRIPTION"
+Adds a user with username \fIuser\fR.
+
+.SH "OPTIONS"
+.TP
+\fB\-c\fR, \fB\-\-gecos\fR=\fIgecos\fR
+Set the \f[SM]GECOS\fR field to \fIgecos\fR.
+The \f[SM]GECOS\fR field is traditionally used to store user's real name and other information.
+.TP
+\fB\-d\fR, \fB\-\-directory\fR=\fIdirectory\fR
+Set user's home directory to \fIdirectory\fR.
+If this option is not present, a default specified by the
+.B freeIPA configuration is used.
+.TP
+\fB\-f\fR, \fB\-\-firstname\fR=\fIgivenName\fR
+Set user's first name to \fIgivenName\fR.
+.TP
+\fB\-l\fR, \fB\-\-lastname\fR=\fIfamilyName\fR
+Set user's last name to \fIfamilyName\fR.
+.TP
+\fB\-p\fR, \fB\-\-password\fR=\fIpassword\fR
+Set user's password to \fIpassword\fR.
+.TP
+\fB\-P\fR
+Prompt for the user's password.
+.TP
+\fB\-s\fR, \fB\-\-shell\fR=\fIshell\fR
+Set the user's login shell to \fIshell\fR.
+If this option is not present, a default specified by the
+.B freeIPA configuration is used.
+.TP
+\fB\-G\fR, \fB\-\-groups\fR=\fIgroups\fR
+Add this user to one or more groups in \fIgroups\fR.
+\fIgroups\fR is a comma\-separated list of groups.
+Each \fIgroup\fR must already exist.
+.TP
+\fB\-k\fR, \fB\-\-krb\-principal\fR=\fIprincipal\fR
+Set this user's principal to \fIprincipal\fR.
+By default the principal is set to \fBuser\fR.
+.TP
+\fB\-M\fR, \fB\-\-mailAddress\fR=\fImail\fR
+Set this user's e\-mail address to \fImail\fR.
+.TP
+\fB\-\-addattr\fR \fIattr=value\fR
+Adds \fIvalue\fR to attribute \fIattr\fR. Attributes set this way are done after other options. If an attribute is listed more than once or already exists in the entry, it is considered a multi\-valued attribute and a list of the values is created.
+.TP
+\fB\-\-setattr\fR \fIattr=value\fR
+Set attribute \fIattr\fR to \fIvalue\fR. Any existing value will be replaced with \fIvalue\fR.
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+Display the XML\-RPC request and response for more verbose debugging output.
+.PP
+The mandatory fields are: user, first name and last name. If any of these are not included on the command line you will be asked interactively.
+
+The password is asked interactively if not passed on the command\-line but it isn't mandatory. Leaving both values blank will leave the password unset on the account.
+
+If no options are passed then all questions are asked.
+.SH "EXIT STATUS"
+The exit status is 0 on success, nonzero on error.
diff --git a/ipa-admintools/man/ipa-defaultoptions.1 b/ipa-admintools/man/ipa-defaultoptions.1
new file mode 100644
index 00000000..f58588fe
--- /dev/null
+++ b/ipa-admintools/man/ipa-defaultoptions.1
@@ -0,0 +1,65 @@
+.\" A man page for ipa-defaultoptions
+.\" Copyright (C) 2008 Red Hat, Inc.
+.\"
+.\" This is free software; you can redistribute it and/or modify it under
+.\" the terms of the GNU Library General Public License as published by
+.\" the Free Software Foundation; version 2 only
+.\"
+.\" This program is distributed in the hope that it will be useful, but
+.\" WITHOUT ANY WARRANTY; without even the implied warranty of
+.\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+.\" General Public License for more details.
+.\"
+.\" You should have received a copy of the GNU Library General Public
+.\" License along with this program; if not, write to the Free Software
+.\" Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+.\"
+.\" Author: Rob Crittenden <rcritten@redhat.com>
+.\"
+.TH "ipa-defaultoptions" "1" "Jul 14 2008" "freeipa" ""
+.SH "NAME"
+ipa\-defaultoptions \- Display or modify the IPA Search and User Policies
+.SH "SYNOPSIS"
+ipa\-defaultoptions [\-\-maxusername number] [\-\-homedir directory] [\-\-defaultshell shell] [\-\-defaultgroup group] [\-\-emaildomain domain] [\-\-searchtimelimit number] [\-\-searchrecordslimit number] [\-\-usersearch fields] [\-\-groupsearch fields] [\-v|\-\-verbose]
+ipa\-defaultoptions \-\-show
+.SH "DESCRIPTION"
+Displays or updates the IPA Search and User Policy.
+
+.SH "OPTIONS"
+.TP
+\fB\-\-maxusername\fR=\fIMAXUSERNAME\fR
+Max. Length of a username
+.TP
+\fB\-\-homedir\fR=\fIHOMEDIR\fR
+Default location of home directories
+.TP
+\fB\-\-defaultshell\fR=\fIDEFAULTSHELL\fR
+Default shell for new users
+.TP
+\fB\-\-defaultgroup\fR=\fIDEFAULTGROUP\fR
+Default group for new users
+.TP
+\fB\-\-emaildomain\fR=\fIEMAILDOMAIN\fR
+Default e\-mail domain
+.TP
+\fB\-\-searchtimelimit\fR=\fISEARCHTIMELIMIT\fR
+Max. amount of time (sec.) for a search (-1 for unlimited)
+.TP
+\fB\-\-searchrecordslimit\fR=\fISEARCHRECORDSLIMIT\fR
+Max. number of records to search (-1 for unlimited)
+.TP
+\fB\-\-usersearch\fR=\fIUSERSEARCH\fR
+A comma\-separated list of fields to search when
+searching for users
+.TP
+\fB\-\-groupsearch\fR=\fIGROUPSEARCH\fR
+A comma\-separated list of fields to search when
+searching for groups
+.TP
+\fB\-\-show\fR
+Display the current password policy.
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+Display the XML\-RPC request and response for more verbose debugging output
+.SH "EXIT STATUS"
+The exit status is 0 on success, nonzero on error.
diff --git a/ipa-admintools/man/ipa-deldelegation.1 b/ipa-admintools/man/ipa-deldelegation.1
new file mode 100644
index 00000000..f3d4a904
--- /dev/null
+++ b/ipa-admintools/man/ipa-deldelegation.1
@@ -0,0 +1,39 @@
+.\" A man page for ipa-deldelegation
+.\" Copyright (C) 2007 Red Hat, Inc.
+.\"
+.\" This is free software; you can redistribute it and/or modify it under
+.\" the terms of the GNU Library General Public License as published by
+.\" the Free Software Foundation; version 2 only
+.\"
+.\" This program is distributed in the hope that it will be useful, but
+.\" WITHOUT ANY WARRANTY; without even the implied warranty of
+.\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+.\" General Public License for more details.
+.\"
+.\" You should have received a copy of the GNU Library General Public
+.\" License along with this program; if not, write to the Free Software
+.\" Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+.\"
+.\" Author: Rob Crittenden <rcritten@redhat.com>
+.\"
+.TH "ipa-deldelegation" "1" "Oct 24 2007" "freeipa" ""
+.SH "NAME"
+ipa\-deldelegation \- Remove a delegation
+
+.SH "SYNOPSIS"
+ipa\-deldelegation \fIname\fR
+
+.SH "OPTIONS"
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+Display the XML\-RPC request and response for more verbose debugging output.
+.SH "DESCRIPTION"
+Removes an existing delegation named \fIname\fR.
+
+A delegation is used to grant write access to certain attributes from one group to another. ipa\-deldelegation removes this access.
+.SH "EXIT STATUS"
+0 if the delegation was removed successfully
+
+1 if an error occurred
+
+2 if no matching delegation was found
diff --git a/ipa-admintools/man/ipa-delgroup.1 b/ipa-admintools/man/ipa-delgroup.1
new file mode 100644
index 00000000..4e87bcf4
--- /dev/null
+++ b/ipa-admintools/man/ipa-delgroup.1
@@ -0,0 +1,37 @@
+.\" A man page for ipa-delgroup
+.\" Copyright (C) 2007 Red Hat, Inc.
+.\"
+.\" This is free software; you can redistribute it and/or modify it under
+.\" the terms of the GNU Library General Public License as published by
+.\" the Free Software Foundation; version 2 only
+.\"
+.\" This program is distributed in the hope that it will be useful, but
+.\" WITHOUT ANY WARRANTY; without even the implied warranty of
+.\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+.\" General Public License for more details.
+.\"
+.\" You should have received a copy of the GNU Library General Public
+.\" License along with this program; if not, write to the Free Software
+.\" Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+.\"
+.\" Author: Rob Crittenden <rcritten@redhat.com>
+.\"
+.TH "ipa-delgroup" "1" "Oct 10 2007" "freeipa" ""
+.SH "NAME"
+ipa\-delgroup \- Delete a group
+
+.SH "SYNOPSIS"
+ipa\-delgroup \fIgroup\fR
+
+.SH "OPTIONS"
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+Display the XML\-RPC request and response for more verbose debugging output.
+.SH "DESCRIPTION"
+Deletes a group with group name \fIgroup\fR.
+
+Members of the group are not affected.
+
+The groups \fIadmins\fR and \fIeditors\fR are required by IPA and may not be removed.
+.SH "EXIT STATUS"
+The exit status is 0 on success, nonzero on error.
diff --git a/ipa-admintools/man/ipa-delservice.1 b/ipa-admintools/man/ipa-delservice.1
new file mode 100644
index 00000000..779b9eca
--- /dev/null
+++ b/ipa-admintools/man/ipa-delservice.1
@@ -0,0 +1,38 @@
+.\" A man page for ipa-delservice
+.\" Copyright (C) 2007 Red Hat, Inc.
+.\"
+.\" This is free software; you can redistribute it and/or modify it under
+.\" the terms of the GNU Library General Public License as published by
+.\" the Free Software Foundation; version 2 only
+.\"
+.\" This program is distributed in the hope that it will be useful, but
+.\" WITHOUT ANY WARRANTY; without even the implied warranty of
+.\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+.\" General Public License for more details.
+.\"
+.\" You should have received a copy of the GNU Library General Public
+.\" License along with this program; if not, write to the Free Software
+.\" Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+.\"
+.\" Author: Rob Crittenden <rcritten@redhat.com>
+.\"
+.TH "ipa-delservice" "1" "Jan 11 2008" "freeipa" ""
+.SH "NAME"
+ipa\-delservice \- Delete a service principal
+
+.SH "SYNOPSIS"
+ipa\-delservice \fIprincipal\fR
+
+.SH "OPTIONS"
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+Display the XML\-RPC request and response for more verbose debugging output.
+.SH "DESCRIPTION"
+Deletes a service principal with name \fIprincipal\fR.
+
+.SH "EXIT STATUS"
+0 on success
+
+1 if an error occurred
+
+2 if not exactly one matching entries was found
diff --git a/ipa-admintools/man/ipa-deluser.1 b/ipa-admintools/man/ipa-deluser.1
new file mode 100644
index 00000000..2679302e
--- /dev/null
+++ b/ipa-admintools/man/ipa-deluser.1
@@ -0,0 +1,35 @@
+.\" A man page for ipa-deluser
+.\" Copyright (C) 2007 Red Hat, Inc.
+.\"
+.\" This is free software; you can redistribute it and/or modify it under
+.\" the terms of the GNU Library General Public License as published by
+.\" the Free Software Foundation; version 2 only
+.\"
+.\" This program is distributed in the hope that it will be useful, but
+.\" WITHOUT ANY WARRANTY; without even the implied warranty of
+.\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+.\" General Public License for more details.
+.\"
+.\" You should have received a copy of the GNU Library General Public
+.\" License along with this program; if not, write to the Free Software
+.\" Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+.\"
+.\" Author: Rob Crittenden <rcritten@redhat.com>
+.\"
+.TH "ipa-deluser" "1" "Oct 10 2007" "freeipa" ""
+.SH "NAME"
+ipa\-deluser \- Delete a user
+
+.SH "SYNOPSIS"
+ipa\-deluser \fIuser\fR
+
+.SH "OPTIONS"
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+Display the XML\-RPC request and response for more verbose debugging output.
+.SH "DESCRIPTION"
+Deletes a user with user name \fIname\fR.
+
+Users are automatically removed from groups when they are deleted.
+.SH "EXIT STATUS"
+The exit status is 0 on success, nonzero on error.
diff --git a/ipa-admintools/man/ipa-findgroup.1 b/ipa-admintools/man/ipa-findgroup.1
new file mode 100644
index 00000000..266a60cd
--- /dev/null
+++ b/ipa-admintools/man/ipa-findgroup.1
@@ -0,0 +1,41 @@
+.\" A man page for ipa-findgroup
+.\" Copyright (C) 2007 Red Hat, Inc.
+.\"
+.\" This is free software; you can redistribute it and/or modify it under
+.\" the terms of the GNU Library General Public License as published by
+.\" the Free Software Foundation; version 2 only
+.\"
+.\" This program is distributed in the hope that it will be useful, but
+.\" WITHOUT ANY WARRANTY; without even the implied warranty of
+.\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+.\" General Public License for more details.
+.\"
+.\" You should have received a copy of the GNU Library General Public
+.\" License along with this program; if not, write to the Free Software
+.\" Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+.\"
+.\" Author: Rob Crittenden <rcritten@redhat.com>
+.\"
+.TH "ipa-findgroup" "1" "Oct 10 2007" "freeipa" ""
+.SH "NAME"
+ipa\-findgroup \- Find a group
+
+.SH "SYNOPSIS"
+ipa\-findgroup \fIstring\fR
+
+.SH "OPTIONS"
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+Display the XML\-RPC request and response for more verbose debugging output.
+.SH "DESCRIPTION"
+Searches for a group that contains \fIstring\fR.
+
+The search is a substring search in the name and description attributes.
+
+All entries that match are displayed.
+.SH "EXIT STATUS"
+0 if one or more entries were found
+
+1 if an error occurred
+
+2 if no matching entries were found
diff --git a/ipa-admintools/man/ipa-findservice.1 b/ipa-admintools/man/ipa-findservice.1
new file mode 100644
index 00000000..3942391c
--- /dev/null
+++ b/ipa-admintools/man/ipa-findservice.1
@@ -0,0 +1,48 @@
+.\" A man page for ipa-findservice
+.\" Copyright (C) 2007 Red Hat, Inc.
+.\"
+.\" This is free software; you can redistribute it and/or modify it under
+.\" the terms of the GNU Library General Public License as published by
+.\" the Free Software Foundation; version 2 only
+.\"
+.\" This program is distributed in the hope that it will be useful, but
+.\" WITHOUT ANY WARRANTY; without even the implied warranty of
+.\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+.\" General Public License for more details.
+.\"
+.\" You should have received a copy of the GNU Library General Public
+.\" License along with this program; if not, write to the Free Software
+.\" Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+.\"
+.\" Author: Rob Crittenden <rcritten@redhat.com>
+.\"
+.TH "ipa-findservice" "1" "Jan 11 2008" "freeipa" ""
+.SH "NAME"
+ipa\-findservice \- Find a service principal
+.SH "SYNOPSIS"
+ipa\-findservice \fIstring\fR
+
+.SH "DESCRIPTION"
+Searches for a service principal that contains \fIstring\fR.
+
+The search is a substring search in the service principal. You can search for all principals for a given host, by service or a substring.
+
+All entries that match are displayed.
+.SH "OPTIONS"
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+Display the XML\-RPC request and response for more verbose debugging output.
+.SH "EXAMPLES"
+Find all ldap service principals issued by the IPA KDC:
+
+ # ipa\-findservice ldap
+
+Find all service principals for ipa.example.com:
+
+ # ipa\-findservice ipa.example.com
+.SH "EXIT STATUS"
+0 if one or more entries were found
+
+1 if an error occurred
+
+2 if no matching entries were found
diff --git a/ipa-admintools/man/ipa-finduser.1 b/ipa-admintools/man/ipa-finduser.1
new file mode 100644
index 00000000..1730daba
--- /dev/null
+++ b/ipa-admintools/man/ipa-finduser.1
@@ -0,0 +1,48 @@
+.\" A man page for ipa-finduser
+.\" Copyright (C) 2007 Red Hat, Inc.
+.\"
+.\" This is free software; you can redistribute it and/or modify it under
+.\" the terms of the GNU Library General Public License as published by
+.\" the Free Software Foundation; version 2 only
+.\"
+.\" This program is distributed in the hope that it will be useful, but
+.\" WITHOUT ANY WARRANTY; without even the implied warranty of
+.\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+.\" General Public License for more details.
+.\"
+.\" You should have received a copy of the GNU Library General Public
+.\" License along with this program; if not, write to the Free Software
+.\" Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+.\"
+.\" Author: Rob Crittenden <rcritten@redhat.com>
+.\"
+.TH "ipa-finduser" "1" "Oct 10 2007" "freeipa" ""
+.SH "NAME"
+ipa\-finduser \- Find a user
+.SH "SYNOPSIS"
+ipa\-finduser [\fIOPTION\fR]... \fIstring\fR
+
+.SH "DESCRIPTION"
+Searches for a user that contains \fIstring\fR.
+
+The search is a substring search in the username, given name, family name, telephone number, organization and title attributes.
+
+All entries that match are displayed.
+.SH "OPTIONS"
+.TP
+\fB\-a\fR, \fB\-\-all
+Display all attributes
+.TP
+\fB\-n\fR, \fB\-\-notranslate\fR
+Don't translate the LDAP attribute names to labels that match the UI.
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+Display the XML\-RPC request and response for more verbose debugging output.
+.PP
+By default, the full name, home directory, login shell, and username fields are displayed.
+.SH "EXIT STATUS"
+0 if one or more entries were found
+
+1 if an error occurred
+
+2 if no matching entries were found
diff --git a/ipa-admintools/man/ipa-listdelegation.1 b/ipa-admintools/man/ipa-listdelegation.1
new file mode 100644
index 00000000..9118bc9a
--- /dev/null
+++ b/ipa-admintools/man/ipa-listdelegation.1
@@ -0,0 +1,37 @@
+.\" A man page for ipa-listdelegation
+.\" Copyright (C) 2007 Red Hat, Inc.
+.\"
+.\" This is free software; you can redistribute it and/or modify it under
+.\" the terms of the GNU Library General Public License as published by
+.\" the Free Software Foundation; version 2 only
+.\"
+.\" This program is distributed in the hope that it will be useful, but
+.\" WITHOUT ANY WARRANTY; without even the implied warranty of
+.\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+.\" General Public License for more details.
+.\"
+.\" You should have received a copy of the GNU Library General Public
+.\" License along with this program; if not, write to the Free Software
+.\" Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+.\"
+.\" Author: Rob Crittenden <rcritten@redhat.com>
+.\"
+.TH "ipa-listdelegation" "1" "Oct 24 2007" "freeipa" ""
+.SH "NAME"
+ipa\-listdelegation \- Lists all current delegations
+
+.SH "SYNOPSIS"
+ipa\-listdelegation
+
+.SH "DESCRIPTION"
+Lists all current delegations.
+
+No sorting is done.
+.SH "OPTIONS"
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+Display the XML\-RPC request and response for more verbose debugging output.
+.SH "EXIT STATUS"
+0 if the delegations are listed successfully
+
+1 if an error occurred
diff --git a/ipa-admintools/man/ipa-lockuser.1 b/ipa-admintools/man/ipa-lockuser.1
new file mode 100644
index 00000000..79df5c0e
--- /dev/null
+++ b/ipa-admintools/man/ipa-lockuser.1
@@ -0,0 +1,38 @@
+.\" A man page for ipa-lockuser
+.\" Copyright (C) 2007 Red Hat, Inc.
+.\"
+.\" This is free software; you can redistribute it and/or modify it under
+.\" the terms of the GNU Library General Public License as published by
+.\" the Free Software Foundation; version 2 only
+.\"
+.\" This program is distributed in the hope that it will be useful, but
+.\" WITHOUT ANY WARRANTY; without even the implied warranty of
+.\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+.\" General Public License for more details.
+.\"
+.\" You should have received a copy of the GNU Library General Public
+.\" License along with this program; if not, write to the Free Software
+.\" Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+.\"
+.\" Author: Rob Crittenden <rcritten@redhat.com>
+.\"
+.TH "ipa-lockuser" "1" "Oct 10 2007" "freeipa" ""
+.SH "NAME"
+ipa\-lockuser \- Lock or unlock a user account
+
+.SH "SYNOPSIS"
+ipa\-lockuser [\fIOPTION\fR]... \fIuser\fR
+
+.SH "DESCRIPTION"
+Locks a user account with login name \fIname\fR.
+
+Users are not removed from groups when their account is locked.
+.SH "OPTIONS"
+.TP
+\fB\-u\fR, \fB\-\-unlock
+Unlock a user's account
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+Display the XML\-RPC request and response for more verbose debugging output.
+.SH "EXIT STATUS"
+The exit status is 0 on success, nonzero on error.
diff --git a/ipa-admintools/man/ipa-moddelegation.1 b/ipa-admintools/man/ipa-moddelegation.1
new file mode 100644
index 00000000..28ab2aa9
--- /dev/null
+++ b/ipa-admintools/man/ipa-moddelegation.1
@@ -0,0 +1,50 @@
+.\" A man page for ipa-moddelegation
+.\" Copyright (C) 2007 Red Hat, Inc.
+.\"
+.\" This is free software; you can redistribute it and/or modify it under
+.\" the terms of the GNU Library General Public License as published by
+.\" the Free Software Foundation; version 2 only
+.\"
+.\" This program is distributed in the hope that it will be useful, but
+.\" WITHOUT ANY WARRANTY; without even the implied warranty of
+.\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+.\" General Public License for more details.
+.\"
+.\" You should have received a copy of the GNU Library General Public
+.\" License along with this program; if not, write to the Free Software
+.\" Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+.\"
+.\" Author: Rob Crittenden <rcritten@redhat.com>
+.\"
+.TH "ipa-moddelegation" "1" "Oct 24 2007" "freeipa" ""
+.SH "NAME"
+ipa\-moddelegation \- Modify an existing delegation
+
+.SH "SYNOPSIS"
+ipa\-moddelegation [\fIOPTION\fR]... \fIname\fR
+
+.SH "DESCRIPTION"
+Modifies an existing delegation named \fIname\fR.
+
+A delegation is used to grant access to certain attributes from one group to another.
+.SH "OPTIONS"
+.TP
+\fB\-a\fR, \fB\-\-attributes\fR=\fIATTRIBUTES\fR
+A comma\-separated list of the the \f[SM]attributes\fR that may be written by the source group. This list of attributes replaces the list in the existing delegation.
+.TP
+\fB\-s\fR, \fB\-\-source\fR=\fISOURCE\fR
+The name of the group that is being granted write permission.
+.TP
+\fB\-t\fR, \fB\-\-target\fR=\fITARGET\fR
+The name of the group that will be written to.
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+Display the XML\-RPC request and response for more verbose debugging output.
+.PP
+At least one of \-a, \-s or \-t is required.
+.SH "EXIT STATUS"
+0 if the delegation was updated successfully
+
+1 if an error occurred
+
+2 if no matching delegation was found
diff --git a/ipa-admintools/man/ipa-modgroup.1 b/ipa-admintools/man/ipa-modgroup.1
new file mode 100644
index 00000000..e52d9636
--- /dev/null
+++ b/ipa-admintools/man/ipa-modgroup.1
@@ -0,0 +1,56 @@
+.\" A man page for ipa-modgroup
+.\" Copyright (C) 2007 Red Hat, Inc.
+.\"
+.\" This is free software; you can redistribute it and/or modify it under
+.\" the terms of the GNU Library General Public License as published by
+.\" the Free Software Foundation; version 2 only
+.\"
+.\" This program is distributed in the hope that it will be useful, but
+.\" WITHOUT ANY WARRANTY; without even the implied warranty of
+.\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+.\" General Public License for more details.
+.\"
+.\" You should have received a copy of the GNU Library General Public
+.\" License along with this program; if not, write to the Free Software
+.\" Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+.\"
+.\" Author: Rob Crittenden <rcritten@redhat.com>
+.\"
+.TH "ipa-modgroup" "1" "Oct 10 2007" "freeipa" ""
+.SH "NAME"
+ipa\-modgroup \- Modify a group
+.SH "SYNOPSIS"
+ipa\-modgroup [\fIOPTION\fR]... \fIgroup\fR
+
+.SH "DESCRIPTION"
+Updates the members or description of \fIgroup\fR.
+.SH "OPTIONS"
+.TP
+\fB\-a\fR, \fB\-\-add\fR=\fIuser1,user2,...usern\fR
+Add one or more users to the group
+.TP
+\fB\-d\fR, \fB\-\-description\fR=\fIdescription\fR
+Modify the description of the group
+.TP
+\fB\-e\fR, \fB\-\-groupdel\fR=\fIgroup1,group2,...groupn\fR
+Remove one or more groups from the group
+.TP
+\fB\-g\fR, \fB\-\-groupadd\fR=\fIgroup1,group2,...groupn\fR
+Add one or more groups to the group
+.TP
+\fB\-r\fR, \fB\-\-remove\fR=\fIuser1,user2,...usern\fR
+Remove one or more users from the group
+.TP
+\fB\-\-addattr\fR \fIattr=value\fR
+Add a new attribute, or value to an existing attribute
+.TP
+\fB\-\-delattr\fR \fIattr=value\fR
+Remove an attribute and all values
+.TP
+\fB\-\-setattr\fR \fIattr=value\fR
+Set an attribute to a new value, removing all old ones
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+Display the XML\-RPC request and response for more verbose debugging output.
+.SH "EXIT STATUS"
+The exit status is 0 on success, nonzero on error.
diff --git a/ipa-admintools/man/ipa-moduser.1 b/ipa-admintools/man/ipa-moduser.1
new file mode 100644
index 00000000..fff038b7
--- /dev/null
+++ b/ipa-admintools/man/ipa-moduser.1
@@ -0,0 +1,56 @@
+.\" A man page for ipa-moduser
+.\" Copyright (C) 2007 Red Hat, Inc.
+.\"
+.\" This is free software; you can redistribute it and/or modify it under
+.\" the terms of the GNU Library General Public License as published by
+.\" the Free Software Foundation; version 2 only
+.\"
+.\" This program is distributed in the hope that it will be useful, but
+.\" WITHOUT ANY WARRANTY; without even the implied warranty of
+.\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+.\" General Public License for more details.
+.\"
+.\" You should have received a copy of the GNU Library General Public
+.\" License along with this program; if not, write to the Free Software
+.\" Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+.\"
+.\" Author: Rob Crittenden <rcritten@redhat.com>
+.\"
+.TH "ipa-moduser" "1" "Oct 10 2007" "freeipa" ""
+.SH "NAME"
+ipa\-moduser \- Modify a user
+.SH "SYNOPSIS"
+ipa\-moduser [\fIOPTION\fR]... \fIname\fR
+
+.SH "DESCRIPTION"
+Updates the user \fIname\fR.
+.SH "OPTIONS"
+.TP
+\fB\-a\fR, \fB\-\-activate\fR
+Activate a user that was previously inactivated
+.TP
+\fB\-c\fR, \fB\-\-gecos\fR=\fIGECOS\fR
+Set the gecos field of the user. This is traditionally the user's full name.
+.TP
+\fB\-d\fR, \fB\-\-directory\fR=\fIdirectory\fR
+Set user's home directory
+.TP
+\fB\-f\fR, \fB\-\-firstname\fR=\fINAME\fR
+Set user's first name
+.TP
+\fB\-l\fR, \fB\-\-lastname\fR=\fINAME\fR
+Set user's last name
+.TP
+\fB\-s\fR, \fB\-\-shell\fR=\fIshell\fR
+Set user's login shell
+.TP
+\fB\-\-addattr\fR \fIattr=value\fR
+Add a new attribute, or value to an existing attribute
+.TP
+\fB\-\-delattr\fR \fIattr=value\fR
+Remove an attribute and all values
+.TP
+\fB\-\-setattr\fR \fIattr=value\fR
+Set an attribute to a new value, removing all old ones
+.SH "EXIT STATUS"
+The exit status is 0 on success, nonzero on error.
diff --git a/ipa-admintools/man/ipa-passwd.1 b/ipa-admintools/man/ipa-passwd.1
new file mode 100644
index 00000000..4554d5d2
--- /dev/null
+++ b/ipa-admintools/man/ipa-passwd.1
@@ -0,0 +1,34 @@
+.\" A man page for ipa-passwd
+.\" Copyright (C) 2007 Red Hat, Inc.
+.\"
+.\" This is free software; you can redistribute it and/or modify it under
+.\" the terms of the GNU Library General Public License as published by
+.\" the Free Software Foundation; version 2 only
+.\"
+.\" This program is distributed in the hope that it will be useful, but
+.\" WITHOUT ANY WARRANTY; without even the implied warranty of
+.\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+.\" General Public License for more details.
+.\"
+.\" You should have received a copy of the GNU Library General Public
+.\" License along with this program; if not, write to the Free Software
+.\" Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+.\"
+.\" Author: Rob Crittenden <rcritten@redhat.com>
+.\"
+.TH "ipa-passwd" "1" "Oct 10 2007" "freeipa" ""
+.SH "NAME"
+ipa\-passwd \- Change a user's password
+.SH "SYNOPSIS"
+ipa\-password [\fIuser\fR]
+
+.SH "DESCRIPTION"
+Changes the current user's password. If a \fIuser\fR is included on the command\-line then that user's password is modified.
+
+The user is the name in the kerberos principal, so it may not match username.
+.SH "OPTIONS"
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+Display the XML\-RPC request and response for more verbose debugging output.
+.SH "EXIT STATUS"
+The exit status is 0 on success, nonzero on error.
diff --git a/ipa-admintools/man/ipa-pwpolicy.1 b/ipa-admintools/man/ipa-pwpolicy.1
new file mode 100644
index 00000000..4c7de4c3
--- /dev/null
+++ b/ipa-admintools/man/ipa-pwpolicy.1
@@ -0,0 +1,54 @@
+.\" A man page for ipa-pwpolicy
+.\" Copyright (C) 2007 Red Hat, Inc.
+.\"
+.\" This is free software; you can redistribute it and/or modify it under
+.\" the terms of the GNU Library General Public License as published by
+.\" the Free Software Foundation; version 2 only
+.\"
+.\" This program is distributed in the hope that it will be useful, but
+.\" WITHOUT ANY WARRANTY; without even the implied warranty of
+.\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+.\" General Public License for more details.
+.\"
+.\" You should have received a copy of the GNU Library General Public
+.\" License along with this program; if not, write to the Free Software
+.\" Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+.\"
+.\" Author: Rob Crittenden <rcritten@redhat.com>
+.\"
+.TH "ipa-pwpolicy" "1" "Feb 25 2008" "freeipa" ""
+.SH "NAME"
+ipa\-pwpolicy \- Display or modify the IPA password policy
+
+.SH "SYNOPSIS"
+ipa\-pwpolicy
+[\-\-maxlife days] [\-\-minlife hours] [\-\-history number] [\-\-minclasses number] [\-\-minlength number]
+.TP
+ipa\-pwpolicy \-\-show
+.SH "DESCRIPTION"
+Displays or updates the IPA password policy.
+
+.SH "OPTIONS"
+.TP
+\fB\-\-maxlife\fR=\fIdays\fR
+Set the maximum Password Lifetime in days
+.TP
+\fB\-\-minlife\fR=\fIhours\fR
+Set the minimum Password Lifetime in hours
+.TP
+\fB\-\-history\fR=\fIinteger\fR
+The number of passwords stored in the password history. A value of 0 means do not store a password history.
+.TP
+\fB\-\-minclasses\fR=\fIinteger\fR
+Set the minimum number of character classes required in a password. The classes are alpha, numeric, mixed\-case and special characters.
+.TP
+\fB\-\-minlength\fR=\fIinteger\fR
+Set the minimum password length.
+.TP
+\fB\-\-show\fR
+Display the current password policy.
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+Display the XML\-RPC request and response for more verbose debugging output
+.SH "EXIT STATUS"
+The exit status is 0 on success, nonzero on error.
diff --git a/ipa-client/AUTHORS b/ipa-client/AUTHORS
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/ipa-client/AUTHORS
diff --git a/ipa-client/Makefile.am b/ipa-client/Makefile.am
new file mode 100644
index 00000000..796a923f
--- /dev/null
+++ b/ipa-client/Makefile.am
@@ -0,0 +1,100 @@
+# This file will be processed with automake-1.7 to create Makefile.in
+
+AUTOMAKE_OPTIONS = 1.7
+
+NULL =
+
+INCLUDES = \
+ -I. \
+ -I$(srcdir) \
+ -DPREFIX=\""$(prefix)"\" \
+ -DBINDIR=\""$(bindir)"\" \
+ -DLIBDIR=\""$(libdir)"\" \
+ -DLIBEXECDIR=\""$(libexecdir)"\" \
+ -DDATADIR=\""$(datadir)"\" \
+ $(KRB5_CFLAGS) \
+ $(OPENLDAP_CFLAGS) \
+ $(MOZLDAP_CFLAGS) \
+ $(SASL_CFLAGS) \
+ $(POPT_CFLAGS) \
+ $(WARN_CFLAGS) \
+ $(NULL)
+
+sbin_PROGRAMS = \
+ ipa-getkeytab \
+ $(NULL)
+
+ipa_getkeytab_SOURCES = \
+ ipa-getkeytab.c \
+ $(NULL)
+
+ipa_getkeytab_LDADD = \
+ $(KRB5_LIBS) \
+ $(OPENLDAP_LIBS) \
+ $(MOZLDAP_LIBS) \
+ $(SASL_LIBS) \
+ $(POPT_LIBS) \
+ $(NULL)
+
+SUBDIRS = \
+ firefox \
+ ipaclient \
+ ipa-install \
+ man \
+ $(NULL)
+
+EXTRA_DIST = \
+ ipa-client.spec \
+ COPYING \
+ AUTHORS \
+ INSTALL \
+ README \
+ HACKING \
+ NEWS \
+ ChangeLog \
+ $(NULL)
+
+DISTCLEANFILES = \
+ $(NULL)
+
+MAINTAINERCLEANFILES = \
+ *~ \
+ intltool-*.in \
+ compile \
+ configure \
+ COPYING \
+ INSTALL \
+ install-sh \
+ missing \
+ mkinstalldirs \
+ config.guess \
+ ltmain.sh \
+ config.sub \
+ depcomp \
+ Makefile.in \
+ config.h.* \
+ aclocal.m4 \
+ version.m4 \
+ ipa-client.spec \
+ py-compile \
+ $(NULL)
+
+# Creating ChangeLog from hg log (taken from cairo/Makefile.am):
+
+ChangeLog: $(srcdir)/ChangeLog
+
+$(srcdir)/ChangeLog:
+ @if test -d "$(srcdir)/../.hg"; then \
+ (cd "$(srcdir)" && \
+ ./missing --run hg log --verbose) | fmt --split-only > $@.tmp \
+ && mv -f $@.tmp $@ \
+ || ($(RM) $@.tmp; \
+ echo Failed to generate ChangeLog, your ChangeLog may be outdated >&2; \
+ (test -f $@ || echo hg log is required to generate this file >> $@)); \
+ else \
+ test -f $@ || \
+ (echo A hg checkout and hg -log is required to generate ChangeLog >&2 && \
+ echo A hg checkout and hg log is required to generate this file >> $@); \
+ fi
+
+.PHONY: ChangeLog $(srcdir)/ChangeLog
diff --git a/ipa-client/NEWS b/ipa-client/NEWS
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/ipa-client/NEWS
diff --git a/ipa-client/README b/ipa-client/README
new file mode 100644
index 00000000..d7c30f0c
--- /dev/null
+++ b/ipa-client/README
@@ -0,0 +1,24 @@
+Code to be installed on any client that wants to be in an IPA domain.
+
+Mostly consists of a tool for Linux systems that will help configure the
+client so it will work properly in a kerberized environment.
+
+It also includes several ways to configure Firefox to do single sign-on.
+
+The two methods on the client side are:
+
+1. globalsetup.sh. This modifies the global Firefox installation so that
+ any profiles created will be pre-configured.
+
+2. usersetup.sh. This will update a user's existing profile.
+
+The downside of #1 is that an rpm -V will return a failure. It will also
+need to be run with every update of Firefox.
+
+One a profile contains the proper preferences it will be unaffected by
+upgrades to Firefox.
+
+The downside of #2 is that every user would need to run this each time they
+create a new profile.
+
+There is a third, server-side method. See ipa-server/README for details.
diff --git a/ipa-client/autogen.sh b/ipa-client/autogen.sh
new file mode 100755
index 00000000..014487e4
--- /dev/null
+++ b/ipa-client/autogen.sh
@@ -0,0 +1,196 @@
+#!/bin/sh
+# Run this to generate all the initial makefiles, etc.
+set -e
+
+PACKAGE=freeipa-client
+
+LIBTOOLIZE=${LIBTOOLIZE-libtoolize}
+LIBTOOLIZE_FLAGS="--copy --force"
+AUTOHEADER=${AUTOHEADER-autoheader}
+AUTOMAKE_FLAGS="--add-missing --gnu"
+AUTOCONF=${AUTOCONF-autoconf}
+
+# automake 1.8 requires autoconf 2.58
+# automake 1.7 requires autoconf 2.54
+automake_min_vers=1.7
+aclocal_min_vers=$automake_min_vers
+autoconf_min_vers=2.54
+libtoolize_min_vers=1.4
+
+# The awk-based string->number conversion we use needs a C locale to work
+# as expected. Setting LC_ALL overrides whether the user set LC_ALL,
+# LC_NUMERIC, or LANG.
+LC_ALL=C
+
+ARGV0=$0
+
+# Allow invocation from a separate build directory; in that case, we change
+# to the source directory to run the auto*, then change back before running configure
+srcdir=`dirname $ARGV0`
+test -z "$srcdir" && srcdir=.
+
+ORIGDIR=`pwd`
+
+cd $srcdir
+
+# Usage:
+# compare_versions MIN_VERSION ACTUAL_VERSION
+# returns true if ACTUAL_VERSION >= MIN_VERSION
+compare_versions() {
+ ch_min_version=$1
+ ch_actual_version=$2
+ ch_status=0
+ IFS="${IFS= }"; ch_save_IFS="$IFS"; IFS="."
+ set $ch_actual_version
+ for ch_min in $ch_min_version; do
+ ch_cur=`echo $1 | sed 's/[^0-9].*$//'`; shift # remove letter suffixes
+ if [ -z "$ch_min" ]; then break; fi
+ if [ -z "$ch_cur" ]; then ch_status=1; break; fi
+ if [ $ch_cur -gt $ch_min ]; then break; fi
+ if [ $ch_cur -lt $ch_min ]; then ch_status=1; break; fi
+ done
+ IFS="$ch_save_IFS"
+ return $ch_status
+}
+
+if ($AUTOCONF --version) < /dev/null > /dev/null 2>&1 ; then
+ if ($AUTOCONF --version | head -n 1 | awk 'NR==1 { if( $(NF) >= '$autoconf_min_vers') \
+ exit 1; exit 0; }');
+ then
+ echo "$ARGV0: ERROR: \`$AUTOCONF' is too old."
+ $AUTOCONF --version
+ echo " (version $autoconf_min_vers or newer is required)"
+ DIE="yes"
+ fi
+else
+ echo $AUTOCONF: command not found
+ echo
+ echo "$ARGV0: ERROR: You must have \`autoconf' installed to compile $PACKAGE."
+ echo " (version $autoconf_min_vers or newer is required)"
+ DIE="yes"
+fi
+
+#
+# Hunt for an appropriate version of automake and aclocal; we can't
+# assume that 'automake' is necessarily the most recent installed version
+#
+# We check automake first to allow it to be a newer version than we know about.
+#
+if test x"$AUTOMAKE" = x || test x"$ACLOCAL" = x ; then
+ am_ver=""
+ for ver in "" "-1.9" "-1.8" "-1.7" ; do
+ am="automake$ver"
+ if ($am --version) < /dev/null > /dev/null 2>&1 ; then
+ if ($am --version | head -n 1 | awk 'NR==1 { if( $(NF) >= '$automake_min_vers') \
+ exit 1; exit 0; }'); then : ; else
+ am_ver=$ver
+ break;
+ fi
+ fi
+ done
+
+ AUTOMAKE=${AUTOMAKE-automake$am_ver}
+ ACLOCAL=${ACLOCAL-aclocal$am_ver}
+fi
+
+#
+# Now repeat the tests with the copies we decided upon and error out if they
+# aren't sufficiently new.
+#
+if ($AUTOMAKE --version) < /dev/null > /dev/null 2>&1 ; then
+ automake_actual_version=`$AUTOMAKE --version | head -n 1 | \
+ sed 's/^.*[ ]\([0-9.]*[a-z]*\).*$/\1/'`
+ if ! compare_versions $automake_min_vers $automake_actual_version; then
+ echo "$ARGV0: ERROR: \`$AUTOMAKE' is too old."
+ $AUTOMAKE --version
+ echo " (version $automake_min_vers or newer is required)"
+ DIE="yes"
+ fi
+ if ($ACLOCAL --version) < /dev/null > /dev/null 2>&1; then
+ aclocal_actual_version=`$ACLOCAL --version | head -n 1 | \
+ sed 's/^.*[ ]\([0-9.]*[a-z]*\).*$/\1/'`
+
+ if ! compare_versions $aclocal_min_vers $aclocal_actual_version; then
+ echo "$ARGV0: ERROR: \`$ACLOCAL' is too old."
+ $ACLOCAL --version
+ echo " (version $aclocal_min_vers or newer is required)"
+ DIE="yes"
+ fi
+ else
+ echo $ACLOCAL: command not found
+ echo
+ echo "$ARGV0: ERROR: Missing \`$ACLOCAL'"
+ echo " The version of $AUTOMAKE installed doesn't appear recent enough."
+ DIE="yes"
+ fi
+else
+ echo $AUTOMAKE: command not found
+ echo
+ echo "$ARGV0: ERROR: You must have \`automake' installed to compile $PACKAGE."
+ echo " (version $automake_min_vers or newer is required)"
+ DIE="yes"
+fi
+
+if ($LIBTOOLIZE --version) < /dev/null > /dev/null 2>&1 ; then
+ if ($LIBTOOLIZE --version | awk 'NR==1 { if( $4 >= '$libtoolize_min_vers') \
+ exit 1; exit 0; }');
+ then
+ echo "$ARGV0: ERROR: \`$LIBTOOLIZE' is too old."
+ echo " (version $libtoolize_min_vers or newer is required)"
+ DIE="yes"
+ fi
+else
+ echo $LIBTOOLIZE: command not found
+ echo
+ echo "$ARGV0: ERROR: You must have \`libtoolize' installed to compile $PACKAGE."
+ echo " (version $libtoolize_min_vers or newer is required)"
+ DIE="yes"
+fi
+
+if test -z "$ACLOCAL_FLAGS"; then
+ acdir=`$ACLOCAL --print-ac-dir`
+ if [ ! -f $acdir/pkg.m4 ]; then
+ echo "$ARGV0: Error: Could not find pkg-config macros."
+ echo " (Looked in $acdir/pkg.m4)"
+ echo " If pkg.m4 is available in /another/directory, please set"
+ echo " ACLOCAL_FLAGS=\"-I /another/directory\""
+ echo " Otherwise, please install pkg-config."
+ echo ""
+ echo "pkg-config is available from:"
+ echo "http://www.freedesktop.org/software/pkgconfig/"
+ DIE=yes
+ fi
+fi
+
+if test "X$DIE" != X; then
+ exit 1
+fi
+
+
+if test -z "$*"; then
+ echo "$ARGV0: Note: \`./configure' will be run with no arguments."
+ echo " If you wish to pass any to it, please specify them on the"
+ echo " \`$0' command line."
+ echo
+fi
+
+do_cmd() {
+ echo "$ARGV0: running \`$@'"
+ $@
+}
+
+do_cmd $LIBTOOLIZE $LIBTOOLIZE_FLAGS
+
+do_cmd $ACLOCAL $ACLOCAL_FLAGS
+
+do_cmd $AUTOHEADER
+
+do_cmd $AUTOMAKE $AUTOMAKE_FLAGS
+
+do_cmd $AUTOCONF
+
+cd $ORIGDIR || exit 1
+
+rm -f config.cache
+
+do_cmd $srcdir/configure --cache-file=config.cache --disable-static --enable-maintainer-mode --enable-gtk-doc ${1+"$@"} && echo "Now type \`make' to compile" || exit 1
diff --git a/ipa-client/configure.ac b/ipa-client/configure.ac
new file mode 100644
index 00000000..b639e1d1
--- /dev/null
+++ b/ipa-client/configure.ac
@@ -0,0 +1,195 @@
+AC_PREREQ(2.59)
+m4_include(version.m4)
+AC_INIT([ipa-client],
+ IPA_VERSION,
+ [https://hosted.fedoraproject.org/projects/freeipa/newticket])
+
+AC_CONFIG_SRCDIR([ipaclient/__init__.py])
+AC_CONFIG_HEADERS([config.h])
+
+AM_INIT_AUTOMAKE
+
+AM_MAINTAINER_MODE
+
+AC_PROG_CC
+AC_STDC_HEADERS
+AC_DISABLE_STATIC
+AC_PROG_LIBTOOL
+
+AC_HEADER_STDC
+
+AC_SUBST(VERSION)
+
+dnl ---------------------------------------------------------------------------
+dnl - Check for KRB5
+dnl ---------------------------------------------------------------------------
+
+KRB5_LIBS=
+AC_CHECK_HEADER(krb5.h)
+
+krb5_impl=mit
+
+if test "x$ac_cv_header_krb5_h" = "xyes" ; then
+ dnl lazy check for Heimdal Kerberos
+ AC_CHECK_HEADERS(heim_err.h)
+ if test $ac_cv_header_heim_err_h = yes ; then
+ krb5_impl=heimdal
+ else
+ krb5_impl=mit
+ fi
+
+ if test "x$krb5_impl" = "xmit"; then
+ AC_CHECK_LIB(k5crypto, main,
+ [krb5crypto=k5crypto],
+ [krb5crypto=crypto])
+
+ AC_CHECK_LIB(krb5, main,
+ [have_krb5=yes
+ KRB5_LIBS="-lkrb5 -l$krb5crypto -lcom_err"],
+ [have_krb5=no],
+ [-l$krb5crypto -lcom_err])
+
+ elif test "x$krb5_impl" = "xheimdal"; then
+ AC_CHECK_LIB(des, main,
+ [krb5crypto=des],
+ [krb5crypto=crypto])
+
+ AC_CHECK_LIB(krb5, main,
+ [have_krb5=yes
+ KRB5_LIBS="-lkrb5 -l$krb5crypto -lasn1 -lroken -lcom_err"],
+ [have_krb5=no],
+ [-l$krb5crypto -lasn1 -lroken -lcom_err])
+
+ AC_DEFINE(HAVE_HEIMDAL_KERBEROS, 1,
+ [define if you have HEIMDAL Kerberos])
+
+ else
+ have_krb5=no
+ AC_MSG_WARN([Unrecognized Kerberos5 Implementation])
+ fi
+
+ if test "x$have_krb5" = "xyes" ; then
+ ol_link_krb5=yes
+
+ AC_DEFINE(HAVE_KRB5, 1,
+ [define if you have Kerberos V])
+
+ else
+ AC_MSG_ERROR([Required Kerberos 5 support not available])
+ fi
+
+fi
+
+AC_SUBST(KRB5_LIBS)
+
+dnl ---------------------------------------------------------------------------
+dnl - Check for Mozilla LDAP or OpenLDAP SDK
+dnl ---------------------------------------------------------------------------
+
+AC_ARG_WITH(openldap, [ --with-openldap Use OpenLDAP])
+
+if test x$with_openldap = xyes; then
+ AC_CHECK_LIB(ldap, ldap_search, with_ldap=yes)
+ dnl Check for other libraries we need to link with to get the main routines.
+ test "$with_ldap" != "yes" && { AC_CHECK_LIB(ldap, ldap_open, [with_ldap=yes with_ldap_lber=yes], , -llber) }
+ test "$with_ldap" != "yes" && { AC_CHECK_LIB(ldap, ldap_open, [with_ldap=yes with_ldap_lber=yes with_ldap_krb=yes], , -llber -lkrb) }
+ test "$with_ldap" != "yes" && { AC_CHECK_LIB(ldap, ldap_open, [with_ldap=yes with_ldap_lber=yes with_ldap_krb=yes with_ldap_des=yes], , -llber -lkrb -ldes) }
+ dnl Recently, we need -lber even though the main routines are elsewhere,
+ dnl because otherwise be get link errors w.r.t. ber_pvt_opt_on. So just
+ dnl check for that (it's a variable not a fun but that doesn't seem to
+ dnl matter in these checks) and stick in -lber if so. Can't hurt (even to
+ dnl stick it in always shouldn't hurt, I don't think) ... #### Someone who
+ dnl #### understands LDAP needs to fix this properly.
+ test "$with_ldap_lber" != "yes" && { AC_CHECK_LIB(lber, ber_pvt_opt_on, with_ldap_lber=yes) }
+
+ if test "$with_ldap" = "yes"; then
+ if test "$with_ldap_des" = "yes" ; then
+ OPENLDAP_LIBS="${OPENLDAP_LIBS} -ldes"
+ fi
+ if test "$with_ldap_krb" = "yes" ; then
+ OPENLDAP_LIBS="${OPENLDAP_LIBS} -lkrb"
+ fi
+ if test "$with_ldap_lber" = "yes" ; then
+ OPENLDAP_LIBS="${OPENLDAP_LIBS} -llber"
+ fi
+ OPENLDAP_LIBS="${OPENLDAP_LIBS} -lldap"
+ else
+ AC_MSG_ERROR([OpenLDAP not found])
+ fi
+
+ AC_SUBST(OPENLDAP_LIBS)
+else
+ PKG_CHECK_MODULES(MOZLDAP, mozldap > 6)
+ MOZLDAP_CFLAGS="${MOZLDAP_CFLAGS} -DWITH_MOZLDAP"
+ AC_SUBST(MOZLDAP_CFLAGS)
+fi
+
+
+dnl ---------------------------------------------------------------------------
+dnl - Check for POPT
+dnl ---------------------------------------------------------------------------
+
+POPT_LIBS=
+AC_CHECK_HEADER(popt.h)
+AC_CHECK_LIB(popt, poptGetContext, [POPT_LIBS="-lpopt"])
+AC_SUBST(POPT_LIBS)
+
+dnl ---------------------------------------------------------------------------
+dnl - Check for SASL
+dnl ---------------------------------------------------------------------------
+
+SASL_LIBS=
+AC_CHECK_HEADER(sasl/sasl.h)
+AC_CHECK_LIB(sasl2, sasl_client_init, [SASL_LIBS="-lsasl2"])
+AC_SUBST(SASL_LIBS)
+
+dnl ---------------------------------------------------------------------------
+dnl - Check for Python
+dnl ---------------------------------------------------------------------------
+
+AC_MSG_NOTICE([Checking for Python])
+have_python=no
+AM_PATH_PYTHON(2.3)
+
+if test "x$PYTHON" = "x" ; then
+ AC_MSG_ERROR([Python not found])
+fi
+
+dnl ---------------------------------------------------------------------------
+dnl - Set the data install directory since we don't use pkgdatadir
+dnl ---------------------------------------------------------------------------
+
+IPA_DATA_DIR="$datadir/ipa"
+AC_SUBST(IPA_DATA_DIR)
+
+dnl ---------------------------------------------------------------------------
+dnl Finish
+dnl ---------------------------------------------------------------------------
+
+# Files
+
+AC_CONFIG_FILES([
+ Makefile
+ firefox/Makefile
+ ipaclient/Makefile
+ ipa-install/Makefile
+ man/Makefile
+])
+
+AC_OUTPUT
+
+echo "
+ IPA client $VERSION
+ ========================
+
+ prefix: ${prefix}
+ exec_prefix: ${exec_prefix}
+ libdir: ${libdir}
+ bindir: ${bindir}
+ sbindir: ${sbindir}
+ sysconfdir: ${sysconfdir}
+ localstatedir: ${localstatedir}
+ datadir: ${datadir}
+ source code location: ${srcdir}
+ Maintainer mode: ${USE_MAINTAINER_MODE}
+"
diff --git a/ipa-client/firefox/Makefile.am b/ipa-client/firefox/Makefile.am
new file mode 100644
index 00000000..daf69424
--- /dev/null
+++ b/ipa-client/firefox/Makefile.am
@@ -0,0 +1,18 @@
+NULL =
+
+appdir = $(IPA_DATA_DIR)/ipaclient
+app_DATA = \
+ ipa.cfg \
+ ipa.js \
+ $(NULL)
+
+EXTRA_DIST = \
+ README \
+ $(app_DATA) \
+ globalsetup.sh \
+ usersetup.sh \
+ $(NULL)
+
+MAINTAINERCLEANFILES = \
+ *~ \
+ Makefile.in
diff --git a/ipa-client/firefox/README b/ipa-client/firefox/README
new file mode 100644
index 00000000..67013fcc
--- /dev/null
+++ b/ipa-client/firefox/README
@@ -0,0 +1,23 @@
+Firefox automatic configuration.
+
+ipa.cfg needs to be installed in the Firefox root directory
+(/usr/lib/firefox-version). It can be a symlink somewhere else. We install
+the actual file into /usr/share/ipa.
+
+ipa.js contains the javascript that sets the desired configuration.
+
+The Firefox all.js needs to be modified to set:
+
+pref('general.config.obscure_value', 0);
+pref('general.config.filename', 'ipa.cfg');
+
+First need to remove any existing values for these.
+
+For more information on autoconfiguration, see:
+http://mit.edu/~firefox/www/maintainers/autoconfig.html
+
+globalsetup.sh will change the default setup for all users and will
+lock the preferences. The downside is that rpm -V will show firefox as
+corrupt.
+
+usersetup.sh will change all existing profiles of the current user
diff --git a/ipa-client/firefox/globalsetup.sh b/ipa-client/firefox/globalsetup.sh
new file mode 100644
index 00000000..403c3d3e
--- /dev/null
+++ b/ipa-client/firefox/globalsetup.sh
@@ -0,0 +1,52 @@
+#!/bin/sh
+
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+ipacfg="ipa.cfg"
+
+for file in /usr/lib/firefox-* /usr/lib64/firefox*
+do
+ # Find the configuration file we want to change
+ cfg=`find $file -name all.js`
+
+ # determine the directory by removing all.js
+ dir=`echo $cfg | sed 's/greprefs\/all.js//'`
+
+ # It is possible that there will be empty Firefox directories, so skip
+ # those.
+ if test "X"$cfg != "X"; then
+
+ rm -f $cfg.new
+
+ # If the configuration already exists, remove it
+ if grep general.config.filename $cfg > /dev/null 2>&1; then
+ grep -v general.config.filename $cfg > $cfg.new
+ mv $cfg.new $cfg
+ fi
+
+ # We have the configuration unobscured
+ if grep general.config.filename $cfg > /dev/null 2>&1; then
+ grep -v general.config.obscure_value $cfg > $cfg.new
+ mv $cfg.new $cfg
+ fi
+
+ # Now we can add the new stuff to the file
+ echo "pref('general.config.obscure_value', 0);" >> "$cfg"
+ echo "pref('general.config.filename', '$ipacfg');" >> "$cfg"
+
+ # Create a link to our configuration file
+ rm -f $dir/$ipacfg
+ ln -s /usr/share/ipa/ipa.cfg $dir/$ipacfg
+ fi
+done
diff --git a/ipa-client/firefox/ipa.cfg b/ipa-client/firefox/ipa.cfg
new file mode 100644
index 00000000..448c350b
--- /dev/null
+++ b/ipa-client/firefox/ipa.cfg
@@ -0,0 +1,19 @@
+#
+/*
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ *
+ */
+
+lockPref("autoadmin.global_config_url","file:///usr/share/ipa/ipa.js");
diff --git a/ipa-client/firefox/ipa.js b/ipa-client/firefox/ipa.js
new file mode 100644
index 00000000..a280954a
--- /dev/null
+++ b/ipa-client/firefox/ipa.js
@@ -0,0 +1,34 @@
+/*
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; version 2 only
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ *
+ */
+
+try
+{
+ /* Kerberos SSO configuration */
+ lockPref("network.negotiate-auth.trusted-uris", ".freeipa.org");
+ lockPref("network.negotiate-auth.delegation-uris", ".freeipa.org");
+
+ /* These are the defaults */
+ lockPref("network.negotiate-auth.gsslib", "");
+ lockPref("network.negotiate-auth.using-native-gsslib", true);
+ lockPref("network.negotiate-auth.allow-proxies", true);
+
+ /* For Windows */
+ lockPref("network.auth.use-sspi", false);
+}
+catch(e)
+{
+ displayError("Error in Autoconfig", e);
+}
diff --git a/ipa-client/firefox/usersetup.sh b/ipa-client/firefox/usersetup.sh
new file mode 100644
index 00000000..e8d152b1
--- /dev/null
+++ b/ipa-client/firefox/usersetup.sh
@@ -0,0 +1,40 @@
+#!/bin/sh
+
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+for file in `find $HOME/.mozilla -name prefs.js`
+do
+ rm -f $file.new
+
+ # If the configuration already exists, remove it
+ if grep network.negotiate- $file > /dev/null 2>&1; then
+ grep -v network.negotiate- $file > $file.new
+ mv $file.new $file
+ fi
+
+ # We have the configuration unobscured
+ if grep network.auth.use-sspi $file > /dev/null 2>&1; then
+ grep -v network.auth.use-sspi $file > $file.new
+ mv $file.new $file
+ fi
+
+ # Now we can add the new stuff to the file
+ echo "user_pref('network.auth.use-sspi', false);" >> $file
+ echo "user_pref('network.cookie.prefsMigrated', true);" >> $file
+ echo "user_pref('network.negotiate-auth.allow-proxies', true);" >> $file
+ echo "user_pref('network.negotiate-auth.delegation-uris', '.freeipa.org');" >> $file
+ echo "user_pref('network.negotiate-auth.gsslib', '');" >> $file
+ echo "user_pref('network.negotiate-auth.trusted-uris', '.freeipa.org');" >> $file
+ echo "user_pref('network.negotiate-auth.using-native-gsslib', true);" >> $file
+done
diff --git a/ipa-client/ipa-client.spec.in b/ipa-client/ipa-client.spec.in
new file mode 100644
index 00000000..686259ad
--- /dev/null
+++ b/ipa-client/ipa-client.spec.in
@@ -0,0 +1,86 @@
+Name: ipa-client
+Version: __VERSION__
+Release: __RELEASE__%{?dist}
+Summary: IPA client
+
+Group: System Environment/Base
+License: GPLv2
+URL: http://www.freeipa.org
+Source0: %{name}-%{version}.tgz
+BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n)
+
+Requires: python python-ldap python-krbV ipa-python cyrus-sasl-gssapi
+
+%{!?python_sitelib: %define python_sitelib %(%{__python} -c "from distutils.sysconfig import get_python_lib; print get_python_lib()")}
+
+%description
+IPA is a server for identity, policy, and audit.
+The client package provide install and configuration scripts for clients.
+
+%prep
+%setup -q
+./configure --prefix=%{buildroot}/usr --libdir=%{buildroot}/%{_libdir} --sysconfdir=%{buildroot}/etc --mandir=%{buildroot}/%{_mandir}
+
+%build
+
+make
+
+%install
+rm -rf %{buildroot}
+
+make install
+
+mkdir -p %{buildroot}/%{_localstatedir}/lib/ipa-client/sysrestore
+
+
+%clean
+rm -rf %{buildroot}
+
+
+%files
+%defattr(-,root,root,-)
+%{_sbindir}/ipa-client-install
+%{_sbindir}/ipa-getkeytab
+
+%dir %{_usr}/share/ipa
+%{_usr}/share/ipa/*
+
+%dir %{python_sitelib}/ipaclient
+%{python_sitelib}/ipaclient/*.py*
+
+%dir %{_localstatedir}/lib/ipa-client
+%dir %{_localstatedir}/lib/ipa-client/sysrestore
+
+%{_mandir}/man1/*
+
+%changelog
+* Thu Apr 3 2008 Rob Crittenden <rcritten@redhat.com> - 1.0.0-1
+- Version bump for release
+
+* Mon Feb 25 2008 Rob Crittenden <rcritten@redhat.com> - 0.99.0-2
+- Add ipa-getkeytab man page
+
+* Thu Feb 21 2008 Rob Crittenden <rcritten@redhat.com> - 0.99.0-1
+- Version bump for release
+
+* Thu Jan 31 2008 Rob Crittenden <rcritten@redhat.com> - 0.6.0-3
+- Marked with wrong license. IPA is GPLv2.
+
+* Thu Jan 17 2008 Rob Crittenden <rcritten@redhat.com> - 0.6.0-2
+- Fixed License in specfile
+- Move client python files to /usr/lib*/python*/site-packages
+
+* Fri Dec 21 2007 Karl MacMillan <kmacmill@redhat.com> - 0.6.0-1
+- Version bump for release
+
+* Wed Nov 21 2007 Karl MacMillan <kmacmill@redhat.com> - 0.5.0-1
+- Version bump for release and rpm name change
+
+* Thu Nov 1 2007 Karl MacMillan <kmacmill@redhat.com> - 0.3.1-1
+- Version bump for release
+
+* Thu Oct 18 2007 Karl MacMillan <kmacmill@redhat.com> - 0.3.0-2
+- Convert to autotools-based build
+
+* Thu Aug 16 2007 Simo Sorce <ssorce@redhat.com> - 0.1.0-1
+- Initial rpm version
diff --git a/ipa-client/ipa-getkeytab.c b/ipa-client/ipa-getkeytab.c
new file mode 100644
index 00000000..fbeb547a
--- /dev/null
+++ b/ipa-client/ipa-getkeytab.c
@@ -0,0 +1,836 @@
+/* Authors: Simo Sorce <ssorce@redhat.com>
+ *
+ * Copyright (C) 2007 Red Hat
+ * see file 'COPYING' for use and warranty information
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; version 2 only
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ */
+
+#define _GNU_SOURCE
+
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <sys/time.h>
+#include <unistd.h>
+#include <stdio.h>
+#include <stdarg.h>
+#include <stdlib.h>
+#include <string.h>
+#include <errno.h>
+#include <time.h>
+#define KRB5_PRIVATE 1
+#include <krb5.h>
+#ifdef WITH_MOZLDAP
+#include <mozldap/ldap.h>
+#else
+#include <ldap.h>
+#endif
+#include <sasl/sasl.h>
+#include <popt.h>
+
+/* Salt types */
+#define NO_SALT -1
+#define KRB5_KDB_SALTTYPE_NORMAL 0
+#define KRB5_KDB_SALTTYPE_V4 1
+#define KRB5_KDB_SALTTYPE_NOREALM 2
+#define KRB5_KDB_SALTTYPE_ONLYREALM 3
+#define KRB5_KDB_SALTTYPE_SPECIAL 4
+#define KRB5_KDB_SALTTYPE_AFS3 5
+
+#define KEYTAB_SET_OID "2.16.840.1.113730.3.8.3.1"
+#define KEYTAB_RET_OID "2.16.840.1.113730.3.8.3.2"
+
+struct krb_key_salt {
+ krb5_enctype enctype;
+ krb5_int32 salttype;
+ krb5_keyblock key;
+ krb5_data salt;
+};
+
+struct keys_container {
+ krb5_int32 nkeys;
+ struct krb_key_salt *ksdata;
+};
+
+static int ldap_sasl_interact(LDAP *ld, unsigned flags, void *priv_data, void *sit)
+{
+ sasl_interact_t *in = NULL;
+ int ret = LDAP_OTHER;
+ krb5_principal princ = (krb5_principal)priv_data;
+
+ if (!ld) return LDAP_PARAM_ERROR;
+
+ for (in = sit; in && in->id != SASL_CB_LIST_END; in++) {
+ switch(in->id) {
+ case SASL_CB_USER:
+ in->result = princ->data[0].data;
+ in->len = princ->data[0].length;
+ ret = LDAP_SUCCESS;
+ break;
+ case SASL_CB_GETREALM:
+ in->result = princ->realm.data;
+ in->len = princ->realm.length;
+ ret = LDAP_SUCCESS;
+ break;
+ default:
+ in->result = NULL;
+ in->len = 0;
+ ret = LDAP_OTHER;
+ }
+ }
+ return ret;
+}
+
+static void free_keys_contents(krb5_context krbctx, struct keys_container *keys)
+{
+ struct krb_key_salt *ksdata;
+ int i;
+
+ ksdata = keys->ksdata;
+ for (i = 0; i < keys->nkeys; i++) {
+ krb5_free_keyblock_contents(krbctx, &ksdata[i].key);
+ krb5_free_data_contents(krbctx, &ksdata[i].salt);
+ }
+ free(ksdata);
+
+ keys->ksdata = NULL;
+ keys->nkeys = 0;
+}
+
+/* Determines Encryption and Salt types,
+ * allocates key_salt data storage,
+ * filters out equivalent encodings,
+ * returns 0 if no enctypes available, >0 if enctypes are available */
+static int prep_ksdata(krb5_context krbctx, const char *str,
+ struct keys_container *keys)
+{
+ struct krb_key_salt *ksdata;
+ krb5_error_code krberr;
+ int n, i, j, nkeys;
+
+ if (str == NULL) {
+ krb5_enctype *ktypes;
+
+ krberr = krb5_get_permitted_enctypes(krbctx, &ktypes);
+ if (krberr) {
+ fprintf(stderr, "No system preferred enctypes ?!\n");
+ return 0;
+ }
+
+ for (n = 0; ktypes[n]; n++) /* count */ ;
+
+ ksdata = calloc(n + 1, sizeof(struct krb_key_salt));
+ if (NULL == ksdata) {
+ fprintf(stderr, "Out of memory!?\n");
+ return 0;
+ }
+
+ for (i = 0; i < n; i++) {
+ ksdata[i].enctype = ktypes[i];
+ ksdata[i].salttype = KRB5_KDB_SALTTYPE_NORMAL;
+ }
+
+ krb5_free_ktypes(krbctx, ktypes);
+
+ nkeys = i;
+
+ } else {
+ char *tmp, *t, *p, *q;
+
+ t = tmp = strdup(str);
+ if (!tmp) {
+ fprintf(stderr, "Out of memory\n");
+ return 0;
+ }
+
+ /* count */
+ n = 0;
+ while ((p = strchr(t, ','))) {
+ t = p+1;
+ n++;
+ }
+ n++; /* count the last one that is 0 terminated instead */
+
+ /* at the end we will have at most n entries + 1 terminating */
+ ksdata = calloc(n + 1, sizeof(struct krb_key_salt));
+ if (!ksdata) {
+ fprintf(stderr, "Out of memory\n");
+ return 0;
+ }
+
+ for (i = 0, j = 0, t = tmp; i < n; i++) {
+
+ p = strchr(t, ',');
+ if (p) *p = '\0';
+
+ q = strchr(t, ':');
+ if (q) *q++ = '\0';
+
+ krberr = krb5_string_to_enctype(t, &ksdata[j].enctype);
+ if (krberr != 0) {
+ fprintf(stderr,
+ "Warning unrecognized encryption type: [%s]\n", t);
+ t = p+1;
+ continue;
+ }
+ t = p+1;
+
+ if (!q) {
+ ksdata[j].salttype = KRB5_KDB_SALTTYPE_NORMAL;
+ j++;
+ continue;
+ }
+
+ krberr = krb5_string_to_salttype(q, &ksdata[j].salttype);
+ if (krberr != 0) {
+ fprintf(stderr, "Warning unrecognized salt type: [%s]\n", q);
+ continue;
+ }
+
+ j++;
+ }
+
+ nkeys = j;
+
+ free(tmp);
+ }
+
+ /* Check we don't already have a key with a similar encoding,
+ * it would just produce redundant data and this is what the
+ * MIT code do anyway */
+
+ for (i = 0, n = 0; i < nkeys; i++ ) {
+ int similar = 0;
+
+ for (j = 0; j < i; j++) {
+ krberr = krb5_c_enctype_compare(krbctx,
+ ksdata[j].enctype,
+ ksdata[i].enctype,
+ &similar);
+ if (krberr) {
+ free_keys_contents(krbctx, keys);
+ fprintf(stderr, "Enctype comparison failed!\n");
+ return 0;
+ }
+ if (similar &&
+ (ksdata[j].salttype == ksdata[i].salttype)) {
+ break;
+ }
+ }
+ if (j < i) {
+ /* redundant encoding, remove it, and shift others */
+ int x;
+ for (x = i; x < nkeys-1; x++) {
+ ksdata[x].enctype = ksdata[x+1].enctype;
+ ksdata[x].salttype = ksdata[x+1].salttype;
+ }
+ continue;
+ }
+ /* count only confirmed enc/salt tuples */
+ n++;
+ }
+
+ keys->nkeys = n;
+ keys->ksdata = ksdata;
+
+ return n;
+}
+
+static int create_keys(krb5_context krbctx,
+ krb5_principal princ,
+ char *password,
+ const char *enctypes_string,
+ struct keys_container *keys)
+{
+ struct krb_key_salt *ksdata;
+ krb5_error_code krberr;
+ krb5_data key_password;
+ krb5_data *realm;
+ int i, j, nkeys;
+ int ret;
+
+ ret = prep_ksdata(krbctx, enctypes_string, keys);
+ if (ret == 0) return 0;
+
+ ksdata = keys->ksdata;
+ nkeys = keys->nkeys;
+
+ if (password) {
+ key_password.data = password;
+ key_password.length = strlen(password);
+
+ realm = krb5_princ_realm(krbctx, princ);
+ }
+
+ for (i = 0; i < nkeys; i++) {
+ krb5_data *salt;
+
+ if (!password) {
+ /* cool, random keys */
+ krberr = krb5_c_make_random_key(krbctx,
+ ksdata[i].enctype,
+ &ksdata[i].key);
+ if (krberr) {
+ fprintf(stderr, "Failed to create random key!\n");
+ return 0;
+ }
+ /* set the salt to NO_SALT as the key was random */
+ ksdata[i].salttype = NO_SALT;
+ continue;
+ }
+
+ /* Make keys using password and required salt */
+ switch (ksdata[i].salttype) {
+ case KRB5_KDB_SALTTYPE_ONLYREALM:
+ krberr = krb5_copy_data(krbctx, realm, &salt);
+ if (krberr) {
+ fprintf(stderr, "Failed to create key!\n");
+ return 0;
+ }
+
+ ksdata[i].salt.length = salt->length;
+ ksdata[i].salt.data = malloc(salt->length);
+ if (!ksdata[i].salt.data) {
+ fprintf(stderr, "Out of memory!\n");
+ return 0;
+ }
+ memcpy(ksdata[i].salt.data, salt->data, salt->length);
+ krb5_free_data(krbctx, salt);
+ break;
+
+ case KRB5_KDB_SALTTYPE_NOREALM:
+ krberr = krb5_principal2salt_norealm(krbctx, princ, &ksdata[i].salt);
+ if (krberr) {
+ fprintf(stderr, "Failed to create key!\n");
+ return 0;
+ }
+ break;
+
+ case KRB5_KDB_SALTTYPE_NORMAL:
+ krberr = krb5_principal2salt(krbctx, princ, &ksdata[i].salt);
+ if (krberr) {
+ fprintf(stderr, "Failed to create key!\n");
+ return 0;
+ }
+ break;
+
+ /* no KRB5_KDB_SALTTYPE_V4, we do not support krb v4 */
+
+ case KRB5_KDB_SALTTYPE_AFS3:
+ /* Comment from MIT sources:
+ * * Why do we do this? Well, the afs_mit_string_to_key
+ * * needs to use strlen, and the realm is not NULL
+ * * terminated....
+ */
+ ksdata[i].salt.data = (char *)malloc(realm->length + 1);
+ if (NULL == ksdata[i].salt.data) {
+ fprintf(stderr, "Out of memory!\n");
+ return 0;
+ }
+ memcpy((char *)ksdata[i].salt.data,
+ (char *)realm->data, realm->length);
+ ksdata[i].salt.data[realm->length] = '\0';
+ /* AFS uses a special length (UGLY) */
+ ksdata[i].salt.length = SALT_TYPE_AFS_LENGTH;
+ break;
+
+ default:
+ fprintf(stderr, "Bad or unsupported salt type (%d)!\n",
+ ksdata[i].salttype);
+ return 0;
+ }
+
+ krberr = krb5_c_string_to_key(krbctx,
+ ksdata[i].enctype,
+ &key_password,
+ &ksdata[i].salt,
+ &ksdata[i].key);
+ if (krberr) {
+ fprintf(stderr, "Failed to create key!\n");
+ return 0;
+ }
+
+ /* set back salt length to real value if AFS3 */
+ if (ksdata[i].salttype == KRB5_KDB_SALTTYPE_AFS3) {
+ ksdata[i].salt.length = realm->length;
+ }
+ }
+
+ return nkeys;
+}
+
+static struct berval *create_key_control(struct keys_container *keys,
+ const char *principalName)
+{
+ struct krb_key_salt *ksdata;
+ struct berval *bval;
+ BerElement *be;
+ int ret, i;
+
+ be = ber_alloc_t(LBER_USE_DER);
+ if (!be) {
+ return NULL;
+ }
+
+ ret = ber_printf(be, "{s{", principalName);
+ if (ret == -1) {
+ ber_free(be, 1);
+ return NULL;
+ }
+
+ ksdata = keys->ksdata;
+ for (i = 0; i < keys->nkeys; i++) {
+
+ /* we set only the EncryptionKey and salt, no s2kparams */
+
+ ret = ber_printf(be, "{t[{t[i]t[o]}]",
+ (ber_tag_t)(LBER_CONSTRUCTED | LBER_CLASS_CONTEXT | 0),
+ (ber_tag_t)(LBER_CONSTRUCTED | LBER_CLASS_CONTEXT | 0),
+ (ber_int_t)ksdata[i].enctype,
+ (ber_tag_t)(LBER_CONSTRUCTED | LBER_CLASS_CONTEXT | 1),
+ (char *)ksdata[i].key.contents, (ber_len_t)ksdata[i].key.length);
+
+ if (ret == -1) {
+ ber_free(be, 1);
+ return NULL;
+ }
+
+ if (ksdata[i].salttype == NO_SALT) {
+ ret = ber_printf(be, "}");
+ continue;
+ }
+
+ /* we have to pass a salt structure */
+ ret = ber_printf(be, "t[{t[i]t[o]}]}",
+ (ber_tag_t)(LBER_CONSTRUCTED | LBER_CLASS_CONTEXT | 1),
+ (ber_tag_t)(LBER_CONSTRUCTED | LBER_CLASS_CONTEXT | 0),
+ (ber_int_t)ksdata[i].salttype,
+ (ber_tag_t)(LBER_CONSTRUCTED | LBER_CLASS_CONTEXT | 1),
+ (char *)ksdata[i].salt.data, (ber_len_t)ksdata[i].salt.length);
+
+ if (ret == -1) {
+ ber_free(be, 1);
+ return NULL;
+ }
+ }
+
+ ret = ber_printf(be, "}}");
+ if (ret == -1) {
+ ber_free(be, 1);
+ return NULL;
+ }
+
+ ret = ber_flatten(be, &bval);
+ if (ret == -1) {
+ ber_free(be, 1);
+ return NULL;
+ }
+
+ ber_free(be, 1);
+ return bval;
+}
+
+int filter_keys(krb5_context krbctx, struct keys_container *keys,
+ ber_int_t *enctypes)
+{
+ struct krb_key_salt *ksdata;
+ int i, j, n;
+
+ n = keys->nkeys;
+ ksdata = keys->ksdata;
+ for (i = 0; i < n; i++) {
+ if (ksdata[i].enctype == enctypes[i]) continue;
+ if (enctypes[i] == 0) {
+ /* remove unsupported one */
+ krb5_free_keyblock_contents(krbctx, &ksdata[i].key);
+ krb5_free_data_contents(krbctx, &ksdata[i].salt);
+ for (j = i; j < n-1; j++) {
+ keys[j] = keys[j + 1];
+ }
+ n--;
+ /* new key has been moved to this position, make sure
+ * we do not skip it, by neutralizing next i increment */
+ i--;
+ }
+ }
+
+ if (n == 0) {
+ fprintf(stderr, "No keys accepted by KDC\n");
+ return 0;
+ }
+
+ keys->nkeys = n;
+ return n;
+}
+
+static int ldap_set_keytab(krb5_context krbctx,
+ const char *servername,
+ const char *principal_name,
+ krb5_principal princ,
+ struct keys_container *keys)
+{
+ int version;
+ LDAP *ld = NULL;
+ BerElement *sctrl = NULL;
+ struct berval *control = NULL;
+ char *retoid = NULL;
+ struct berval *retdata = NULL;
+ struct timeval tv;
+ LDAPMessage *res = NULL;
+ LDAPControl **srvctrl = NULL;
+ LDAPControl *pprc = NULL;
+ char *err = NULL;
+ int msgid;
+ int ret, rc;
+ int kvno, i;
+ ber_tag_t rtag;
+ ber_int_t *encs = NULL;
+
+ /* cant' return more than nkeys, sometimes less */
+ encs = calloc(keys->nkeys + 1, sizeof(ber_int_t));
+ if (!encs) {
+ fprintf(stderr, "Out of Memory!\n");
+ return 0;
+ }
+
+ /* build password change control */
+ control = create_key_control(keys, principal_name);
+ if (!control) {
+ fprintf(stderr, "Failed to create control!\n");
+ goto error_out;
+ }
+
+ /* TODO: support referrals ? */
+ ld = ldap_init(servername, 389);
+ if(ld == NULL) {
+ fprintf(stderr, "Unable to initialize ldap library!\n");
+ goto error_out;
+ }
+
+ version = LDAP_VERSION3;
+ ret = ldap_set_option(ld, LDAP_OPT_PROTOCOL_VERSION, &version);
+ if (ret != LDAP_SUCCESS) {
+ fprintf(stderr, "Unable to set ldap options!\n");
+ goto error_out;
+ }
+
+ ret = ldap_sasl_interactive_bind_s(ld,
+ NULL, "GSSAPI",
+ NULL, NULL,
+ LDAP_SASL_QUIET,
+ ldap_sasl_interact, princ);
+ if (ret != LDAP_SUCCESS) {
+ fprintf(stderr, "SASL Bind failed!\n");
+ goto error_out;
+ }
+
+ /* find base dn */
+ /* TODO: address the case where we have multiple naming contexts */
+ tv.tv_sec = 10;
+ tv.tv_usec = 0;
+
+ /* perform password change */
+ ret = ldap_extended_operation(ld,
+ KEYTAB_SET_OID,
+ control, NULL, NULL,
+ &msgid);
+ if (ret != LDAP_SUCCESS) {
+ fprintf(stderr, "Operation failed! %s\n", ldap_err2string(ret));
+ goto error_out;
+ }
+
+ ber_bvfree(control);
+ control = NULL;
+
+ tv.tv_sec = 10;
+ tv.tv_usec = 0;
+
+ ret = ldap_result(ld, msgid, 1, &tv, &res);
+ if (ret == -1) {
+ fprintf(stderr, "Operation failed! %s\n", ldap_err2string(ret));
+ goto error_out;
+ }
+
+ ret = ldap_parse_extended_result(ld, res, &retoid, &retdata, 0);
+ if(ret != LDAP_SUCCESS) {
+ fprintf(stderr, "Operation failed! %s\n", ldap_err2string(ret));
+ goto error_out;
+ }
+
+ ret = ldap_parse_result(ld, res, &rc, NULL, &err, NULL, &srvctrl, 0);
+ if(ret != LDAP_SUCCESS || rc != LDAP_SUCCESS) {
+ fprintf(stderr, "Operation failed! %s\n", err?err:ldap_err2string(ret));
+ goto error_out;
+ }
+
+ if (!srvctrl) {
+ fprintf(stderr, "Missing reply control!\n");
+ goto error_out;
+ }
+
+ for (i = 0; srvctrl[i]; i++) {
+ if (0 == strcmp(srvctrl[i]->ldctl_oid, KEYTAB_RET_OID)) {
+ pprc = srvctrl[i];
+ }
+ }
+ if (!pprc) {
+ fprintf(stderr, "Missing reply control!\n");
+ goto error_out;
+ }
+
+ sctrl = ber_init(&pprc->ldctl_value);
+
+ if (!sctrl) {
+ fprintf(stderr, "ber_init() failed, Invalid control ?!\n");
+ goto error_out;
+ }
+
+ /* Format of response
+ *
+ * KeytabGetRequest ::= SEQUENCE {
+ * new_kvno Int32
+ * SEQUENCE OF KeyTypes
+ * }
+ *
+ * * List of accepted enctypes *
+ * KeyTypes ::= SEQUENCE {
+ * enctype Int32
+ * }
+ */
+
+ rtag = ber_scanf(sctrl, "{i{", &kvno);
+ if (rtag == LBER_ERROR) {
+ fprintf(stderr, "ber_scanf() failed, Invalid control ?!\n");
+ goto error_out;
+ }
+
+ for (i = 0; i < keys->nkeys; i++) {
+ ret = ber_scanf(sctrl, "{i}", &encs[i]);
+ if (ret == LBER_ERROR) break;
+ }
+
+ ret = filter_keys(krbctx, keys, encs);
+ if (ret == 0) goto error_out;
+
+ if (err) ldap_memfree(err);
+ ber_free(sctrl, 1);
+ ldap_controls_free(srvctrl);
+ ldap_msgfree(res);
+ ldap_unbind_ext(ld, NULL, NULL);
+ return kvno;
+
+error_out:
+ if (sctrl) ber_free(sctrl, 1);
+ if (srvctrl) ldap_controls_free(srvctrl);
+ if (err) ldap_memfree(err);
+ if (res) ldap_msgfree(res);
+ if (ld) ldap_unbind_ext(ld, NULL, NULL);
+ if (control) ber_bvfree(control);
+ free(encs);
+ return 0;
+}
+
+static char *ask_password(krb5_context krbctx)
+{
+ krb5_prompt ap_prompts[2];
+ krb5_data k5d_pw0;
+ krb5_data k5d_pw1;
+ char pw0[256];
+ char pw1[256];
+ char *password;
+
+ k5d_pw0.length = sizeof(pw0);
+ k5d_pw0.data = pw0;
+ ap_prompts[0].prompt = "New Principal Password";
+ ap_prompts[0].hidden = 1;
+ ap_prompts[0].reply = &k5d_pw0;
+
+ k5d_pw1.length = sizeof(pw1);
+ k5d_pw1.data = pw1;
+ ap_prompts[1].prompt = "Verify Principal Password";
+ ap_prompts[1].hidden = 1;
+ ap_prompts[1].reply = &k5d_pw1;
+
+ krb5_prompter_posix(krbctx, NULL,
+ NULL, NULL,
+ 2, ap_prompts);
+
+ if (strcmp(pw0, pw1)) {
+ fprintf(stderr, "Passwords do not match!");
+ return NULL;
+ }
+
+ password = malloc(k5d_pw0.length + 1);
+ if (!password) return NULL;
+ memcpy(password, pw0, k5d_pw0.length);
+ password[k5d_pw0.length] = '\0';
+
+ return password;
+}
+
+int main(int argc, char *argv[])
+{
+ static const char *server = NULL;
+ static const char *principal = NULL;
+ static const char *keytab = NULL;
+ static const char *enctypes_string = NULL;
+ int quiet = 0;
+ int askpass = 0;
+ int permitted_enctypes = 0;
+ struct poptOption options[] = {
+ { "quiet", 'q', POPT_ARG_NONE, &quiet, 0, "Print as little as possible", "Output only on errors"},
+ { "server", 's', POPT_ARG_STRING, &server, 0, "Contact this specific KDC Server", "Server Name" },
+ { "principal", 'p', POPT_ARG_STRING, &principal, 0, "The principal to get a keytab for (ex: ftp/ftp.example.com@EXAMPLE.COM)", "Kerberos Service Principal Name" },
+ { "keytab", 'k', POPT_ARG_STRING, &keytab, 0, "File were to store the keytab information", "Keytab File Name" },
+ { "enctypes", 'e', POPT_ARG_STRING, &enctypes_string, 0, "Encryption types to request", "Comma separated encryption types list" },
+ { "permitted-enctypes", 0, POPT_ARG_NONE, &permitted_enctypes, 0, "Show the list of permitted encryption types and exit", "Permitted Encryption Types"},
+ { "password", 'P', POPT_ARG_NONE, &askpass, 0, "Asks for a non-random password to use for the principal" },
+ { NULL, 0, POPT_ARG_NONE, NULL, 0, NULL, NULL }
+ };
+ poptContext pc;
+ char *ktname;
+ char *password = NULL;
+ krb5_context krbctx;
+ krb5_ccache ccache;
+ krb5_principal uprinc;
+ krb5_principal sprinc;
+ krb5_error_code krberr;
+ ber_int_t *enctypes;
+ struct keys_container keys;
+ krb5_keytab kt;
+ int kvno;
+ int i, ret;
+
+ krberr = krb5_init_context(&krbctx);
+ if (krberr) {
+ fprintf(stderr, "Kerberos context initialization failed\n");
+ exit(1);
+ }
+
+ pc = poptGetContext("ipa-getkeytab", argc, (const char **)argv, options, 0);
+ ret = poptGetNextOpt(pc);
+ if (ret == -1 && permitted_enctypes &&
+ !(server || principal || keytab || quiet)) {
+ krb5_enctype *ktypes;
+ char enc[79]; /* fit std terminal or truncate */
+
+ krberr = krb5_get_permitted_enctypes(krbctx, &ktypes);
+ if (krberr) {
+ fprintf(stderr, "No system preferred enctypes ?!\n");
+ exit(1);
+ }
+ fprintf(stdout, "Supported encryption types:\n");
+ for (i = 0; ktypes[i]; i++) {
+ krberr = krb5_enctype_to_string(ktypes[i], enc, 79);
+ if (krberr) {
+ fprintf(stderr, "Warning: failed to convert type (#%d)\n", i);
+ continue;
+ }
+ fprintf(stdout, "%s\n", enc);
+ }
+ krb5_free_ktypes(krbctx, ktypes);
+ exit (0);
+ }
+
+ if (ret != -1 || !server || !principal || !keytab || permitted_enctypes) {
+ if (!quiet) {
+ poptPrintUsage(pc, stderr, 0);
+ }
+ exit(2);
+ }
+
+ if (askpass) {
+ password = ask_password(krbctx);
+ if (!password) {
+ exit(2);
+ }
+ } else if (enctypes_string && strchr(enctypes_string, ':')) {
+ if (!quiet) {
+ fprintf(stderr, "Warning: salt types are not honored with randomized passwords (see opt. -P)\n");
+ }
+ }
+
+ ret = asprintf(&ktname, "WRFILE:%s", keytab);
+ if (ret == -1) {
+ exit(3);
+ }
+
+ krberr = krb5_parse_name(krbctx, principal, &sprinc);
+ if (krberr) {
+ fprintf(stderr, "Invalid Service Principal Name\n");
+ exit(4);
+ }
+
+ krberr = krb5_cc_default(krbctx, &ccache);
+ if (krberr) {
+ fprintf(stderr, "Kerberos Credential Cache not found\n"
+ "Do you have a Kerberos Ticket?\n");
+ exit(5);
+ }
+
+ krberr = krb5_cc_get_principal(krbctx, ccache, &uprinc);
+ if (krberr) {
+ fprintf(stderr, "Kerberos User Principal not found\n"
+ "Do you have a valid Credential Cache?\n");
+ exit(6);
+ }
+
+ krberr = krb5_kt_resolve(krbctx, ktname, &kt);
+ if (krberr) {
+ fprintf(stderr, "Failed to open Keytab\n");
+ exit(7);
+ }
+
+ /* create key material */
+ ret = create_keys(krbctx, sprinc, password, enctypes_string, &keys);
+ if (!ret) {
+ fprintf(stderr, "Failed to create key material\n");
+ exit(8);
+ }
+
+ kvno = ldap_set_keytab(krbctx, server, principal, uprinc, &keys);
+ if (!kvno) {
+ exit(9);
+ }
+
+ for (i = 0; i < keys.nkeys; i++) {
+ krb5_keytab_entry kt_entry;
+ memset((char *)&kt_entry, 0, sizeof(kt_entry));
+ kt_entry.principal = sprinc;
+ kt_entry.key = keys.ksdata[i].key;
+ kt_entry.vno = kvno;
+
+ krberr = krb5_kt_add_entry(krbctx, kt, &kt_entry);
+ if (krberr) {
+ fprintf(stderr, "Failed to add key to the keytab\n");
+ exit (11);
+ }
+ }
+
+ free_keys_contents(krbctx, &keys);
+
+ krberr = krb5_kt_close(krbctx, kt);
+ if (krberr) {
+ fprintf(stderr, "Failed to close the keytab\n");
+ exit (12);
+ }
+
+ if (!quiet) {
+ fprintf(stderr,
+ "Keytab successfully retrieved and stored in: %s\n",
+ keytab);
+ }
+ exit(0);
+}
diff --git a/ipa-client/ipa-install/Makefile.am b/ipa-client/ipa-install/Makefile.am
new file mode 100644
index 00000000..ad0c4e0c
--- /dev/null
+++ b/ipa-client/ipa-install/Makefile.am
@@ -0,0 +1,13 @@
+NULL =
+
+sbin_SCRIPTS = \
+ ipa-client-install \
+ $(NULL)
+
+EXTRA_DIST = \
+ $(sbin_SCRIPTS) \
+ $(NULL)
+
+MAINTAINERCLEANFILES = \
+ *~ \
+ Makefile.in
diff --git a/ipa-client/ipa-install/ipa-client-install b/ipa-client/ipa-install/ipa-client-install
new file mode 100644
index 00000000..cd5bfdde
--- /dev/null
+++ b/ipa-client/ipa-install/ipa-client-install
@@ -0,0 +1,387 @@
+#! /usr/bin/python -E
+# Authors: Simo Sorce <ssorce@redhat.com>
+# Karl MacMillan <kmacmillan@mentalrootkit.com>
+#
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+try:
+ import sys
+
+ import os
+ import krbV
+ import socket
+ import logging
+ from optparse import OptionParser
+ import ipaclient.ipadiscovery
+ import ipaclient.ipachangeconf
+ import ipaclient.ntpconf
+ from ipa.ipautil import run, user_input
+ from ipa import sysrestore
+ from ipa import version
+except ImportError:
+ print >> sys.stderr, """\
+There was a problem importing one of the required Python modules. The
+error was:
+
+ %s
+""" % sys.exc_value
+ sys.exit(1)
+
+
+def parse_options():
+ parser = OptionParser(version=version.VERSION)
+ parser.add_option("--domain", dest="domain", help="domain name")
+ parser.add_option("--server", dest="server", help="IPA server")
+ parser.add_option("--realm", dest="realm_name", help="realm name")
+ parser.add_option("-f", "--force", dest="force", action="store_true",
+ default=False, help="force setting of ldap/kerberos conf")
+ parser.add_option("-d", "--debug", dest="debug", action="store_true",
+ default=False, help="print debugging information")
+ parser.add_option("-U", "--unattended", dest="unattended",
+ action="store_true",
+ help="unattended installation never prompts the user")
+ parser.add_option("--ntp-server", dest="ntp_server", help="ntp server to use")
+ parser.add_option("-N", "--no-ntp", action="store_false",
+ help="do not configure ntp", default=True, dest="conf_ntp")
+ parser.add_option("--on-master", dest="on_master", action="store_true",
+ help="use this option when run on a master", default=False)
+ parser.add_option("", "--uninstall", dest="uninstall", action="store_true",
+ default=False, help="uninstall an existing installation")
+
+ options, args = parser.parse_args()
+
+ if (options.server and not options.domain):
+ parser.error("--server cannot be used without providing --domain")
+
+ return options
+
+def logging_setup(options):
+ # Always log everything (i.e., DEBUG) to the log
+ # file.
+
+ log_file = "/var/log/ipaclient-install.log"
+ if options.uninstall:
+ log_file = "/var/log/ipaclient-uninstall.log"
+
+ old_umask = os.umask(077)
+ logging.basicConfig(level=logging.DEBUG,
+ format='%(asctime)s %(levelname)s %(message)s',
+ filename=log_file,
+ filemode='w')
+ os.umask(old_umask)
+
+ console = logging.StreamHandler()
+ # If the debug option is set, also log debug messages to the console
+ if options.debug:
+ console.setLevel(logging.DEBUG)
+ else:
+ # Otherwise, log critical and error messages
+ console.setLevel(logging.ERROR)
+ formatter = logging.Formatter('%(name)-12s: %(levelname)-8s %(message)s')
+ console.setFormatter(formatter)
+ logging.getLogger('').addHandler(console)
+
+def uninstall(options):
+
+ print "Restoring client configuration files"
+ fstore.restore_all_files()
+
+ print "Disabling client Kerberos and Ldap configurations"
+ try:
+ run(["/usr/sbin/authconfig", "--disableldap", "--disablekrb5", "--update"])
+ except Exception, e:
+ print "Failed to remove krb5/ldap configuration. " +str(e)
+ sys.exit(1)
+
+ try:
+ run(["/sbin/service", "nscd", "restart"])
+ except:
+ print "Failed to restart start the NSCD daemon"
+
+ if not options.unattended:
+ print "The original nsswitch.conf configuration has been restored."
+ print "You may need to restart services or reboot the machine."
+ if not options.on_master:
+ if user_input("Do you want to reboot the machine?", False):
+ try:
+ run(["/usr/bin/reboot"])
+ except Exception, e:
+ print "Reboot command failed to exceute. " + str(e)
+ sys.exit(1)
+
+def main():
+ options = parse_options()
+ logging_setup(options)
+ dnsok = True
+
+ global fstore
+ fstore = sysrestore.FileStore('/var/lib/ipa-client/sysrestore')
+
+ if options.uninstall:
+ return uninstall(options)
+
+ cli_domain = None
+ cli_server = None
+ cli_realm = None
+ cli_basedn = None
+
+ # Create the discovery instance
+ ds = ipaclient.ipadiscovery.IPADiscovery()
+
+ ret = ds.search(domain=options.domain, server=options.server)
+ if ret == -10:
+ print "Can't get the fully qualified name of this host"
+ print "Please check that the client is properly configured"
+ return ret
+ if ret == -1 or not ds.getDomainName():
+ logging.debug("Domain not found")
+ if options.domain:
+ cli_domain = options.domain
+ elif options.unattended:
+ return ret
+ else:
+ print "DNS discovery failed to determine your DNS domain"
+ cli_domain = user_input("Please provide the domain name of your IPA server (ex: example.com)", allow_empty = False)
+ ret = ds.search(domain=cli_domain, server=options.server)
+ if not cli_domain:
+ if ds.getDomainName():
+ cli_domain = ds.getDomainName()
+
+ if ret == -2 or not ds.getServerName():
+ dnsok = False
+ logging.debug("IPA Server not found")
+ if options.server:
+ cli_server = options.server
+ elif options.unattended:
+ return ret
+ else:
+ print "DNS discovery failed to find the IPA Server"
+ cli_server = user_input("Please provide your IPA server name (ex: ipa.example.com)", allow_empty = False)
+ ret = ds.search(domain=cli_domain, server=cli_server)
+ if not cli_server:
+ if ds.getServerName():
+ cli_server = ds.getServerName()
+
+ if ret != 0:
+ print "Failed to verify that "+cli_server+" is an IPA Server."
+ print "This may mean that the remote server is not up or is not reachable"
+ print "due to network or firewall settings."
+ return ret
+
+ if dnsok:
+ print "Discovery was successful!"
+ elif not options.unattended:
+ print "\nThe failure to use DNS to find your IPA server indicates that your"
+ print "resolv.conf file is not properly configured.\n"
+ print "Autodiscovery of servers for failover cannot work with this configuration.\n"
+ print "If you proceed with the installation, services will be configured to always"
+ print "access the discovered server for all operation and will not fail over to"
+ print "other servers in case of failure.\n"
+ if not user_input("Do you want to proceed and configure the system with fixed values with no DNS discovery?", False):
+ return ret
+
+ if options.realm_name and options.realm_name != ds.getRealmName():
+ if not options.unattended:
+ print "ERROR: The provided realm name: ["+options.realm_name+"] does not match with the discovered one: ["+ds.getRealmName()+"]\n"
+ return -3
+
+ cli_realm = ds.getRealmName()
+ cli_basedn = ds.getBaseDN()
+
+ print "Realm: "+cli_realm
+ print "DNS Domain: "+cli_domain
+ print "IPA Server: "+cli_server
+ print "BaseDN: "+cli_basedn
+
+ print "\n"
+ if not options.unattended and not user_input("Continue to configure the system with these values?", False):
+ return 1
+
+ # Configure ipa.conf
+ if not options.on_master:
+ ipaconf = ipaclient.ipachangeconf.IPAChangeConf("IPA Installer")
+ ipaconf.setOptionAssignment(" = ")
+ ipaconf.setSectionNameDelimiters(("[","]"))
+
+ opts = [{'name':'comment', 'type':'comment', 'value':'File modified by ipa-client-install'},
+ {'name':'empty', 'type':'empty'}]
+
+ #[defaults]
+ defopts = [{'name':'server', 'type':'option', 'value':cli_server},
+ {'name':'realm', 'type':'option', 'value':cli_realm},
+ {'name':'domain', 'type':'option', 'value':cli_domain}]
+
+ opts.append({'name':'defaults', 'type':'section', 'value':defopts})
+ opts.append({'name':'empty', 'type':'empty'})
+
+ fstore.backup_file("/etc/ipa/ipa.conf")
+ ipaconf.newConf("/etc/ipa/ipa.conf", opts)
+ print "Created /etc/ipa/ipa.conf"
+
+
+ # Configure ldap.conf
+ ldapconf = ipaclient.ipachangeconf.IPAChangeConf("IPA Installer")
+ ldapconf.setOptionAssignment(" ")
+
+ opts = [{'name':'comment', 'type':'comment', 'value':'File modified by ipa-client-install'},
+ {'name':'empty', 'type':'empty'},
+ {'name':'ldap_version', 'type':'option', 'value':'3'},
+ {'name':'base', 'type':'option', 'value':cli_basedn},
+ {'name':'empty', 'type':'empty'},
+ {'name':'nss_base_passwd', 'type':'option', 'value':'cn=users,cn=accounts,'+cli_basedn+'?sub'},
+ {'name':'nss_base_group', 'type':'option', 'value':'cn=groups,cn=accounts,'+cli_basedn+'?sub'},
+ {'name':'nss_schema', 'type':'option', 'value':'rfc2307bis'},
+ {'name':'nss_map_attribute', 'type':'option', 'value':'uniqueMember member'},
+ {'name':'nss_initgroups_ignoreusers', 'type':'option', 'value':'root,dirsrv'},
+ {'name':'empty', 'type':'empty'},
+ {'name':'nss_reconnect_maxsleeptime', 'type':'option', 'value':'8'},
+ {'name':'nss_reconnect_sleeptime', 'type':'option', 'value':'1'},
+ {'name':'bind_timelimit', 'type':'option', 'value':'5'},
+ {'name':'timelimit', 'type':'option', 'value':'15'},
+ {'name':'empty', 'type':'empty'}]
+ if not dnsok or options.force or options.on_master:
+ if options.on_master:
+ opts.append({'name':'uri', 'type':'option', 'value':'ldap://localhost'})
+ else:
+ opts.append({'name':'uri', 'type':'option', 'value':'ldap://'+cli_server})
+ else:
+ opts.append({'name':'nss_srv_domain', 'type':'option', 'value':cli_domain})
+
+ opts.append({'name':'empty', 'type':'empty'})
+ try:
+ fstore.backup_file("/etc/ldap.conf")
+ ldapconf.newConf("/etc/ldap.conf", opts)
+ print "Configured /etc/ldap.conf"
+ except Exception, e:
+ print "Creation of /etc/ldap.conf: " + str(e)
+ return 1
+
+ #If on master assume kerberos is already configured properly.
+ if not options.on_master:
+
+ #Configure krb5.conf
+ krbconf = ipaclient.ipachangeconf.IPAChangeConf("IPA Installer")
+ krbconf.setOptionAssignment(" = ")
+ krbconf.setSectionNameDelimiters(("[","]"))
+ krbconf.setSubSectionDelimiters(("{","}"))
+ krbconf.setIndent((""," "," "))
+
+ opts = [{'name':'comment', 'type':'comment', 'value':'File modified by ipa-client-install'},
+ {'name':'empty', 'type':'empty'}]
+
+ #[libdefaults]
+ libopts = [{'name':'default_realm', 'type':'option', 'value':cli_realm}]
+ if not dnsok or options.force:
+ libopts.append({'name':'dns_lookup_realm', 'type':'option', 'value':'false'})
+ libopts.append({'name':'dns_lookup_kdc', 'type':'option', 'value':'false'})
+ else:
+ libopts.append({'name':'dns_lookup_realm', 'type':'option', 'value':'true'})
+ libopts.append({'name':'dns_lookup_kdc', 'type':'option', 'value':'true'})
+ libopts.append({'name':'ticket_lifetime', 'type':'option', 'value':'24h'})
+ libopts.append({'name':'forwardable', 'type':'option', 'value':'yes'})
+
+ opts.append({'name':'libdefaults', 'type':'section', 'value':libopts})
+ opts.append({'name':'empty', 'type':'empty'})
+
+ #the following are necessary only if DNS discovery does not work
+ if not dnsok or options.force:
+ #[realms]
+ kropts =[{'name':'kdc', 'type':'option', 'value':cli_server+':88'},
+ {'name':'admin_server', 'type':'option', 'value':cli_server+':749'},
+ {'name':'default_domain', 'type':'option', 'value':cli_domain}]
+ ropts = [{'name':cli_realm, 'type':'subsection', 'value':kropts}]
+ opts.append({'name':'realms', 'type':'section', 'value':ropts})
+ opts.append({'name':'empty', 'type':'empty'})
+
+ #[domain_realm]
+ dropts = [{'name':'.'+cli_domain, 'type':'option', 'value':cli_realm},
+ {'name':cli_domain, 'type':'option', 'value':cli_realm}]
+ opts.append({'name':'domain_realm', 'type':'section', 'value':dropts})
+ opts.append({'name':'empty', 'type':'empty'})
+
+ #[appdefaults]
+ pamopts = [{'name':'debug', 'type':'option', 'value':'false'},
+ {'name':'ticket_lifetime', 'type':'option', 'value':'36000'},
+ {'name':'renew_lifetime', 'type':'option', 'value':'36000'},
+ {'name':'forwardable', 'type':'option', 'value':'true'},
+ {'name':'krb4_convert', 'type':'option', 'value':'false'}]
+ appopts = [{'name':'pam', 'type':'subsection', 'value':pamopts}]
+ opts.append({'name':'appdefaults', 'type':'section', 'value':appopts})
+
+ fstore.backup_file("/etc/krb5.conf")
+ krbconf.newConf("/etc/krb5.conf", opts);
+ print "Configured /etc/krb5.conf for IPA realm " + cli_realm
+
+ #Modify nsswitch to add nss_ldap
+ run(["/usr/sbin/authconfig", "--enableldap", "--update"])
+ print "LDAP enabled"
+
+ #Check nss_ldap is working properly
+ if not options.on_master:
+ try:
+ run(["getent", "passwd", "admin"])
+ except Exception, e:
+ print "nss_ldap is not able to use DNS discovery!"
+ print "Changing configuration to use hardcoded server name: " +cli_server
+
+ opts = [{'name':'uri', 'type':'option', 'action':'set', 'value':'ldap://'+cli_server},
+ {'name':'empty', 'type':'empty'}]
+ try:
+ ldapconf.changeConf("/etc/ldap.conf", opts)
+ except Exception, e:
+ print "Adding hardcoded server name to /etc/ldap.conf failed: " + str(e)
+ return 1
+
+ #Modify pam to add pam_krb5
+ run(["/usr/sbin/authconfig", "--enablekrb5", "--update"])
+ print "Kerberos 5 enabled"
+
+ if options.conf_ntp and not options.on_master:
+ if options.ntp_server:
+ ntp_server = options.ntp_server
+ else:
+ ntp_server = cli_server
+ ipaclient.ntpconf.config_ntp(ntp_server, fstore)
+ print "NTP enabled"
+
+ #Activate Name Server Caching Daemon
+ try:
+ run(["/sbin/service", "nscd", "restart"])
+ except:
+ print "Failed to start the NSCD daemon"
+ print "Caching of users/groups will not be available"
+ pass
+
+ try:
+ run(["/sbin/chkconfig", "nscd", "on"])
+ except:
+ print "Failed to configure automatic startup of the NSCD daemon"
+ print "Caching of users/groups will not be available after reboot"
+ pass
+
+ print "Client configuration complete."
+
+ return 0
+
+try:
+ if __name__ == "__main__":
+ sys.exit(main())
+except SystemExit, e:
+ sys.exit(e)
+except KeyboardInterrupt:
+ sys.exit(1)
diff --git a/ipa-client/ipaclient/Makefile.am b/ipa-client/ipaclient/Makefile.am
new file mode 100644
index 00000000..1d7df526
--- /dev/null
+++ b/ipa-client/ipaclient/Makefile.am
@@ -0,0 +1,16 @@
+NULL =
+
+appdir = $(pythondir)/ipaclient
+app_PYTHON = \
+ __init__.py \
+ ipachangeconf.py \
+ ipadiscovery.py \
+ ntpconf.py \
+ $(NULL)
+
+EXTRA_DIST = \
+ $(NULL)
+
+MAINTAINERCLEANFILES = \
+ *~ \
+ Makefile.in
diff --git a/ipa-client/ipaclient/__init__.py b/ipa-client/ipaclient/__init__.py
new file mode 100644
index 00000000..3eabc0f3
--- /dev/null
+++ b/ipa-client/ipaclient/__init__.py
@@ -0,0 +1,21 @@
+# Authors: Simo Sorce <ssorce@redhat.com>
+#
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 or later
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+__all__ = ["ipadiscovery", "ipachangeconf"]
+
diff --git a/ipa-client/ipaclient/ipachangeconf.py b/ipa-client/ipaclient/ipachangeconf.py
new file mode 100644
index 00000000..34c08d10
--- /dev/null
+++ b/ipa-client/ipaclient/ipachangeconf.py
@@ -0,0 +1,459 @@
+#
+# ipachangeconf - configuration file manipulation classes and functions
+# partially based on authconfig code
+# Copyright (c) 1999-2007 Red Hat, Inc.
+# Author: Simo Sorce <ssorce@redhat.com>
+#
+# This is free software; you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by
+# the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+#
+
+import fcntl
+import os
+import string
+import time
+import shutil
+
+def openLocked(filename, perms):
+ fd = -1
+ try:
+ fd = os.open(filename, os.O_RDWR | os.O_CREAT, perms)
+
+ fcntl.lockf(fd, fcntl.LOCK_EX)
+ except OSError, (errno, strerr):
+ if fd != -1:
+ try:
+ os.close(fd)
+ except OSError:
+ pass
+ raise IOError(errno, strerr)
+ return os.fdopen(fd, "r+")
+
+
+ #TODO: add subsection as a concept
+ # (ex. REALM.NAME = { foo = x bar = y } )
+ #TODO: put section delimiters as separating element of the list
+ # so that we can process multiple sections in one go
+ #TODO: add a comment all but provided options as a section option
+class IPAChangeConf:
+
+ def __init__(self, name):
+ self.progname = name
+ self.indent = ("","","")
+ self.assign = (" = ","=")
+ self.dassign = self.assign[0]
+ self.comment = ("#",)
+ self.dcomment = self.comment[0]
+ self.eol = ("\n",)
+ self.deol = self.eol[0]
+ self.sectnamdel = ("[","]")
+ self.subsectdel = ("{","}")
+
+ def setProgName(self, name):
+ self.progname = name
+
+ def setIndent(self, indent):
+ if type(indent) is tuple:
+ self.indent = indent
+ elif type(indent) is str:
+ self.indent = (indent, )
+ else:
+ raise ValueError, 'Indent must be a list of strings'
+
+ def setOptionAssignment(self, assign):
+ if type(assign) is tuple:
+ self.assign = assign
+ else:
+ self.assign = (assign, )
+ self.dassign = self.assign[0]
+
+ def setCommentPrefix(self, comment):
+ if type(comment) is tuple:
+ self.comment = comment
+ else:
+ self.comment = (comment, )
+ self.dcomment = self.comment[0]
+
+ def setEndLine(self, eol):
+ if type(eol) is tuple:
+ self.eol = eol
+ else:
+ self.eol = (eol, )
+ self.deol = self.eol[0]
+
+ def setSectionNameDelimiters(self, delims):
+ self.sectnamdel = delims
+
+ def setSubSectionDelimiters(self, delims):
+ self.subsectdel = delims
+
+ def matchComment(self, line):
+ for v in self.comment:
+ if line.lstrip().startswith(v):
+ return line.lstrip()[len(v):]
+ return False
+
+ def matchEmpty(self, line):
+ if line.strip() == "":
+ return True
+ return False
+
+ def matchSection(self, line):
+ cl = "".join(line.strip().split()).lower()
+ if len(self.sectnamdel) != 2:
+ return False
+ if not cl.startswith(self.sectnamdel[0]):
+ return False
+ if not cl.endswith(self.sectnamdel[1]):
+ return False
+ return cl[len(self.sectnamdel[0]):-len(self.sectnamdel[1])]
+
+ def matchSubSection(self, line):
+ if self.matchComment(line):
+ return False
+
+ parts = line.split(self.dassign, 1)
+ if len(parts) < 2:
+ return False
+
+ if parts[1].strip() == self.subsectdel[0]:
+ return parts[0].strip()
+
+ return False
+
+ def matchSubSectionEnd(self, line):
+ if self.matchComment(line):
+ return False
+
+ if line.strip() == self.subsectdel[1]:
+ return True
+
+ return False
+
+ def getSectionLine(self, section):
+ if len(self.sectnamdel) != 2:
+ return section
+ return self.sectnamdel[0]+section+self.sectnamdel[1]+self.deol
+
+ def dump(self, options, level=0):
+ output = ""
+ if level >= len(self.indent):
+ level = len(self.indent)-1
+
+ for o in options:
+ if o['type'] == "section":
+ output += self.sectnamdel[0]+o['name']+self.sectnamdel[1]+self.deol
+ output += self.dump(o['value'], level+1)
+ continue
+ if o['type'] == "subsection":
+ output += self.indent[level]+o['name']+self.dassign+self.subsectdel[0]+self.deol
+ output += self.dump(o['value'], level+1)
+ output += self.indent[level]+self.subsectdel[1]+self.deol
+ continue
+ if o['type'] == "option":
+ output += self.indent[level]+o['name']+self.dassign+o['value']+self.deol
+ continue
+ if o['type'] == "comment":
+ output += self.dcomment+o['value']+self.deol
+ continue
+ if o['type'] == "empty":
+ output += self.deol
+ continue
+ raise SyntaxError, 'Unknown type: ['+o['type']+']'
+
+ return output
+
+ def parseLine(self, line):
+
+ if self.matchEmpty(line):
+ return {'name':'empty', 'type':'empty'}
+
+ value = self.matchComment(line)
+ if value:
+ return {'name':'comment', 'type':'comment', 'value':value.rstrip()}
+
+ parts = line.split(self.dassign, 1)
+ if len(parts) < 2:
+ raise SyntaxError, 'Syntax Error: Unknown line format'
+
+ return {'name':parts[0].strip(), 'type':'option', 'value':parts[1].rstrip()}
+
+ def findOpts(self, opts, type, name, exclude_sections=False):
+
+ num = 0
+ for o in opts:
+ if o['type'] == type and o['name'] == name:
+ return (num, o)
+ if exclude_sections and (o['type'] == "section" or o['type'] == "subsection"):
+ return (num, None)
+ num += 1
+ return (num, None)
+
+ def commentOpts(self, inopts, level = 0):
+
+ opts = []
+
+ if level >= len(self.indent):
+ level = len(self.indent)-1
+
+ for o in inopts:
+ if o['type'] == 'section':
+ no = self.commentOpts(o['value'], level+1)
+ val = self.dcomment+self.sectnamdel[0]+o['name']+self.sectnamdel[1]
+ opts.append({'name':'comment', 'type':'comment', 'value':val})
+ for n in no:
+ opts.append(n)
+ continue
+ if o['type'] == 'subsection':
+ no = self.commentOpts(o['value'], level+1)
+ val = self.indent[level]+o['name']+self.dassign+self.subsectdel[0]
+ opts.append({'name':'comment', 'type':'comment', 'value':val})
+ for n in no:
+ opts.append(n)
+ val = self.indent[level]+self.subsectdel[1]
+ opts.append({'name':'comment', 'type':'comment', 'value':val})
+ continue
+ if o['type'] == 'option':
+ val = self.indent[level]+o['name']+self.dassign+o['value']
+ opts.append({'name':'comment', 'type':'comment', 'value':val})
+ continue
+ if o['type'] == 'comment':
+ opts.append(o)
+ continue
+ if o['type'] == 'empty':
+ opts.append({'name':'comment', 'type':'comment', 'value':''})
+ continue
+ raise SyntaxError, 'Unknown type: ['+o['type']+']'
+
+ return opts
+
+ def mergeOld(self, oldopts, newopts):
+
+ opts = []
+
+ for o in oldopts:
+ if o['type'] == "section" or o['type'] == "subsection":
+ (num, no) = self.findOpts(newopts, o['type'], o['name'])
+ if not no:
+ opts.append(o)
+ continue
+ if no['action'] == "set":
+ mo = self.mergeOld(o['value'], no['value'])
+ opts.append({'name':o['name'], 'type':o['type'], 'value':mo})
+ continue
+ if no['action'] == "comment":
+ co = self.commentOpts(o['value'])
+ for c in co:
+ opts.append(c)
+ continue
+ if no['action'] == "remove":
+ continue
+ raise SyntaxError, 'Unknown action: ['+no['action']+']'
+
+ if o['type'] == "comment" or o['type'] == "empty":
+ opts.append(o)
+ continue
+
+ if o['type'] == "option":
+ (num, no) = self.findOpts(newopts, 'option', o['name'], True)
+ if not no:
+ opts.append(o)
+ continue
+ if no['action'] == 'comment' or no['action'] == 'remove':
+ if no['value'] != None and o['value'] != no['value']:
+ opts.append(o)
+ continue
+ if no['action'] == 'comment':
+ opts.append({'name':'comment', 'type':'comment',
+ 'value':self.dcomment+o['name']+self.dassign+o['value']})
+ continue
+ if no['action'] == 'set':
+ opts.append(no)
+ continue
+ raise SyntaxError, 'Unknown action: ['+o['action']+']'
+
+ raise SyntaxError, 'Unknown type: ['+o['type']+']'
+
+ return opts
+
+ def mergeNew(self, opts, newopts):
+
+ cline = 0
+
+ for no in newopts:
+
+ if no['type'] == "section" or no['type'] == "subsection":
+ (num, o) = self.findOpts(opts, no['type'], no['name'])
+ if not o:
+ if no['action'] == 'set':
+ opts.append(no)
+ continue
+ if no['action'] == "set":
+ self.mergeNew(o['value'], no['value'])
+ continue
+ cline = num+1
+ continue
+
+ if no['type'] == "option":
+ (num, o) = self.findOpts(opts, no['type'], no['name'], True)
+ if not o:
+ if no['action'] == 'set':
+ opts.append(no)
+ continue
+ cline = num+1
+ continue
+
+ if no['type'] == "comment" or no['type'] == "empty":
+ opts.insert(cline, no)
+ cline += 1
+ continue
+
+ raise SyntaxError, 'Unknown type: ['+no['type']+']'
+
+
+ def merge(self, oldopts, newopts):
+
+ #Use a two pass strategy
+ #First we create a new opts tree from oldopts removing/commenting
+ # the options as indicated by the contents of newopts
+ #Second we fill in the new opts tree with options as indicated
+ # in the newopts tree (this is becaus eentire (sub)sections may
+ # exist in the newopts that do not exist in oldopts)
+
+ opts = self.mergeOld(oldopts, newopts)
+ self.mergeNew(opts, newopts)
+ return opts
+
+ #TODO: Make parse() recursive?
+ def parse(self, f):
+
+ opts = []
+ sectopts = []
+ section = None
+ subsectopts = []
+ subsection = None
+ curopts = opts
+ fatheropts = opts
+
+ # Read in the old file.
+ for line in f:
+
+ # It's a section start.
+ value = self.matchSection(line)
+ if value:
+ if section is not None:
+ opts.append({'name':section, 'type':'section', 'value':sectopts})
+ sectopts = []
+ curopts = sectopts
+ fatheropts = sectopts
+ section = value
+ continue
+
+ # It's a subsection start.
+ value = self.matchSubSection(line)
+ if value:
+ if subsection is not None:
+ raise SyntaxError, 'nested subsections are not supported yet'
+ subsectopts = []
+ curopts = subsectopts
+ subsection = value
+ continue
+
+ value = self.matchSubSectionEnd(line)
+ if value:
+ if subsection is None:
+ raise SyntaxError, 'Unmatched end subsection terminator found'
+ fatheropts.append({'name':subsection, 'type':'subsection', 'value':subsectopts})
+ subsection = None
+ curopts = fatheropts
+ continue
+
+ # Copy anything else as is.
+ curopts.append(self.parseLine(line))
+
+ #Add last section if any
+ if len(sectopts) is not 0:
+ opts.append({'name':section, 'type':'section', 'value':sectopts})
+
+ return opts
+
+ # Write settings to configuration file
+ # file is a path
+ # options is a set of dictionaries in the form:
+ # [{'name': 'foo', 'value': 'bar', 'action': 'set/comment'}]
+ # section is a section name like 'global'
+ def changeConf(self, file, newopts):
+ autosection = False
+ savedsection = None
+ done = False
+ output = ""
+ f = None
+ try:
+ #Do not catch an unexisting file error, we want to fail in that case
+ shutil.copy2(file, file+".ipabkp")
+
+ f = openLocked(file, 0644)
+
+ oldopts = self.parse(f)
+
+ options = self.merge(oldopts, newopts)
+
+ output = self.dump(options)
+
+ # Write it out and close it.
+ f.seek(0)
+ f.truncate(0)
+ f.write(output)
+ finally:
+ try:
+ if f:
+ f.close()
+ except IOError:
+ pass
+ return True
+
+ # Write settings to new file, backup old
+ # file is a path
+ # options is a set of dictionaries in the form:
+ # [{'name': 'foo', 'value': 'bar', 'action': 'set/comment'}]
+ # section is a section name like 'global'
+ def newConf(self, file, options):
+ autosection = False
+ savedsection = None
+ done = False
+ output = ""
+ f = None
+ try:
+ try:
+ shutil.copy2(file, file+".ipabkp")
+ except IOError, err:
+ if err.errno == 2:
+ # The orign file did not exist
+ pass
+
+ f = openLocked(file, 0644)
+
+ # Trunkate
+ f.seek(0)
+ f.truncate(0)
+
+ output = self.dump(options)
+
+ f.write(output)
+ finally:
+ try:
+ if f:
+ f.close()
+ except IOError:
+ pass
+ return True
diff --git a/ipa-client/ipaclient/ipadiscovery.py b/ipa-client/ipaclient/ipadiscovery.py
new file mode 100644
index 00000000..2bd15192
--- /dev/null
+++ b/ipa-client/ipaclient/ipadiscovery.py
@@ -0,0 +1,248 @@
+# Authors: Simo Sorce <ssorce@redhat.com>
+#
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+import socket
+import logging
+import ipa.dnsclient
+import ldap
+from ldap import LDAPError
+
+class IPADiscovery:
+
+ def __init__(self):
+ self.realm = None
+ self.domain = None
+ self.server = None
+ self.basedn = None
+
+ def getServerName(self):
+ return self.server
+
+ def getDomainName(self):
+ return self.domain
+
+ def getRealmName(self):
+ return self.realm
+
+ def getBaseDN(self):
+ return self.basedn
+
+ def search(self, domain = "", server = ""):
+ hostname = ""
+ qname = ""
+ results = []
+ result = []
+ krbret = []
+ ldapret = []
+
+ if not server:
+
+ if not domain: #domain not provided do full DNS discovery
+
+ # get the local host name
+ hostname = socket.getfqdn()
+ if not hostname:
+ return -10 #bad host configuration
+
+ # first, check for an LDAP server for the local domain
+ p = hostname.find(".")
+ if p == -1: #no domain name
+ return -1
+ domain = hostname[p+1:]
+
+ while not self.server:
+ logging.debug("[ipadnssearchldap("+domain+")]")
+ self.server = self.ipadnssearchldap(domain)
+ if self.server:
+ self.domain = domain
+ else:
+ p = domain.find(".")
+ if p == -1: #no ldap server found and last component of the domain already tested
+ return -1
+ domain = domain[p+1:]
+ else:
+ logging.debug("[ipadnssearchldap]")
+ self.server = self.ipadnssearchldap(domain)
+ if self.server:
+ self.domain = domain
+ else:
+ return -2 #no ldap server found
+
+
+ #search for kerberos TODO: move this after ipacheckldap()
+ logging.debug("[ipadnssearchkrb]")
+ krbret = self.ipadnssearchkrb(self.domain)
+ if not krbret:
+ return -3 #no krb server found
+
+ self.realm = krbret[0]
+
+ else: #server forced on us, this means DNS doesn't work :/
+
+ self.domain = domain
+ self.server = server
+
+ logging.debug("[ipacheckldap]")
+ # check ldap now
+ ldapret = self.ipacheckldap(self.server, self.realm)
+
+ if not ldapret:
+ return -4 # not an IPA server (or broken config)
+
+ self.server = ldapret[0]
+ self.realm = ldapret[1]
+
+ return 0
+
+ def ipacheckldap(self, thost, trealm):
+
+ lret = []
+ lres = []
+ lattr = ""
+ linfo = ""
+ lrealms = []
+
+ i = 0
+
+ #now verify the server is really an IPA server
+ try:
+ logging.debug("Init ldap with: ldap://"+thost+":389")
+ lh = ldap.initialize("ldap://"+thost+":389")
+ lh.simple_bind_s("","")
+
+ logging.debug("Search rootdse")
+ lret = lh.search_s("", ldap.SCOPE_BASE, "(objectClass=*)")
+ for lattr in lret[0][1]:
+ if lattr.lower() == "namingcontexts":
+ self.basedn = lret[0][1][lattr][0]
+
+ logging.debug("Search for (info=*) in "+self.basedn+"(base)")
+ lret = lh.search_s(self.basedn, ldap.SCOPE_BASE, "(info=IPA*)")
+ if not lret:
+ return []
+ logging.debug("Found: "+str(lret))
+
+ for lattr in lret[0][1]:
+ if lattr.lower() == "info":
+ linfo = lret[0][1][lattr][0].lower()
+ break
+
+ if not linfo:
+ return []
+
+ #search and return known realms
+ logging.debug("Search for (objectClass=krbRealmContainer) in "+self.basedn+"(sub)")
+ lret = lh.search_s("cn=kerberos,"+self.basedn, ldap.SCOPE_SUBTREE, "(objectClass=krbRealmContainer)")
+ if not lret:
+ #something very wrong
+ return []
+ logging.debug("Found: "+str(lret))
+
+ for lres in lret:
+ for lattr in lres[1]:
+ if lattr.lower() == "cn":
+ lrealms.append(lres[1][lattr][0])
+
+
+ if trealm:
+ for r in lrealms:
+ if trealm == r:
+ return [thost, trealm]
+ # must match or something is very wrong
+ return []
+ else:
+ if len(lrealms) != 1:
+ #which one? we can't attach to a multi-realm server without DNS working
+ return []
+ else:
+ return [thost, lrealms[0]]
+
+ #we shouldn't get here
+ return []
+
+ except LDAPError, err:
+ #no good
+ try:
+ if type(err.message) == dict:
+ for (k, v) in err.message.iteritems():
+ logging.error("LDAP Error: %s" % v )
+ else:
+ logging.error("LDAP Error: "+err.message)
+ except AttributeError:
+ logging.error("LDAP Error: "+str(err))
+ return []
+
+
+ def ipadnssearchldap(self, tdomain):
+ servers = ""
+ rserver = ""
+
+ qname = "_ldap._tcp."+tdomain
+ # terminate the name
+ if not qname.endswith("."):
+ qname += "."
+ results = ipa.dnsclient.query(qname, ipa.dnsclient.DNS_C_IN, ipa.dnsclient.DNS_T_SRV)
+
+ for result in results:
+ if result.dns_type == ipa.dnsclient.DNS_T_SRV:
+ rserver = result.rdata.server.rstrip(".")
+ if result.rdata.port and result.rdata.port != 389:
+ rserver += ":" + str(result.rdata.port)
+ if servers:
+ servers += "," + rserver
+ else:
+ servers = rserver
+ break
+
+ return servers
+
+ def ipadnssearchkrb(self, tdomain):
+ realm = ""
+ kdc = ""
+ # now, check for a Kerberos realm the local host or domain is in
+ qname = "_kerberos." + tdomain
+ # terminate the name
+ if not qname.endswith("."):
+ qname += "."
+ results = ipa.dnsclient.query(qname, ipa.dnsclient.DNS_C_IN, ipa.dnsclient.DNS_T_TXT)
+
+ for result in results:
+ if result.dns_type == ipa.dnsclient.DNS_T_TXT:
+ realm = result.rdata.data
+ if realm:
+ break
+
+ if realm:
+ # now fetch server information for the realm
+ qname = "_kerberos._udp." + tdomain
+ # terminate the name
+ if not qname.endswith("."):
+ qname += "."
+ results = ipa.dnsclient.query(qname, ipa.dnsclient.DNS_C_IN, ipa.dnsclient.DNS_T_SRV)
+ for result in results:
+ if result.dns_type == ipa.dnsclient.DNS_T_SRV:
+ qname = result.rdata.server.rstrip(".")
+ if result.rdata.port and result.rdata.port != 88:
+ qname += ":" + str(result.rdata.port)
+ if kdc:
+ kdc += "," + qname
+ else:
+ kdc = qname
+
+ return [realm, kdc]
diff --git a/ipa-client/ipaclient/ntpconf.py b/ipa-client/ipaclient/ntpconf.py
new file mode 100644
index 00000000..14e720c2
--- /dev/null
+++ b/ipa-client/ipaclient/ntpconf.py
@@ -0,0 +1,111 @@
+# Authors: Karl MacMillan <kmacmillan@redhat.com>
+#
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+from ipa.ipautil import *
+import shutil
+
+ntp_conf = """# Permit time synchronization with our time source, but do not
+# permit the source to query or modify the service on this system.
+restrict default kod nomodify notrap nopeer noquery
+restrict -6 default kod nomodify notrap nopeer noquery
+
+# Permit all access over the loopback interface. This could
+# be tightened as well, but to do so would effect some of
+# the administrative functions.
+restrict 127.0.0.1
+restrict -6 ::1
+
+# Hosts on local network are less restricted.
+#restrict 192.168.1.0 mask 255.255.255.0 nomodify notrap
+
+# Use public servers from the pool.ntp.org project.
+# Please consider joining the pool (http://www.pool.ntp.org/join.html).
+server $SERVER
+
+#broadcast 192.168.1.255 key 42 # broadcast server
+#broadcastclient # broadcast client
+#broadcast 224.0.1.1 key 42 # multicast server
+#multicastclient 224.0.1.1 # multicast client
+#manycastserver 239.255.254.254 # manycast server
+#manycastclient 239.255.254.254 key 42 # manycast client
+
+# Undisciplined Local Clock. This is a fake driver intended for backup
+# and when no outside source of synchronized time is available.
+server 127.127.1.0 # local clock
+#fudge 127.127.1.0 stratum 10
+
+# Drift file. Put this in a directory which the daemon can write to.
+# No symbolic links allowed, either, since the daemon updates the file
+# by creating a temporary in the same directory and then rename()'ing
+# it to the file.
+driftfile /var/lib/ntp/drift
+
+# Key file containing the keys and key identifiers used when operating
+# with symmetric key cryptography.
+keys /etc/ntp/keys
+
+# Specify the key identifiers which are trusted.
+#trustedkey 4 8 42
+
+# Specify the key identifier to use with the ntpdc utility.
+#requestkey 8
+
+# Specify the key identifier to use with the ntpq utility.
+#controlkey 8
+"""
+
+ntp_sysconfig = """# Drop root to id 'ntp:ntp' by default.
+OPTIONS="-x -u ntp:ntp -p /var/run/ntpd.pid"
+
+# Set to 'yes' to sync hw clock after successful ntpdate
+SYNC_HWCLOCK=yes
+
+# Additional options for ntpdate
+NTPDATE_OPTIONS=""
+"""
+
+def config_ntp(server_fqdn, fstore = None):
+ sub_dict = { }
+ sub_dict["SERVER"] = server_fqdn
+
+ nc = template_str(ntp_conf, sub_dict)
+
+ if fstore:
+ fstore.backup_file("/etc/ntp.conf")
+ else:
+ shutil.copy("/etc/ntp.conf", "/etc/ntp.conf.ipasave")
+
+ fd = open("/etc/ntp.conf", "w")
+ fd.write(nc)
+ fd.close()
+
+ if fstore:
+ fstore.backup_file("/etc/sysconfig/ntpd")
+ else:
+ shutil.copy("/etc/sysconfig/ntpd", "/etc/sysconfig/ntpd.ipasave")
+
+ fd = open("/etc/sysconfig/ntpd", "w")
+ fd.write(ntp_sysconfig)
+ fd.close()
+
+ # Set the ntpd to start on boot
+ run(["/sbin/chkconfig", "ntpd", "on"])
+
+ # Restart ntpd
+ run(["/sbin/service", "ntpd", "restart"])
diff --git a/ipa-client/man/Makefile.am b/ipa-client/man/Makefile.am
new file mode 100644
index 00000000..7d0a3aa4
--- /dev/null
+++ b/ipa-client/man/Makefile.am
@@ -0,0 +1,16 @@
+# This file will be processed with automake-1.7 to create Makefile.in
+
+AUTOMAKE_OPTIONS = 1.7
+
+NULL =
+
+man_MANS = \
+ ipa-getkeytab.1 \
+ ipa-client-install.1
+
+install-data-hook:
+ @for i in $(man_MANS) ; do gzip -f $(DESTDIR)$(man1dir)/$$i ; done
+
+MAINTAINERCLEANFILES = \
+ Makefile.in \
+ $(NULL)
diff --git a/ipa-client/man/ipa-client-install.1 b/ipa-client/man/ipa-client-install.1
new file mode 100644
index 00000000..49595a06
--- /dev/null
+++ b/ipa-client/man/ipa-client-install.1
@@ -0,0 +1,55 @@
+.\" A man page for ipa-client-install
+.\" Copyright (C) 2008 Red Hat, Inc.
+.\"
+.\" This is free software; you can redistribute it and/or modify it under
+.\" the terms of the GNU Library General Public License as published by
+.\" the Free Software Foundation; version 2 only
+.\"
+.\" This program is distributed in the hope that it will be useful, but
+.\" WITHOUT ANY WARRANTY; without even the implied warranty of
+.\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+.\" General Public License for more details.
+.\"
+.\" You should have received a copy of the GNU Library General Public
+.\" License along with this program; if not, write to the Free Software
+.\" Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+.\"
+.\" Author: Rob Crittenden <rcritten@redhat.com>
+.\"
+.TH "ipa-client-install" "1" "Mar 14 2008" "freeipa" ""
+.SH "NAME"
+ipa\-client\-install \- Configure an IPA client
+.SH "SYNOPSIS"
+ipa\-client\-install [\fIOPTION\fR]...
+.SH "DESCRIPTION"
+Configures a client machine to use IPA for authentication and identity services.
+
+This configures PAM and NSS (Name Switching Service) to work with an IPA server over Kerberos and LDAP.
+.SH "OPTIONS"
+.TP
+\fB\-\-domain\fR=\fIDOMAIN\fR
+Set the domain name to DOMAIN
+.TP
+\fB\-\-server\fR=\fISERVER\fR
+Set the IPA server to connect to
+.TP
+\fB\-\-realm\fR=\fIREALM_NAME\fR
+Set the IPA realm name to REALM_NAME
+.TP
+\fB\-f\fR, \fB\-\-force\fR
+Force the settings even if errors occur
+.TP
+\fB\-d\fR, \fB\-\-debug\fR
+Print debugging information to stdout
+.TP
+\fB\-U\fR, \fB\-\-unattended\fR
+Unattended installation. The user will not be prompted.
+.TP
+\fB\-N\fR, \fB\-\-no\-ntp\fR
+Do not configure or enable NTP
+\fB\-\-on\-master\fB
+The client is being configured on an IPA server
+.SH "EXIT STATUS"
+0 if the installation was successful
+
+1 if an error occurred
diff --git a/ipa-client/man/ipa-getkeytab.1 b/ipa-client/man/ipa-getkeytab.1
new file mode 100644
index 00000000..93db094e
--- /dev/null
+++ b/ipa-client/man/ipa-getkeytab.1
@@ -0,0 +1,101 @@
+.\" A man page for ipa-getkeytab
+.\" Copyright (C) 2007 Red Hat, Inc.
+.\"
+.\" This is free software; you can redistribute it and/or modify it under
+.\" the terms of the GNU Library General Public License as published by
+.\" the Free Software Foundation; version 2 only
+.\"
+.\" This program is distributed in the hope that it will be useful, but
+.\" WITHOUT ANY WARRANTY; without even the implied warranty of
+.\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+.\" General Public License for more details.
+.\"
+.\" You should have received a copy of the GNU Library General Public
+.\" License along with this program; if not, write to the Free Software
+.\" Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+.\"
+.\" Author: Karl MacMillan <kmacmill@redhat.com>
+.\" Author: Simo Sorce <ssorce@redhat.com>
+.\"
+.TH "ipa-getkeytab" "1" "Oct 10 2007" "freeipa" ""
+.SH "NAME"
+ipa\-getkeytab \- Get a keytab for a kerberos principal
+.SH "SYNOPSIS"
+ipa\-getkeytab [ \fB\-s\fR ipaserver ] [ \fB\-p\fR principal\-name ] [ \fB\-k\fR keytab\-file ] [ \fB\-e\fR encryption\-types ] [ \fB\-q\fR ]
+
+.SH "DESCRIPTION"
+Retrieves a kerberos \fIkeytab\fR.
+
+Kerberos keytabs are used for services (like sshd) to
+perform kerberos authentication. A keytab is a file
+with one or more secrets (or keys) for a kerberos
+principal.
+
+A kerberos service principal is a kerberos identity
+that can be used for authentication. Service principals
+contain the name of the service, the hostname of the
+server, and the realm name. For example, the following
+is an example principal for an ldap server:
+
+ ldap/foo.example.com@EXAMPLE.COM
+
+When using ipa\-getkeytab the realm name is already
+provided, so the principal name is just the service
+name and hostname (ldap/foo.example.com from the
+example above).
+
+\fBWARNING:\fR retrieving the keytab resets the secret for the Kerberos principal.
+This renders all other keytabs for that principal invalid.
+.SH "OPTIONS"
+.TP
+\fB\-s ipaserver\fR
+The IPA server to retrieve the keytab from (FQDN).
+.TP
+\fB\-p principal\-name\fR
+The non\-realm part of the full principal name.
+.TP
+\fB\-k keytab\-file\fR
+The keytab file where to append the new key (will be
+created if it does not exist).
+.TP
+\fB\-e encryption\-types\fR
+The list of encryption types to use to generate keys.
+ipa\-getkeytab will use local client defaults if not provided.
+Valid values depend on the kerberos library version and configuration.
+Common values are:
+aes256\-cts
+aes128\-cts
+des3\-hmac\-sha1
+arcfour\-hmac
+des\-hmac\-sha1
+des\-cbc\-md5
+des\-cbc\-crc
+.TP
+\fB\-q\fR
+Quiet mode. Only errors are displayed.
+.TP
+\fB\-\-permitted\-enctypes\fR
+This options returns a description of the permitted encryption types, like this:
+Supported encryption types:
+AES\-256 CTS mode with 96\-bit SHA\-1 HMAC
+AES\-128 CTS mode with 96\-bit SHA\-1 HMAC
+Triple DES cbc mode with HMAC/sha1
+ArcFour with HMAC/md5
+DES cbc mode with CRC\-32
+DES cbc mode with RSA\-MD5
+DES cbc mode with RSA\-MD4
+.SH "EXAMPLES"
+Add and retrieve a keytab for the NFS service principal on
+the host foo.example.com and save it in the file /tmp/nfs.keytab and retrieve just the des\-cbc\-crc key.
+
+ # ipa\-getkeytab \-s ipaserver.example.com \-p nfs/foo.example.com \-k /tmp/nfs.keytab \-e des\-cbc\-crc
+
+Add and retrieve a keytab for the ldap service principal on
+the host foo.example.com and save it in the file /tmp/ldap.keytab.
+
+ # ipa\-getkeytab \-s ipaserver.example.com \-p ldap/foo.example.com \-k /tmp/ldap.keytab
+
+
+
+.SH "EXIT STATUS"
+The exit status is 0 on success, nonzero on error.
diff --git a/ipa-client/version.m4.in b/ipa-client/version.m4.in
new file mode 100644
index 00000000..5ddc8cea
--- /dev/null
+++ b/ipa-client/version.m4.in
@@ -0,0 +1 @@
+define([IPA_VERSION], [__VERSION__])
diff --git a/ipa-python/MANIFEST.in b/ipa-python/MANIFEST.in
new file mode 100644
index 00000000..e2cad6f2
--- /dev/null
+++ b/ipa-python/MANIFEST.in
@@ -0,0 +1,3 @@
+include *.conf
+include ipa-python.spec*
+
diff --git a/ipa-python/Makefile b/ipa-python/Makefile
new file mode 100644
index 00000000..4ac027e1
--- /dev/null
+++ b/ipa-python/Makefile
@@ -0,0 +1,28 @@
+PYTHONLIBDIR ?= $(shell python -c "from distutils.sysconfig import *; print get_python_lib()")
+PACKAGEDIR ?= $(DESTDIR)/$(PYTHONLIBDIR)/ipa
+CONFIGDIR ?= $(DESTDIR)/etc/ipa
+TESTS = $(wildcard test/*.py)
+
+all: ;
+
+install:
+ if [ "$(DESTDIR)" = "" ]; then \
+ python setup.py install; \
+ else \
+ python setup.py install --root $(DESTDIR); \
+ fi
+
+clean:
+ rm -f *~ *.pyc
+
+distclean: clean
+ rm -f setup.py ipa-python.spec version.py
+
+maintainer-clean: distclean
+ rm -rf build
+
+.PHONY: test
+test: $(subst .py,.tst,$(TESTS))
+
+%.tst: %.py
+ python $<
diff --git a/ipa-python/README b/ipa-python/README
new file mode 100644
index 00000000..99fcccc7
--- /dev/null
+++ b/ipa-python/README
@@ -0,0 +1,30 @@
+This is a set of libraries common to IPA clients and servers though mostly
+geared currently towards command-line tools.
+
+A brief overview:
+
+aci.py - a basic LDAP ACI parser for doing delegations.
+config.py - identify the IPA server domain and realm. It uses dnsclient to
+ try to detect this information first and will fall back to
+ /etc/ipa/ipa.conf if that fails.
+ipaadminutil.py - routines to help reduce the number of entries from a search
+dnsclient.py - find IPA information via DNS
+
+ipaclient.py - the main interface for any command-line tools. When initialized
+ if transport is True then it means the IPA server is on the
+ same machine so no need to use the XML-RPC interface.
+rpcclient.py - the XML-RPC client API. Callers should use ipaclient instead
+ of this directly.
+
+ipautil.py - helper functions
+
+radius_util.py - helper functions for Radius
+
+user.py
+group.py
+entity.py - entity is the main data type. User and Group extend this class
+ (but don't add anything currently).
+
+ipavalidate.py - basic data validation routines
+ipaerror.py - our own error types
+krbtransport.py - do Kerberos auth over HTTP/S
diff --git a/ipa-python/__init__.py b/ipa-python/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/ipa-python/__init__.py
diff --git a/ipa-python/aci.py b/ipa-python/aci.py
new file mode 100644
index 00000000..58a3b1d2
--- /dev/null
+++ b/ipa-python/aci.py
@@ -0,0 +1,166 @@
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+import re
+import urllib
+import ldap
+
+import ipa.ipautil
+
+class ACI:
+ """
+ Holds the basic data for an ACI entry, as stored in the cn=accounts
+ entry in LDAP. Has methods to parse an ACI string and export to an
+ ACI String.
+ """
+
+ def __init__(self,acistr=None):
+ self.name = ''
+ self.source_group = ''
+ self.dest_group = ''
+ self.attrs = []
+ self.orig_acistr = acistr
+ if acistr is not None:
+ self.parse_acistr(acistr)
+
+ def __getitem__(self,key):
+ """Fake getting attributes by key for sorting"""
+ if key == 0:
+ return self.name
+ if key == 1:
+ return self.source_group
+ if key == 2:
+ return self.dest_group
+ raise TypeError("Unknown key value %s" % key)
+
+ def export_to_string(self):
+ """Converts the ACI to a string suitable for an LDAP aci attribute."""
+ attrs_str = ' || '.join(self.attrs)
+
+ # dest_group and source_group are assumed to be pre-escaped.
+ # dn's aren't typed in, but searched for, and the search results
+ # will return escaped dns
+
+ acistr = ('(targetattr="%s")' +
+ '(targetfilter="(memberOf=%s)")' +
+ '(version 3.0;' +
+ 'acl "%s";' +
+ 'allow (write) ' +
+ 'groupdn="ldap:///%s";)') % (attrs_str,
+ self.dest_group,
+ self.name,
+ urllib.quote(self.source_group, "/=, "))
+ return acistr
+
+ def to_dict(self):
+ result = ipa.ipautil.CIDict()
+ result['name'] = self.name
+ result['source_group'] = self.source_group
+ result['dest_group'] = self.dest_group
+ result['attrs'] = self.attrs
+ result['orig_acistr'] = self.orig_acistr
+
+ return result
+
+ def _match(self, prefix, inputstr):
+ """Returns inputstr with prefix removed, or else raises a
+ SyntaxError."""
+ if inputstr.startswith(prefix):
+ return inputstr[len(prefix):]
+ else:
+ raise SyntaxError, "'%s' not found at '%s'" % (prefix, inputstr)
+
+ def _match_str(self, inputstr):
+ """Tries to extract a " delimited string from the front of inputstr.
+ Returns (string, inputstr) where:
+ - string is the extracted string (minus the enclosing " chars)
+ - inputstr is the parameter with the string removed.
+ Raises SyntaxError is a string is not found."""
+ if not inputstr.startswith('"'):
+ raise SyntaxError, "string not found at '%s'" % inputstr
+
+ found = False
+ start_index = 1
+ final_index = 1
+ while not found and (final_index < len(inputstr)):
+ if inputstr[final_index] == '\\':
+ final_index += 2
+ elif inputstr[final_index] == '"':
+ found = True
+ else:
+ final_index += 1
+ if not found:
+ raise SyntaxError, "string not found at '%s'" % inputstr
+
+ match = inputstr[start_index:final_index]
+ inputstr = inputstr[final_index + 1:]
+
+ return(match, inputstr)
+
+ def parse_acistr(self, acistr):
+ """Parses the acistr. If the string isn't recognized, a SyntaxError
+ is raised."""
+ self.orig_acistr = acistr
+
+ acistr = self._match('(targetattr=', acistr)
+ (attrstr, acistr) = self._match_str(acistr)
+ self.attrs = attrstr.split(' || ')
+
+ acistr = self._match(')(targetfilter=', acistr)
+ (target_dn_str, acistr) = self._match_str(acistr)
+ target_dn_str = self._match('(memberOf=', target_dn_str)
+ if target_dn_str.endswith(')'):
+ self.dest_group = target_dn_str[:-1]
+ else:
+ raise SyntaxError, "illegal dest_group at '%s'" % target_dn_str
+
+ acistr = self._match(')(version 3.0;acl ', acistr)
+ (name_str, acistr) = self._match_str(acistr)
+ self.name = name_str
+
+ acistr = self._match(';allow (write) groupdn=', acistr)
+ (src_dn_str, acistr) = self._match_str(acistr)
+ src_dn_str = self._match('ldap:///', src_dn_str)
+ self.source_group = urllib.unquote(src_dn_str)
+
+ acistr = self._match(';)', acistr)
+ if len(acistr) > 0:
+ raise SyntaxError, "unexpected aci suffix at '%s'" % acistr
+
+def extract_group_cns(aci_list, client):
+ """Extracts all the cn's from a list of aci's and returns them as a hash
+ from group_dn to group_cn.
+
+ It first tries to cheat by looking at the first rdn for the
+ group dn. If that's not cn for some reason, it looks up the group."""
+ group_dn_to_cn = {}
+ for aci in aci_list:
+ for dn in (aci.source_group, aci.dest_group):
+ if not group_dn_to_cn.has_key(dn):
+ rdn_list = ldap.explode_dn(dn, 0)
+ first_rdn = rdn_list[0]
+ (type,value) = first_rdn.split('=')
+ if type == "cn":
+ group_dn_to_cn[dn] = value
+ else:
+ try:
+ group = client.get_entry_by_dn(dn, ['cn'])
+ group_dn_to_cn[dn] = group.getValue('cn')
+ except ipaerror.IPAError, e:
+ group_dn_to_cn[dn] = 'unknown'
+
+ return group_dn_to_cn
diff --git a/ipa-python/config.py b/ipa-python/config.py
new file mode 100644
index 00000000..8755f628
--- /dev/null
+++ b/ipa-python/config.py
@@ -0,0 +1,183 @@
+# Authors: Karl MacMillan <kmacmill@redhat.com>
+#
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+import ConfigParser
+from optparse import OptionParser, IndentedHelpFormatter
+
+import krbV
+import socket
+import ipa.dnsclient
+import re
+
+class IPAConfigError(Exception):
+ def __init__(self, msg=''):
+ self.msg = msg
+ Exception.__init__(self, msg)
+
+ def __repr__(self):
+ return self.msg
+
+ __str__ = __repr__
+
+class IPAFormatter(IndentedHelpFormatter):
+ """Our own optparse formatter that indents multiple lined usage string."""
+ def format_usage(self, usage):
+ usage_string = "Usage:"
+ spacing = " " * len(usage_string)
+ lines = usage.split("\n")
+ ret = "%s %s\n" % (usage_string, lines[0])
+ for line in lines[1:]:
+ ret += "%s %s\n" % (spacing, line)
+ return ret
+
+def verify_args(parser, args, needed_args = None):
+ """Verify that we have all positional arguments we need, if not, exit."""
+ if needed_args:
+ needed_list = needed_args.split(" ")
+ else:
+ needed_list = []
+ len_need = len(needed_list)
+ len_have = len(args)
+ if len_have > len_need:
+ parser.error("too many arguments")
+ elif len_have < len_need:
+ parser.error("no %s specified" % needed_list[len_have])
+
+class IPAConfig:
+ def __init__(self):
+ self.default_realm = None
+ self.default_server = []
+ self.default_domain = None
+
+ def get_realm(self):
+ if self.default_realm:
+ return self.default_realm
+ else:
+ raise IPAConfigError("no default realm")
+
+ def get_server(self):
+ if len(self.default_server):
+ return self.default_server
+ else:
+ raise IPAConfigError("no default server")
+
+ def get_domain(self):
+ if self.default_domain:
+ return self.default_domain
+ else:
+ raise IPAConfigError("no default domain")
+
+# Global library config
+config = IPAConfig()
+
+def __parse_config(discover_server = True):
+ p = ConfigParser.SafeConfigParser()
+ p.read("/etc/ipa/ipa.conf")
+
+ try:
+ if not config.default_realm:
+ config.default_realm = p.get("defaults", "realm")
+ except:
+ pass
+ if discover_server:
+ try:
+ s = p.get("defaults", "server")
+ config.default_server.extend(re.sub("\s+", "", s).split(','))
+ except:
+ pass
+ try:
+ if not config.default_domain:
+ config.default_domain = p.get("defaults", "domain")
+ except:
+ pass
+
+def __discover_config(discover_server = True):
+ rl = 0
+ try:
+ if not config.default_realm:
+ krbctx = krbV.default_context()
+ config.default_realm = krbctx.default_realm
+ if not config.default_realm:
+ return False
+
+ if not config.default_domain:
+ #try once with REALM -> domain
+ dom_name = config.default_realm.lower()
+ name = "_ldap._tcp."+dom_name+"."
+ rs = ipa.dnsclient.query(name, ipa.dnsclient.DNS_C_IN, ipa.dnsclient.DNS_T_SRV)
+ rl = len(rs)
+ if rl == 0:
+ #try cycling on domain components of FQDN
+ dom_name = socket.getfqdn()
+ while rl == 0:
+ tok = dom_name.find(".")
+ if tok == -1:
+ return False
+ dom_name = dom_name[tok+1:]
+ name = "_ldap._tcp." + dom_name + "."
+ rs = ipa.dnsclient.query(name, ipa.dnsclient.DNS_C_IN, ipa.dnsclient.DNS_T_SRV)
+ rl = len(rs)
+
+ config.default_domain = dom_name
+
+ if discover_server:
+ if rl == 0:
+ name = "_ldap._tcp."+config.default_domain+"."
+ rs = ipa.dnsclient.query(name, ipa.dnsclient.DNS_C_IN, ipa.dnsclient.DNS_T_SRV)
+
+ for r in rs:
+ if r.dns_type == ipa.dnsclient.DNS_T_SRV:
+ rsrv = r.rdata.server.rstrip(".")
+ config.default_server.append(rsrv)
+
+ except:
+ pass
+
+def add_standard_options(parser):
+ parser.add_option("--realm", dest="realm", help="Override default IPA realm")
+ parser.add_option("--server", dest="server", help="Override default IPA server")
+ parser.add_option("--domain", dest="domain", help="Override default IPA DNS domain")
+
+def init_config(options=None):
+ if options:
+ config.default_realm = options.realm
+ config.default_domain = options.domain
+ if options.server:
+ config.default_server.extend(options.server.split(","))
+
+ if len(config.default_server):
+ discover_server = False
+ else:
+ discover_server = True
+ __parse_config(discover_server)
+ __discover_config(discover_server)
+
+ # make sure the server list only contains unique items
+ new_server = []
+ for server in config.default_server:
+ if server not in new_server:
+ new_server.append(server)
+ config.default_server = new_server
+
+ if not config.default_realm:
+ raise IPAConfigError("IPA realm not found in DNS, in the config file (/etc/ipa/ipa.conf) or on the command line.")
+ if not config.default_server:
+ raise IPAConfigError("IPA server not found in DNS, in the config file (/etc/ipa/ipa.conf) or on the command line.")
+ if not config.default_domain:
+ raise IPAConfigError("IPA domain not found in the config file (/etc/ipa/ipa.conf) or on the command line.")
diff --git a/ipa-python/dnsclient.py b/ipa-python/dnsclient.py
new file mode 100644
index 00000000..58d93d85
--- /dev/null
+++ b/ipa-python/dnsclient.py
@@ -0,0 +1,443 @@
+#
+# Copyright 2001, 2005 Red Hat, Inc.
+#
+# This is free software; you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 only
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+#
+
+import struct
+import socket
+import sys
+
+import acutil
+
+DNS_C_IN = 1
+DNS_C_CS = 2
+DNS_C_CHAOS = 3
+DNS_C_HS = 4
+DNS_C_ANY = 255
+
+DNS_T_A = 1
+DNS_T_NS = 2
+DNS_T_CNAME = 5
+DNS_T_SOA = 6
+DNS_T_NULL = 10
+DNS_T_WKS = 11
+DNS_T_PTR = 12
+DNS_T_HINFO = 13
+DNS_T_MX = 15
+DNS_T_TXT = 16
+DNS_T_SRV = 33
+DNS_T_ANY = 255
+
+DEBUG_DNSCLIENT = False
+
+class DNSQueryHeader:
+ FORMAT = "!HBBHHHH"
+ def __init__(self):
+ self.dns_id = 0
+ self.dns_rd = 0
+ self.dns_tc = 0
+ self.dns_aa = 0
+ self.dns_opcode = 0
+ self.dns_qr = 0
+ self.dns_rcode = 0
+ self.dns_z = 0
+ self.dns_ra = 0
+ self.dns_qdcount = 0
+ self.dns_ancount = 0
+ self.dns_nscount = 0
+ self.dns_arcount = 0
+
+ def pack(self):
+ return struct.pack(DNSQueryHeader.FORMAT,
+ self.dns_id,
+ (self.dns_rd & 1) |
+ (self.dns_tc & 1) << 1 |
+ (self.dns_aa & 1) << 2 |
+ (self.dns_opcode & 15) << 3 |
+ (self.dns_qr & 1) << 7,
+ (self.dns_rcode & 15) |
+ (self.dns_z & 7) << 4 |
+ (self.dns_ra & 1) << 7,
+ self.dns_qdcount,
+ self.dns_ancount,
+ self.dns_nscount,
+ self.dns_arcount)
+
+ def unpack(self, data):
+ (self.dns_id, byte1, byte2, self.dns_qdcount, self.dns_ancount,
+ self.dns_nscount, self.dns_arcount) = struct.unpack(DNSQueryHeader.FORMAT, data[0:self.size()])
+ self.dns_rd = byte1 & 1
+ self.dns_tc = (byte1 >> 1) & 1
+ self.dns_aa = (byte1 >> 2) & 1
+ self.dns_opcode = (byte1 >> 3) & 15
+ self.dns_qr = (byte1 >> 7) & 1
+ self.dns_rcode = byte2 & 15
+ self.dns_z = (byte2 >> 4) & 7
+ self.dns_ra = (byte1 >> 7) & 1
+
+ def size(self):
+ return struct.calcsize(DNSQueryHeader.FORMAT)
+
+def unpackQueryHeader(data):
+ header = DNSQueryHeader()
+ header.unpack(data)
+ return header
+
+class DNSResult:
+ FORMAT = "!HHIH"
+ QFORMAT = "!HH"
+ def __init__(self):
+ self.dns_name = ""
+ self.dns_type = 0
+ self.dns_class = 0
+ self.dns_ttl = 0
+ self.dns_rlength = 0
+ self.rdata = None
+
+ def unpack(self, data):
+ (self.dns_type, self.dns_class, self.dns_ttl,
+ self.dns_rlength) = struct.unpack(DNSResult.FORMAT, data[0:self.size()])
+
+ def qunpack(self, data):
+ (self.dns_type, self.dns_class) = struct.unpack(DNSResult.QFORMAT, data[0:self.qsize()])
+
+ def size(self):
+ return struct.calcsize(DNSResult.FORMAT)
+
+ def qsize(self):
+ return struct.calcsize(DNSResult.QFORMAT)
+
+class DNSRData:
+ def __init__(self):
+ pass
+
+#typedef struct dns_rr_a {
+# u_int32_t address;
+#} dns_rr_a_t;
+#
+#typedef struct dns_rr_cname {
+# const char *cname;
+#} dns_rr_cname_t;
+#
+#typedef struct dns_rr_hinfo {
+# const char *cpu, *os;
+#} dns_rr_hinfo_t;
+#
+#typedef struct dns_rr_mx {
+# u_int16_t preference;
+# const char *exchange;
+#} dns_rr_mx_t;
+#
+#typedef struct dns_rr_null {
+# unsigned const char *data;
+#} dns_rr_null_t;
+#
+#typedef struct dns_rr_ns {
+# const char *nsdname;
+#} dns_rr_ns_t;
+#
+#typedef struct dns_rr_ptr {
+# const char *ptrdname;
+#} dns_rr_ptr_t;
+#
+#typedef struct dns_rr_soa {
+# const char *mname;
+# const char *rname;
+# u_int32_t serial;
+# int32_t refresh;
+# int32_t retry;
+# int32_t expire;
+# int32_t minimum;
+#} dns_rr_soa_t;
+#
+#typedef struct dns_rr_txt {
+# const char *data;
+#} dns_rr_txt_t;
+#
+#typedef struct dns_rr_srv {
+# const char *server;
+# u_int16_t priority;
+# u_int16_t weight;
+# u_int16_t port;
+#} dns_rr_srv_t;
+
+def dnsNameToLabel(name):
+ out = ""
+ name = name.split(".")
+ for part in name:
+ out += chr(len(part)) + part
+ return out
+
+def dnsFormatQuery(query, qclass, qtype):
+ header = DNSQueryHeader()
+
+ header.dns_id = 0 # FIXME: id = 0
+ header.dns_rd = 1 # don't know why the original code didn't request recursion for non SOA requests
+ header.dns_qr = 0 # query
+ header.dns_opcode = 0 # standard query
+ header.dns_qdcount = 1 # single query
+
+ qlabel = dnsNameToLabel(query)
+ if not qlabel:
+ return ""
+
+ out = header.pack() + qlabel
+ out += chr(qtype >> 8)
+ out += chr(qtype & 0xff)
+ out += chr(qclass >> 8)
+ out += chr(qclass & 0xff)
+
+ return out
+
+def dnsParseLabel(label, base):
+ # returns (output, rest)
+ if not label:
+ return ("", None)
+
+ update = 1
+ rest = label
+ output = ""
+ skip = 0
+
+ try:
+ while ord(rest[0]):
+ if ord(rest[0]) & 0xc0:
+ rest = base[((ord(rest[0]) & 0x3f) << 8) + ord(rest[1]):]
+ if update:
+ skip += 2
+ update = 0
+ continue
+ output += rest[1:ord(rest[0]) + 1] + "."
+ if update:
+ skip += ord(rest[0]) + 1
+ rest = rest[ord(rest[0]) + 1:]
+ except IndexError:
+ return ("", None)
+ return (label[skip+update:], output)
+
+def dnsParseA(data, base):
+ rdata = DNSRData()
+ if len(data) < 4:
+ rdata.address = 0
+ return None
+
+ rdata.address = (ord(data[0])<<24) | (ord(data[1])<<16) | (ord(data[2])<<8) | (ord(data[3])<<0)
+
+ if DEBUG_DNSCLIENT:
+ print "A = %d.%d.%d.%d." % (ord(data[0]), ord(data[1]), ord(data[2]), ord(data[3]))
+ return rdata
+
+def dnsParseText(data):
+ if len(data) < 1:
+ return ("", None)
+ tlen = ord(data[0])
+ if len(data) < tlen + 1:
+ return ("", None)
+ return (data[tlen+1:], data[1:tlen+1])
+
+def dnsParseNS(data, base):
+ rdata = DNSRData()
+ (rest, rdata.nsdname) = dnsParseLabel(data, base)
+ if DEBUG_DNSCLIENT:
+ print "NS DNAME = \"%s\"." % (rdata.nsdname)
+ return rdata
+
+def dnsParseCNAME(data, base):
+ rdata = DNSRData()
+ (rest, rdata.cname) = dnsParseLabel(data, base)
+ if DEBUG_DNSCLIENT:
+ print "CNAME = \"%s\"." % (rdata.cname)
+ return rdata
+
+def dnsParseSOA(data, base):
+ rdata = DNSRData()
+ format = "!IIIII"
+
+ (rest, rdata.mname) = dnsParseLabel(data, base)
+ if rdata.mname is None:
+ return None
+ (rest, rdata.rname) = dnsParseLabel(rest, base)
+ if rdata.rname is None:
+ return None
+ if len(rest) < struct.calcsize(format):
+ return None
+
+ (rdata.serial, rdata.refresh, rdata.retry, rdata.expire,
+ rdata.minimum) = struct.unpack(format, rest[:struct.calcsize(format)])
+
+ if DEBUG_DNSCLIENT:
+ print "SOA(mname) = \"%s\"." % rdata.mname
+ print "SOA(rname) = \"%s\"." % rdata.rname
+ print "SOA(serial) = %d." % rdata.serial
+ print "SOA(refresh) = %d." % rdata.refresh
+ print "SOA(retry) = %d." % rdata.retry
+ print "SOA(expire) = %d." % rdata.expire
+ print "SOA(minimum) = %d." % rdata.minimum
+ return rdata
+
+def dnsParseNULL(data, base):
+ # um, yeah
+ return None
+
+def dnsParseWKS(data, base):
+ return None
+
+def dnsParseHINFO(data, base):
+ rdata = DNSRData()
+ (rest, rdata.cpu) = dnsParseText(data)
+ if rest:
+ (rest, rdata.os) = dnsParseText(rest)
+ if DEBUG_DNSCLIENT:
+ print "HINFO(cpu) = \"%s\"." % rdata.cpu
+ print "HINFO(os) = \"%s\"." % rdata.os
+ return rdata
+
+def dnsParseMX(data, base):
+ rdata = DNSRData()
+ if len(data) < 2:
+ return None
+ rdata.preference = (ord(data[0]) << 8) | ord(data[1])
+ (rest, rdata.exchange) = dnsParseLabel(data[2:], base)
+ if DEBUG_DNSCLIENT:
+ print "MX(exchanger) = \"%s\"." % rdata.exchange
+ print "MX(preference) = %d." % rdata.preference
+ return rdata
+
+def dnsParseTXT(data, base):
+ rdata = DNSRData()
+ (rest, rdata.data) = dnsParseText(data)
+ if DEBUG_DNSCLIENT:
+ print "TXT = \"%s\"." % rdata.data
+ return rdata
+
+def dnsParsePTR(data, base):
+ rdata = DNSRData()
+ (rest, rdata.ptrdname) = dnsParseLabel(data, base)
+ if DEBUG_DNSCLIENT:
+ print "PTR = \"%s\"." % rdata.ptrdname
+ return rdata
+
+def dnsParseSRV(data, base):
+ rdata = DNSRData()
+ format = "!HHH"
+ flen = struct.calcsize(format)
+ if len(data) < flen:
+ return None
+
+ (rdata.priority, rdata.weight, rdata.port) = struct.unpack(format, data[:flen])
+ (rest, rdata.server) = dnsParseLabel(data[flen:], base)
+ if DEBUG_DNSCLIENT:
+ print "SRV(server) = \"%s\"." % rdata.server
+ print "SRV(weight) = %d." % rdata.weight
+ print "SRV(priority) = %d." % rdata.priority
+ print "SRV(port) = %d." % rdata.port
+ return rdata
+
+def dnsParseResults(results):
+ try:
+ header = unpackQueryHeader(results)
+ except struct.error:
+ return []
+
+ if header.dns_qr != 1: # should be a response
+ return []
+
+ if header.dns_rcode != 0: # should be no error
+ return []
+
+ rest = results[header.size():]
+
+ rrlist = []
+
+ for i in xrange(header.dns_qdcount):
+ if not rest:
+ return []
+
+ qq = DNSResult()
+
+ (rest, label) = dnsParseLabel(rest, results)
+ if label is None:
+ return []
+
+ if len(rest) < qq.qsize():
+ return []
+
+ qq.qunpack(rest)
+
+ rest = rest[qq.qsize():]
+
+ if DEBUG_DNSCLIENT:
+ print "Queried for '%s', class = %d, type = %d." % (label,
+ qq.dns_class, qq.dns_type)
+
+ for i in xrange(header.dns_ancount + header.dns_nscount + header.dns_arcount):
+ (rest, label) = dnsParseLabel(rest, results)
+ if label is None:
+ return []
+
+ rr = DNSResult()
+
+ rr.dns_name = label
+
+ if len(rest) < rr.size():
+ return []
+
+ rr.unpack(rest)
+
+ rest = rest[rr.size():]
+
+ if DEBUG_DNSCLIENT:
+ print "Answer %d for '%s', class = %d, type = %d, ttl = %d." % (i,
+ rr.dns_name, rr.dns_class, rr.dns_type,
+ rr.dns_ttl)
+
+ if len(rest) < rr.dns_rlength:
+ if DEBUG_DNSCLIENT:
+ print "Answer too short."
+ return []
+
+ fmap = { DNS_T_A: dnsParseA, DNS_T_NS: dnsParseNS,
+ DNS_T_CNAME: dnsParseCNAME, DNS_T_SOA: dnsParseSOA,
+ DNS_T_NULL: dnsParseNULL, DNS_T_WKS: dnsParseWKS,
+ DNS_T_PTR: dnsParsePTR, DNS_T_HINFO: dnsParseHINFO,
+ DNS_T_MX: dnsParseMX, DNS_T_TXT: dnsParseTXT,
+ DNS_T_SRV: dnsParseSRV}
+
+ if not rr.dns_type in fmap:
+ if DEBUG_DNSCLIENT:
+ print "Don't know how to parse RR type %d!" % rr.dns_type
+ else:
+ rr.rdata = fmap[rr.dns_type](rest[:rr.dns_rlength], results)
+
+ rest = rest[rr.dns_rlength:]
+ rrlist += [rr]
+
+ return rrlist
+
+def query(query, qclass, qtype):
+ qdata = dnsFormatQuery(query, qclass, qtype)
+ if not qdata:
+ return []
+ answer = acutil.res_send(qdata)
+ if not answer:
+ return []
+ return dnsParseResults(answer)
+
+if __name__ == '__main__':
+ DEBUG_DNSCLIENT = True
+ print "Sending query."
+ rr = query(len(sys.argv) > 1 and sys.argv[1] or "devserv.devel.redhat.com.",
+ DNS_C_IN, DNS_T_ANY)
+ sys.exit(0)
diff --git a/ipa-python/entity.py b/ipa-python/entity.py
new file mode 100644
index 00000000..64db350b
--- /dev/null
+++ b/ipa-python/entity.py
@@ -0,0 +1,202 @@
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+import ldap
+import ldif
+import re
+import cStringIO
+import copy
+
+import ipa.ipautil
+
+def utf8_encode_value(value):
+ if isinstance(value,unicode):
+ return value.encode('utf-8')
+ return value
+
+def utf8_encode_values(values):
+ if isinstance(values,list) or isinstance(values,tuple):
+ return map(utf8_encode_value, values)
+ else:
+ return utf8_encode_value(values)
+
+def copy_CIDict(x):
+ """Do a deep copy of a CIDict"""
+ y = {}
+ for key, value in x.iteritems():
+ y[copy.deepcopy(key)] = copy.deepcopy(value)
+ return y
+
+class Entity:
+ """This class represents an IPA user. An LDAP entry consists of a DN
+ and a list of attributes. Each attribute consists of a name and a list of
+ values. For the time being I will maintain this.
+
+ In python-ldap, entries are returned as a list of 2-tuples.
+ Instance variables:
+ dn - string - the string DN of the entry
+ data - CIDict - case insensitive dict of the attributes and values
+ orig_data - CIDict - case insentiive dict of the original attributes and values"""
+
+ def __init__(self,entrydata=None):
+ """data is the raw data returned from the python-ldap result method,
+ which is a search result entry or a reference or None.
+ If creating a new empty entry, data is the string DN."""
+ if entrydata:
+ if isinstance(entrydata,tuple):
+ self.dn = entrydata[0]
+ self.data = ipa.ipautil.CIDict(entrydata[1])
+ elif isinstance(entrydata,str) or isinstance(entrydata,unicode):
+ self.dn = entrydata
+ self.data = ipa.ipautil.CIDict()
+ elif isinstance(entrydata,dict):
+ self.dn = entrydata['dn']
+ del entrydata['dn']
+ self.data = ipa.ipautil.CIDict(entrydata)
+ else:
+ self.dn = ''
+ self.data = ipa.ipautil.CIDict()
+
+ self.orig_data = ipa.ipautil.CIDict(copy_CIDict(self.data))
+
+ def __nonzero__(self):
+ """This allows us to do tests like if entry: returns false if there is no data,
+ true otherwise"""
+ return self.data != None and len(self.data) > 0
+
+ def hasAttr(self,name):
+ """Return True if this entry has an attribute named name, False otherwise"""
+ return self.data and self.data.has_key(name)
+
+ def __setattr__(self,name,value):
+ """One should use setValue() or setValues() to set values except for
+ dn and data which are special."""
+ if name != 'dn' and name != 'data' and name != 'orig_data':
+ raise KeyError, 'use setValue() or setValues()'
+ else:
+ self.__dict__[name] = value
+
+ def __getattr__(self,name):
+ """If name is the name of an LDAP attribute, return the first value for that
+ attribute - equivalent to getValue - this allows the use of
+ entry.cn
+ instead of
+ entry.getValue('cn')
+ This also allows us to return None if an attribute is not found rather than
+ throwing an exception"""
+ return self.getValue(name)
+
+ def getValues(self,name):
+ """Get the list (array) of values for the attribute named name"""
+ return self.data.get(name)
+
+ def getValue(self,name,default=None):
+ """Get the first value for the attribute named name"""
+ value = self.data.get(name,default)
+ if isinstance(value,list) or isinstance(value,tuple):
+ return value[0]
+ else:
+ return value
+
+ def setValue(self,name,*value):
+ """Value passed in may be a single value, several values, or a single sequence.
+ For example:
+ ent.setValue('name', 'value')
+ ent.setValue('name', 'value1', 'value2', ..., 'valueN')
+ ent.setValue('name', ['value1', 'value2', ..., 'valueN'])
+ ent.setValue('name', ('value1', 'value2', ..., 'valueN'))
+ Since *value is a tuple, we may have to extract a list or tuple from that
+ tuple as in the last two examples above"""
+ if (len(value) < 1):
+ return
+ if (len(value) == 1):
+ self.data[name] = utf8_encode_values(value[0])
+ else:
+ self.data[name] = utf8_encode_values(value)
+
+ setValues = setValue
+
+ def setValueNotEmpty(self,name,*value):
+ """Similar to setValue() but will not set an empty field. This
+ is an attempt to avoid adding empty attributes."""
+ if (len(value) >= 1) and value[0] and len(value[0]) > 0:
+ if isinstance(value[0], list):
+ if len(value[0][0]) > 0:
+ self.setValue(name, *value)
+ return
+ else:
+ self.setValue(name, *value)
+ return
+
+ # At this point we have an empty incoming value. See if they are
+ # trying to erase the current value. If so we'll delete it so
+ # it gets marked as removed in the modlist.
+ v = self.getValues(name)
+ if v:
+ self.delValue(name)
+
+ return
+
+ def delValue(self,name):
+ """Remove the attribute named name."""
+ if self.data.get(name,None):
+ del self.data[name]
+
+ def toTupleList(self):
+ """Convert the attrs and values to a list of 2-tuples. The first element
+ of the tuple is the attribute name. The second element is either a
+ single value or a list of values."""
+ return self.data.items()
+
+ def toDict(self):
+ """Convert the attrs and values to a dict. The dict is keyed on the
+ attribute name. The value is either single value or a list of values."""
+ result = ipa.ipautil.CIDict(self.data)
+ result['dn'] = self.dn
+ return result
+
+ def attrList(self):
+ """Return a list of all attributes in the entry"""
+ return self.data.keys()
+
+ def origDataDict(self):
+ """Returns a dict of the original values of the user. Used for updates."""
+ result = ipa.ipautil.CIDict(self.orig_data)
+ result['dn'] = self.dn
+ return result
+
+# def __str__(self):
+# """Convert the Entry to its LDIF representation"""
+# return self.__repr__()
+#
+# # the ldif class base64 encodes some attrs which I would rather see in raw form - to
+# # encode specific attrs as base64, add them to the list below
+# ldif.safe_string_re = re.compile('^$')
+# base64_attrs = ['nsstate', 'krbprincipalkey', 'krbExtraData']
+#
+# def __repr__(self):
+# """Convert the Entry to its LDIF representation"""
+# sio = cStringIO.StringIO()
+# # what's all this then? the unparse method will currently only accept
+# # a list or a dict, not a class derived from them. self.data is a
+# # cidict, so unparse barfs on it. I've filed a bug against python-ldap,
+# # but in the meantime, we have to convert to a plain old dict for printing
+# # I also don't want to see wrapping, so set the line width really high (1000)
+# newdata = {}
+# newdata.update(self.data)
+# ldif.LDIFWriter(sio,User.base64_attrs,1000).unparse(self.dn,newdata)
+# return sio.getvalue()
diff --git a/ipa-python/group.py b/ipa-python/group.py
new file mode 100644
index 00000000..342a905b
--- /dev/null
+++ b/ipa-python/group.py
@@ -0,0 +1,24 @@
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+from ipa.entity import Entity
+
+class Group(Entity):
+
+ def __init2__(self):
+ pass
+
diff --git a/ipa-python/ipa-python.spec.in b/ipa-python/ipa-python.spec.in
new file mode 100755
index 00000000..a41a413e
--- /dev/null
+++ b/ipa-python/ipa-python.spec.in
@@ -0,0 +1,82 @@
+Name: ipa-python
+Version: __VERSION__
+Release: __RELEASE__%{?dist}
+Summary: IPA authentication server
+
+Group: System Environment/Base
+License: GPLv2
+URL: http://www.freeipa.org
+Source0: http://www.freeipa.org/downloads/%{name}-%{version}.tgz
+BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n)
+BuildArch: noarch
+BuildRequires: python-devel
+Requires: python-kerberos gnupg
+
+%{!?python_sitelib: %define python_sitelib %(%{__python} -c "from distutils.sysconfig import get_python_lib; print get_python_lib()")}
+
+%description
+IPA is a server for identity, policy, and audit.
+
+%prep
+%setup -q
+
+%build
+
+%install
+rm -rf %{buildroot}
+%{__python} setup.py install --no-compile --root=%{buildroot}
+
+%clean
+rm -rf %{buildroot}
+
+%files
+%defattr(-,root,root,-)
+%{python_sitelib}/*
+%config(noreplace) %{_sysconfdir}/ipa/ipa.conf
+
+%changelog
+* Thu Apr 3 2008 Rob Crittenden <rcritten@redhat.com> - 1.0.0-1
+- Version bump for release
+
+* Thu Feb 21 2008 Rob Crittenden <rcritten@redhat.com> - 0.99.0-1
+- Version bump for release
+
+* Thu Jan 31 2008 Rob Crittenden <rcritten@redhat.com> - 0.6.0-4
+- Marked with wrong license. IPA is GPLv2.
+
+* Thu Jan 24 2008 Rob Crittenden <rcritten@redhat.com> - 0.6.0-3
+- Use new name of PyKerberos, python-kerberos, in Requires
+
+* Thu Jan 17 2008 Rob Crittenden <rcritten@redhat.com> - 0.6.0-2
+- Fixed License in specfile
+
+* Fri Dec 21 2007 Karl MacMillan <kmacmill@redhat.com> - 0.6.0-1
+- Version bump for release
+
+* Wed Nov 21 2007 Karl MacMillan <kmacmill@redhat.com> - 0.5.0-1
+- Version bump for release and rename of rpm
+
+* Thu Nov 1 2007 Karl MacMillan <kmacmill@redhat.com> - 0.4.1-1
+- Version bump for release
+
+* Wed Oct 17 2007 Rob Crittenden <rcritten@redhat.com> - 0.4.0-2
+- Use new python setup.py build script
+
+* Tue Oct 2 2007 Karl MacMillan <kmacmill@redhat.com> - 0.4.0-1
+- Milestone 4
+
+* Mon Sep 10 2007 Karl MacMillan <kmacmill@redhat.com> - 0.3.0-1
+- Milestone 3
+
+* Fri Aug 17 2007 Karl MacMillan <kmacmill@redhat.com> = 0.2.0-4
+- Added PyKerberos dep.
+
+* Mon Aug 5 2007 Rob Crittenden <rcritten@redhat.com> - 0.1.0-3
+- Abstracted client class to work directly or over RPC
+
+* Wed Aug 1 2007 Rob Crittenden <rcritten@redhat.com> - 0.1.0-2
+- Add User class
+- Add kerberos authentication to the XML-RPC request made from tools.
+
+* Fri Jul 27 2007 Karl MacMillan <kmacmill@localhost.localdomain> - 0.1.0-1
+- Initial rpm version
diff --git a/ipa-python/ipa.conf b/ipa-python/ipa.conf
new file mode 100644
index 00000000..516f764d
--- /dev/null
+++ b/ipa-python/ipa.conf
@@ -0,0 +1,3 @@
+[defaults]
+# realm = EXAMPLE.COM
+# server = ipa.example.com
diff --git a/ipa-python/ipaadminutil.py b/ipa-python/ipaadminutil.py
new file mode 100644
index 00000000..5f0b2fa9
--- /dev/null
+++ b/ipa-python/ipaadminutil.py
@@ -0,0 +1,96 @@
+# Authors: Rob Crittenden <rcritten@redhat.com>
+#
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+import string
+import tempfile
+import logging
+import subprocess
+import os
+import ipa.ipavalidate as ipavalidate
+
+def select_user(counter, users):
+ """counter is the number of User objects in users
+ users is a list of User objects
+
+ This purposely doesn't catch KeyboardInterrupt
+ """
+ i = 1
+ print "%s entries were found. Which one would you like to display?" % counter
+ for ent in users:
+ if (ent.getValues('givenname')) is not None:
+ print "%s: %s %s (%s)" % (i, ent.getValues('givenname'), ent.getValues('sn'), ent.getValues('uid'))
+ else:
+ print "%s: %s (%s)" % (i, ent.getValues('sn'), ent.getValues('uid'))
+ i += 1
+ while True:
+ resp = raw_input("Choose one: (1 - %s), 0 for all, q to quit: " % counter)
+ if resp == "q":
+ return "q"
+ if resp == "0":
+ userindex = -1
+ break
+ try:
+ userindex = int(resp) - 1
+ if (userindex >= 0 and userindex < counter):
+ break
+ except:
+ # fall through to the error msg
+ pass
+
+ print "Please enter a number between 1 and %s" % counter
+
+ return userindex
+
+def select_group(counter, groups):
+ """counter is the number of Group objects in users
+ users is a list of Group objects
+
+ This purposely doesn't catch KeyboardInterrupt
+ """
+ i = 1
+ print "%s entries were found. Which one would you like to display?" % counter
+ for ent in groups:
+ print "%s: %s" % (i, ent.getValues('cn'))
+ i += 1
+ while True:
+ resp = raw_input("Choose one: (1 - %s), 0 for all, q to quit: " % counter)
+ if resp == "q":
+ return "q"
+ if resp == "0":
+ groupindex = -1
+ break
+ try:
+ groupindex = int(resp) - 1
+ if (groupindex >= 0 and groupindex < counter):
+ break
+ except:
+ # fall through to the error msg
+ pass
+
+ print "Please enter a number between 1 and %s" % counter
+
+ return groupindex
+
+def check_name(name):
+ """Helper to ensure that a user or group name is legal"""
+
+ if (not ipavalidate.GoodName(name, notEmpty=True)):
+ raise ValueError("may only include letters, numbers, _, -, . and $")
+
+ return
diff --git a/ipa-python/ipaclient.py b/ipa-python/ipaclient.py
new file mode 100644
index 00000000..6f31dd8f
--- /dev/null
+++ b/ipa-python/ipaclient.py
@@ -0,0 +1,471 @@
+# Authors: Rob Crittenden <rcritten@redhat.com>
+#
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+import ipa.rpcclient as rpcclient
+import entity
+import user
+import group
+import radius_util
+
+class IPAClient:
+
+ def __init__(self,transport=None,verbose=False):
+ if transport:
+ self.local = True
+ self.transport = transport
+ else:
+ self.local = False
+ self.transport = rpcclient.RPCClient(verbose)
+
+ def set_principal(self,princ):
+ """Set the name of the principal that will be used for
+ LDAP proxy authentication"""
+ if self.local:
+ self.transport.set_principal(princ)
+
+ def set_krbccache(self,krbccache):
+ """Set the file location of the Kerberos credentials cache to be used
+ for LDAP authentication"""
+ if self.local:
+ self.transport.set_krbccache(krbccache)
+
+# Higher-level API
+
+ def get_aci_entry(self, sattrs=None):
+ """Returns the entry containing access control ACIs."""
+
+ result = self.transport.get_aci_entry(sattrs)
+ return entity.Entity(result)
+
+# General searches
+
+ def get_entry_by_dn(self,dn,sattrs=None):
+ """Get a specific entry by dn. If sattrs is set then only those
+ attributes will be returned, otherwise all available attributes
+ are returned."""
+ result = self.transport.get_entry_by_dn(dn,sattrs)
+ return entity.Entity(result)
+
+ def get_entry_by_cn(self,cn,sattrs=None):
+ """Get a specific entry by cn. If sattrs is set then only those
+ attributes will be returned, otherwise all available attributes
+ are returned."""
+ result = self.transport.get_entry_by_cn(cn,sattrs)
+ return entity.Entity(result)
+
+ def update_entry(self,entry):
+ """Update a entry."""
+
+ result = self.transport.update_entry(entry.origDataDict(), entry.toDict())
+ return result
+
+# User support
+ def get_user_by_uid(self,uid,sattrs=None):
+ """Get a specific user by uid. If sattrs is set then only those
+ attributes will be returned, otherwise all available attributes
+ are returned."""
+ result = self.transport.get_user_by_uid(uid,sattrs)
+ return user.User(result)
+
+ def get_user_by_principal(self,principal,sattrs=None):
+ """Get a specific user by uid. If sattrs is set then only those
+ attributes will be returned, otherwise all available attributes
+ are returned."""
+ result = self.transport.get_user_by_principal(principal,sattrs)
+ return user.User(result)
+
+ def get_user_by_email(self,email,sattrs=None):
+ """Get a specific user's entry. Return as a dict of values.
+ Multi-valued fields are represented as lists.
+ """
+ result = self.transport.get_user_by_email(email,sattrs)
+ return user.User(result)
+
+ def get_users_by_manager(self,manager_dn,sattrs=None):
+ """Gets the users the report to a particular manager.
+ If sattrs is not None then only those
+ attributes will be returned, otherwise all available
+ attributes are returned. The result is a list of groups."""
+ results = self.transport.get_users_by_manager(manager_dn, sattrs)
+
+ return map(lambda result: user.User(result), results)
+
+ def add_user(self,user,user_container=None):
+ """Add a user. user is a ipa.user.User object"""
+
+ user_dict = user.toDict()
+
+ # convert to a regular dict before sending
+ result = self.transport.add_user(user_dict, user_container)
+ return result
+
+ def get_all_users(self):
+ """Get as a list of User objects all users in the directory"""
+ result = self.transport.get_all_users()
+
+ all_users = []
+ for attrs in result:
+ if attrs is not None:
+ all_users.append(user.User(attrs))
+
+ return all_users
+
+ def get_custom_fields(self):
+ """Get custom user fields"""
+ result = self.transport.get_custom_fields()
+ return result
+
+ def set_custom_fields(self, schema):
+ """Set custom user fields"""
+ result = self.transport.set_custom_fields(schema)
+ return result
+
+ def find_users(self, criteria, sattrs=None, sizelimit=-1, timelimit=-1):
+ """Return a list: counter followed by a User object for each user that
+ matches the criteria. If the results are truncated, counter will
+ be set to -1"""
+ result = self.transport.find_users(criteria, sattrs, sizelimit, timelimit)
+ counter = result[0]
+
+ users = [counter]
+ for attrs in result[1:]:
+ if attrs is not None:
+ users.append(user.User(attrs))
+
+ return users
+
+ def update_user(self,user):
+ """Update a user entry."""
+
+ result = self.transport.update_user(user.origDataDict(), user.toDict())
+ return result
+
+ def delete_user(self,uid):
+ """Delete a user entry."""
+
+ result = self.transport.delete_user(uid)
+ return result
+
+ def modifyPassword(self,principal,oldpass,newpass):
+ """Modify a user's password"""
+
+ result = self.transport.modifyPassword(principal,oldpass,newpass)
+
+ return result
+
+ def mark_user_active(self,uid):
+ """Set a user as active by uid."""
+
+ result = self.transport.mark_user_active(uid)
+ return result
+
+ def mark_user_inactive(self,uid):
+ """Set a user as inactive by uid."""
+
+ result = self.transport.mark_user_inactive(uid)
+ return result
+
+# Groups support
+
+ def get_groups_by_member(self,member_dn,sattrs=None):
+ """Gets the groups that member_dn belongs to.
+ If sattrs is not None then only those
+ attributes will be returned, otherwise all available
+ attributes are returned. The result is a list of groups."""
+ results = self.transport.get_groups_by_member(member_dn,sattrs)
+
+ return map(lambda result: group.Group(result), results)
+
+ def add_group(self,group,group_container=None):
+ """Add a group. group is a ipa.group.Group object"""
+
+ group_dict = group.toDict()
+
+ # dn is set on the server-side
+ del group_dict['dn']
+
+ # convert to a regular dict before sending
+ result = self.transport.add_group(group_dict, group_container)
+ return result
+
+ def find_groups(self, criteria, sattrs=None, sizelimit=-1, timelimit=-1):
+ """Find groups whose cn matches the criteria. Wildcards are
+ acceptable. Returns a list of Group objects."""
+ result = self.transport.find_groups(criteria, sattrs, sizelimit, timelimit)
+ counter = result[0]
+
+ groups = [counter]
+ for attrs in result[1:]:
+ if attrs is not None:
+ groups.append(group.Group(attrs))
+
+ return groups
+
+ def add_member_to_group(self, member_dn, group_dn):
+ """Add a member to an existing group.
+ """
+
+ return self.transport.add_member_to_group(member_dn, group_dn)
+
+ def add_members_to_group(self, member_dns, group_dn):
+ """Add several members to an existing group.
+ member_dns is a list of dns to add
+
+ Returns a list of the dns that were not added.
+ """
+
+ return self.transport.add_members_to_group(member_dns, group_dn)
+
+ def remove_member_from_group(self, member_dn, group_dn):
+ """Remove a member from an existing group.
+ """
+
+ return self.transport.remove_member_from_group(member_dn, group_dn)
+
+ def remove_members_from_group(self, member_dns, group_dn):
+ """Remove several members from an existing group.
+ member_dns is a list of dns to remove
+
+ Returns a list of the dns that were not removed.
+ """
+
+ return self.transport.remove_members_from_group(member_dns, group_dn)
+
+ def add_user_to_group(self, user_uid, group_dn):
+ """Add a user to an existing group.
+ user is a uid of the user to add
+ group is the cn of the group to be added to
+ """
+
+ return self.transport.add_user_to_group(user_uid, group_dn)
+
+ def add_users_to_group(self, user_uids, group_dn):
+ """Add several users to an existing group.
+ user_uids is a list of uids of the users to add
+
+ Returns a list of the user uids that were not added.
+ """
+
+ return self.transport.add_users_to_group(user_uids, group_dn)
+
+ def remove_user_from_group(self, user_uid, group_dn):
+ """Remove a user from an existing group.
+ user is a uid of the user to remove
+ group is the cn of the group to be removed from
+ """
+
+ return self.transport.remove_user_from_group(user_uid, group_dn)
+
+ def remove_users_from_group(self, user_uids, group_dn):
+ """Remove several users from an existing group.
+ user_uids is a list of uids of the users to remove
+
+ Returns a list of the user uids that were not removed.
+ """
+
+ return self.transport.remove_users_from_group(user_uids, group_dn)
+
+ def add_groups_to_user(self, group_dns, user_dn):
+ """Given a list of group dn's add them to the user.
+
+ Returns a list of the group dns that were not added.
+ """
+ return self.transport.add_groups_to_user(group_dns, user_dn)
+
+ def remove_groups_from_user(self, group_dns, user_dn):
+ """Given a list of group dn's remove them from the user.
+
+ Returns a list of the group dns that were not removed.
+ """
+
+ return self.transport.remove_groups_from_user(group_dns, user_dn)
+
+ def update_group(self,group):
+ """Update a group entry."""
+
+ return self.transport.update_group(group.origDataDict(), group.toDict())
+
+ def delete_group(self,group_dn):
+ """Delete a group entry."""
+
+ return self.transport.delete_group(group_dn)
+
+ def add_group_to_group(self, group_cn, tgroup_cn):
+ """Add a group to an existing group.
+ group_cn is a cn of the group to add
+ tgroup_cn is the cn of the group to be added to
+ """
+
+ return self.transport.add_group_to_group(group_cn, tgroup_cn)
+
+ def attrs_to_labels(self,attrs):
+ """Convert a list of LDAP attributes into a more readable form."""
+
+ return self.transport.attrs_to_labels(attrs)
+
+ def get_all_attrs(self):
+ """We have a list of hardcoded attributes -> readable labels. Return
+ that complete list if someone wants it.
+ """
+
+ return self.transport.get_all_attrs()
+
+ def group_members(self, groupdn, attr_list, membertype):
+ """Do a memberOf search of groupdn and return the attributes in
+ attr_list (an empty list returns everything)."""
+
+ results = self.transport.group_members(groupdn, attr_list, membertype)
+
+ counter = results[0]
+
+ entries = [counter]
+ for e in results[1:]:
+ if e is not None:
+ entries.append(user.User(e))
+
+ return entries
+
+ def mark_group_active(self,cn):
+ """Set a group as active by cn."""
+
+ result = self.transport.mark_group_active(cn)
+ return result
+
+ def mark_group_inactive(self,cn):
+ """Set a group as inactive by cn."""
+
+ result = self.transport.mark_group_inactive(cn)
+ return result
+
+# Configuration
+
+ def get_ipa_config(self):
+ """Get the IPA configuration"""
+ result = self.transport.get_ipa_config()
+ return entity.Entity(result)
+
+ def update_ipa_config(self, config):
+ """Updates the IPA configuration.
+
+ config is an Entity object.
+ """
+ result = self.transport.update_ipa_config(config.origDataDict(), config.toDict())
+ return result
+
+ def get_password_policy(self):
+ """Get the IPA password policy"""
+ result = self.transport.get_password_policy()
+ return entity.Entity(result)
+
+ def update_password_policy(self, policy):
+ """Updates the IPA password policy.
+
+ policy is an Entity object.
+ """
+ result = self.transport.update_password_policy(policy.origDataDict(), policy.toDict())
+ return result
+
+ def add_service_principal(self, princ_name, force):
+ return self.transport.add_service_principal(princ_name, force)
+
+ def delete_service_principal(self, principal_dn):
+ return self.transport.delete_service_principal(principal_dn)
+
+ def find_service_principal(self, criteria, sattrs=None, sizelimit=-1, timelimit=-1):
+ """Return a list: counter followed by a Entity object for each host that
+ matches the criteria. If the results are truncated, counter will
+ be set to -1"""
+ result = self.transport.find_service_principal(criteria, sattrs, sizelimit, timelimit)
+ counter = result[0]
+
+ hosts = [counter]
+ for attrs in result[1:]:
+ if attrs is not None:
+ hosts.append(entity.Entity(attrs))
+
+ return hosts
+
+ def get_keytab(self, princ_name):
+ return self.transport.get_keytab(princ_name)
+
+# radius support
+ def get_radius_client_by_ip_addr(self, ip_addr, container=None, sattrs=None):
+ result = self.transport.get_radius_client_by_ip_addr(ip_addr, container, sattrs)
+ return radius_util.RadiusClient(result)
+
+ def add_radius_client(self, client, container=None):
+ client_dict = client.toDict()
+
+ # dn is set on the server-side
+ del client_dict['dn']
+
+ # convert to a regular dict before sending
+ result = self.transport.add_radius_client(client_dict, container)
+ return result
+
+ def update_radius_client(self, client):
+ result = self.transport.update_radius_client(client.origDataDict(), client.toDict())
+ return result
+
+ def delete_radius_client(self, ip_addr, container=None):
+ return self.transport.delete_radius_client(ip_addr, container)
+
+ def find_radius_clients(self, criteria, container=None, sattrs=None, sizelimit=-1, timelimit=-1):
+ result = self.transport.find_radius_clients(criteria, container, sattrs, sizelimit, timelimit)
+ counter = result[0]
+
+ users = [counter]
+ for attrs in result[1:]:
+ if attrs is not None:
+ users.append(user.User(attrs))
+
+ return users
+
+ def get_radius_profile_by_uid(self, uid, user_profile=None, sattrs=None):
+ result = self.transport.get_radius_profile_by_uid(uid, user_profile, sattrs)
+ return radius_util.RadiusClient(result)
+
+ def add_radius_profile(self, profile, user_profile=None):
+ profile_dict = profile.toDict()
+
+ # dn is set on the server-side
+ del profile_dict['dn']
+
+ # convert to a regular dict before sending
+ result = self.transport.add_radius_profile(profile_dict, user_profile)
+ return result
+
+ def update_radius_profile(self, profile):
+ result = self.transport.update_radius_profile(profile.origDataDict(), profile.toDict())
+ return result
+
+ def delete_radius_profile(self, ip_addr, user_profile=None):
+ return self.transport.delete_radius_profile(ip_addr, user_profile)
+
+ def find_radius_profiles(self, criteria, user_profile=None, sattrs=None, sizelimit=-1, timelimit=-1):
+ result = self.transport.find_radius_profiles(criteria, user_profile, sattrs, sizelimit, timelimit)
+ counter = result[0]
+
+ users = [counter]
+ for attrs in result[1:]:
+ if attrs is not None:
+ users.append(user.User(attrs))
+
+ return users
+
diff --git a/ipa-python/ipaerror.py b/ipa-python/ipaerror.py
new file mode 100644
index 00000000..9357bd74
--- /dev/null
+++ b/ipa-python/ipaerror.py
@@ -0,0 +1,259 @@
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+import exceptions
+import types
+
+class IPAError(exceptions.Exception):
+ """Base error class for IPA Code"""
+
+ def __init__(self, code, message="", detail=None):
+ """code is the IPA error code.
+ message is a human viewable error message.
+ detail is an optional exception that provides more detail about the
+ error."""
+ self.code = code
+ self.message = message
+ # Fill this in as an empty LDAP error message so we don't have a lot
+ # of "if e.detail ..." everywhere
+ if detail is None:
+ detail = []
+ detail.append({'desc':'','info':''})
+ self.detail = detail
+
+ def __str__(self):
+ return self.message
+
+ def __repr__(self):
+ repr = "%d: %s" % (self.code, self.message)
+ if self.detail:
+ repr += "\n%s" % str(self.detail)
+ return repr
+
+
+###############
+# Error codes #
+###############
+
+code_map_dict = {}
+
+def gen_exception(code, message=None, nested_exception=None):
+ """This should be used by IPA code to translate error codes into the
+ correct exception/message to throw.
+
+ message is an optional argument which overrides the default message.
+
+ nested_exception is an optional argument providing more details
+ about the error."""
+ (default_message, exception) = code_map_dict.get(code, ("unknown", IPAError))
+ if not message:
+ message = default_message
+ return exception(code, message, nested_exception)
+
+def exception_for(code):
+ """Used to look up the corresponding exception for an error code.
+ Will usually be used for an except block."""
+ (default_message, exception) = code_map_dict.get(code, ("unknown", IPAError))
+ return exception
+
+def gen_error_code(category, detail, message):
+ """Private method used to generate exception codes.
+ category is one of the 16 bit error code category constants.
+ detail is a 16 bit code within the category.
+ message is a human readable description on the error.
+ exception is the exception to throw for this error code."""
+ code = (category << 16) + detail
+ exception = types.ClassType("IPAError%d" % code,
+ (IPAError,),
+ {})
+ code_map_dict[code] = (message, exception)
+
+ return code
+
+#
+# Error codes are broken into two 16-bit values: category and detail
+#
+
+#
+# LDAP Errors: 0x0001
+#
+LDAP_CATEGORY = 0x0001
+
+LDAP_DATABASE_ERROR = gen_error_code(
+ LDAP_CATEGORY,
+ 0x0001,
+ "A database error occurred")
+
+LDAP_MIDAIR_COLLISION = gen_error_code(
+ LDAP_CATEGORY,
+ 0x0002,
+ "Change collided with another change")
+
+LDAP_NOT_FOUND = gen_error_code(
+ LDAP_CATEGORY,
+ 0x0003,
+ "Entry not found")
+
+LDAP_DUPLICATE = gen_error_code(
+ LDAP_CATEGORY,
+ 0x0004,
+ "This entry already exists")
+
+LDAP_MISSING_DN = gen_error_code(
+ LDAP_CATEGORY,
+ 0x0005,
+ "Entry missing dn")
+
+LDAP_EMPTY_MODLIST = gen_error_code(
+ LDAP_CATEGORY,
+ 0x0006,
+ "No modifications to be performed")
+
+LDAP_NO_CONFIG = gen_error_code(
+ LDAP_CATEGORY,
+ 0x0007,
+ "IPA configuration not found")
+
+#
+# Function input errors
+#
+INPUT_CATEGORY = 0x0002
+
+INPUT_INVALID_PARAMETER = gen_error_code(
+ INPUT_CATEGORY,
+ 0x0001,
+ "Invalid parameter(s)")
+
+INPUT_SAME_GROUP = gen_error_code(
+ INPUT_CATEGORY,
+ 0x0002,
+ "You can't add a group to itself")
+
+INPUT_NOT_DNS_A_RECORD = gen_error_code(
+ INPUT_CATEGORY,
+ 0x0003,
+ "The requested hostname is not a DNS A record. This is required by Kerberos.")
+
+INPUT_ADMINS_IMMUTABLE = gen_error_code(
+ INPUT_CATEGORY,
+ 0x0004,
+ "The admins group cannot be renamed.")
+
+INPUT_MALFORMED_SERVICE_PRINCIPAL = gen_error_code(
+ INPUT_CATEGORY,
+ 0x0005,
+ "The requested service principal is not of the form: service/fully-qualified host name")
+
+INPUT_REALM_MISMATCH = gen_error_code(
+ INPUT_CATEGORY,
+ 0x0006,
+ "The realm for the principal does not match the realm for this IPA server.")
+
+INPUT_ADMIN_REQUIRED = gen_error_code(
+ INPUT_CATEGORY,
+ 0x0007,
+ "The admin user cannot be deleted.")
+
+INPUT_CANT_INACTIVATE = gen_error_code(
+ INPUT_CATEGORY,
+ 0x0008,
+ "This entry cannot be inactivated.")
+
+INPUT_ADMIN_REQUIRED_IN_ADMINS = gen_error_code(
+ INPUT_CATEGORY,
+ 0x0009,
+ "The admin user cannot be removed from the admins group.")
+
+INPUT_SERVICE_PRINCIPAL_REQUIRED = gen_error_code(
+ INPUT_CATEGORY,
+ 0x000A,
+ "You cannot remove IPA server service principals.")
+
+INPUT_UID_TOO_LONG = gen_error_code(
+ INPUT_CATEGORY,
+ 0x0009,
+ "The requested username is too long.")
+
+#
+# Connection errors
+#
+CONNECTION_CATEGORY = 0x0003
+
+CONNECTION_NO_CONN = gen_error_code(
+ CONNECTION_CATEGORY,
+ 0x0001,
+ "Connection to database failed")
+
+CONNECTION_NO_CCACHE = gen_error_code(
+ CONNECTION_CATEGORY,
+ 0x0002,
+ "No Kerberos credentials cache is available. Connection cannot be made.")
+
+CONNECTION_GSSAPI_CREDENTIALS = gen_error_code(
+ CONNECTION_CATEGORY,
+ 0x0003,
+ "GSSAPI Authorization error")
+
+CONNECTION_UNWILLING = gen_error_code(
+ CONNECTION_CATEGORY,
+ 0x0004,
+ "Account inactivated. Server is unwilling to perform.")
+
+#
+# Configuration errors
+#
+CONFIGURATION_CATEGORY = 0x0004
+
+CONFIG_REQUIRED_GROUPS = gen_error_code(
+ CONFIGURATION_CATEGORY,
+ 0x0001,
+ "The admins and editors groups are required.")
+
+CONFIG_DEFAULT_GROUP = gen_error_code(
+ CONFIGURATION_CATEGORY,
+ 0x0002,
+ "You cannot remove the default users group.")
+
+CONFIG_INVALID_OC = gen_error_code(
+ CONFIGURATION_CATEGORY,
+ 0x0003,
+ "Invalid object class.")
+
+#
+# Entry status errors
+#
+STATUS_CATEGORY = 0x0005
+
+STATUS_ALREADY_ACTIVE = gen_error_code(
+ STATUS_CATEGORY,
+ 0x0001,
+ "This entry is already active.")
+
+STATUS_ALREADY_INACTIVE = gen_error_code(
+ STATUS_CATEGORY,
+ 0x0002,
+ "This entry is already inactive.")
+
+STATUS_HAS_NSACCOUNTLOCK = gen_error_code(
+ STATUS_CATEGORY,
+ 0x0003,
+ "This entry appears to have the nsAccountLock attribute in it so the Class of Service activation/inactivation will not work. You will need to remove the attribute nsAccountLock for this to work.")
+
+STATUS_NOT_GROUP_MEMBER = gen_error_code(
+ STATUS_CATEGORY,
+ 0x0004,
+ "This entry is not a member of the group.")
diff --git a/ipa-python/ipautil.py b/ipa-python/ipautil.py
new file mode 100644
index 00000000..780fef3d
--- /dev/null
+++ b/ipa-python/ipautil.py
@@ -0,0 +1,989 @@
+# Authors: Simo Sorce <ssorce@redhat.com>
+#
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+SHARE_DIR = "/usr/share/ipa/"
+PLUGINS_SHARE_DIR = "/usr/share/ipa/plugins"
+
+import string
+import tempfile
+import logging
+import subprocess
+import random
+import os, sys, traceback, readline
+import stat
+import shutil
+
+from ipa import ipavalidate
+from ipa import ipaadminutil
+from types import *
+
+import re
+import xmlrpclib
+import datetime
+from ipa import config
+try:
+ from subprocess import CalledProcessError
+ class CalledProcessError(subprocess.CalledProcessError):
+ def __init__(self, returncode, cmd):
+ super(CalledProcessError, self).__init__(returncode, cmd)
+except ImportError:
+ # Python 2.4 doesn't implement CalledProcessError
+ class CalledProcessError(Exception):
+ """This exception is raised when a process run by check_call() returns
+ a non-zero exit status. The exit status will be stored in the
+ returncode attribute."""
+ def __init__(self, returncode, cmd):
+ self.returncode = returncode
+ self.cmd = cmd
+ def __str__(self):
+ return "Command '%s' returned non-zero exit status %d" % (self.cmd, self.returncode)
+
+def get_domain_name():
+ try:
+ config.init_config()
+ domain_name = config.config.get_domain()
+ except Exception, e:
+ return None
+
+ return domain_name
+
+def realm_to_suffix(realm_name):
+ s = realm_name.split(".")
+ terms = ["dc=" + x.lower() for x in s]
+ return ",".join(terms)
+
+def template_str(txt, vars):
+ return string.Template(txt).substitute(vars)
+
+def template_file(infilename, vars):
+ txt = open(infilename).read()
+ return template_str(txt, vars)
+
+def write_tmp_file(txt):
+ fd = tempfile.NamedTemporaryFile()
+ fd.write(txt)
+ fd.flush()
+
+ return fd
+
+def run(args, stdin=None):
+ if stdin:
+ p = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True)
+ stdout,stderr = p.communicate(stdin)
+ else:
+ p = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True)
+ stdout,stderr = p.communicate()
+
+ logging.info(stdout)
+ logging.info(stderr)
+
+ if p.returncode != 0:
+ raise CalledProcessError(p.returncode, ' '.join(args))
+
+ return (stdout, stderr)
+
+def file_exists(filename):
+ try:
+ mode = os.stat(filename)[stat.ST_MODE]
+ if stat.S_ISREG(mode):
+ return True
+ else:
+ return False
+ except:
+ return False
+
+def dir_exists(filename):
+ try:
+ mode = os.stat(filename)[stat.ST_MODE]
+ if stat.S_ISDIR(mode):
+ return True
+ else:
+ return False
+ except:
+ return False
+
+def install_file(fname, dest):
+ if file_exists(dest):
+ os.rename(dest, dest + ".orig")
+ shutil.move(fname, dest)
+
+def backup_file(fname):
+ if file_exists(fname):
+ os.rename(fname, fname + ".orig")
+
+# uses gpg to compress and encrypt a file
+def encrypt_file(source, dest, password, workdir = None):
+ if type(source) is not StringType or not len(source):
+ raise ValueError('Missing Source File')
+ #stat it so that we get back an exception if it does no t exist
+ os.stat(source)
+
+ if type(dest) is not StringType or not len(dest):
+ raise ValueError('Missing Destination File')
+
+ if type(password) is not StringType or not len(password):
+ raise ValueError('Missing Password')
+
+ #create a tempdir so that we can clean up with easily
+ tempdir = tempfile.mkdtemp('', 'ipa-', workdir)
+ gpgdir = tempdir+"/.gnupg"
+
+ try:
+ try:
+ #give gpg a fake dir so that we can leater remove all
+ #the cruft when we clean up the tempdir
+ os.mkdir(gpgdir)
+ args = ['/usr/bin/gpg', '--homedir', gpgdir, '--passphrase-fd', '0', '--yes', '--no-tty', '-o', dest, '-c', source]
+ run(args, password)
+ except:
+ raise
+ finally:
+ #job done, clean up
+ shutil.rmtree(tempdir, ignore_errors=True)
+
+
+def decrypt_file(source, dest, password, workdir = None):
+ if type(source) is not StringType or not len(source):
+ raise ValueError('Missing Source File')
+ #stat it so that we get back an exception if it does no t exist
+ os.stat(source)
+
+ if type(dest) is not StringType or not len(dest):
+ raise ValueError('Missing Destination File')
+
+ if type(password) is not StringType or not len(password):
+ raise ValueError('Missing Password')
+
+ #create a tempdir so that we can clean up with easily
+ tempdir = tempfile.mkdtemp('', 'ipa-', workdir)
+ gpgdir = tempdir+"/.gnupg"
+
+ try:
+ try:
+ #give gpg a fake dir so that we can leater remove all
+ #the cruft when we clean up the tempdir
+ os.mkdir(gpgdir)
+ args = ['/usr/bin/gpg', '--homedir', gpgdir, '--passphrase-fd', '0', '--yes', '--no-tty', '-o', dest, '-d', source]
+ run(args, password)
+ except:
+ raise
+ finally:
+ #job done, clean up
+ shutil.rmtree(tempdir, ignore_errors=True)
+
+
+class CIDict(dict):
+ """
+ Case-insensitive but case-respecting dictionary.
+
+ This code is derived from python-ldap's cidict.py module,
+ written by stroeder: http://python-ldap.sourceforge.net/
+
+ This version extends 'dict' so it works properly with TurboGears.
+ If you extend UserDict, isinstance(foo, dict) returns false.
+ """
+
+ def __init__(self,default=None):
+ super(CIDict, self).__init__()
+ self._keys = {}
+ self.update(default or {})
+
+ def __getitem__(self,key):
+ return super(CIDict,self).__getitem__(string.lower(key))
+
+ def __setitem__(self,key,value):
+ lower_key = string.lower(key)
+ self._keys[lower_key] = key
+ return super(CIDict,self).__setitem__(string.lower(key),value)
+
+ def __delitem__(self,key):
+ lower_key = string.lower(key)
+ del self._keys[lower_key]
+ return super(CIDict,self).__delitem__(string.lower(key))
+
+ def update(self,dict):
+ for key in dict.keys():
+ self[key] = dict[key]
+
+ def has_key(self,key):
+ return super(CIDict, self).has_key(string.lower(key))
+
+ def get(self,key,failobj=None):
+ try:
+ return self[key]
+ except KeyError:
+ return failobj
+
+ def keys(self):
+ return self._keys.values()
+
+ def items(self):
+ result = []
+ for k in self._keys.values():
+ result.append((k,self[k]))
+ return result
+
+ def copy(self):
+ copy = {}
+ for k in self._keys.values():
+ copy[k] = self[k]
+ return copy
+
+ def iteritems(self):
+ return self.copy().iteritems()
+
+ def iterkeys(self):
+ return self.copy().iterkeys()
+
+ def setdefault(self,key,value=None):
+ try:
+ return self[key]
+ except KeyError:
+ self[key] = value
+ return value
+
+ def pop(self, key, *args):
+ try:
+ value = self[key]
+ del self[key]
+ return value
+ except KeyError:
+ if len(args) == 1:
+ return args[0]
+ raise
+
+ def popitem(self):
+ (lower_key,value) = super(CIDict,self).popitem()
+ key = self._keys[lower_key]
+ del self._keys[lower_key]
+
+ return (key,value)
+
+
+#
+# The safe_string_re regexp and needs_base64 function are extracted from the
+# python-ldap ldif module, which was
+# written by Michael Stroeder <michael@stroeder.com>
+# http://python-ldap.sourceforge.net
+#
+# It was extracted because ipaldap.py is naughtily reaching into the ldif
+# module and squashing this regexp.
+#
+SAFE_STRING_PATTERN = '(^(\000|\n|\r| |:|<)|[\000\n\r\200-\377]+|[ ]+$)'
+safe_string_re = re.compile(SAFE_STRING_PATTERN)
+
+def needs_base64(s):
+ """
+ returns 1 if s has to be base-64 encoded because of special chars
+ """
+ return not safe_string_re.search(s) is None
+
+
+def wrap_binary_data(data):
+ """Converts all binary data strings into Binary objects for transport
+ back over xmlrpc."""
+ if isinstance(data, str):
+ if needs_base64(data):
+ return xmlrpclib.Binary(data)
+ else:
+ return data
+ elif isinstance(data, list) or isinstance(data,tuple):
+ retval = []
+ for value in data:
+ retval.append(wrap_binary_data(value))
+ return retval
+ elif isinstance(data, dict):
+ retval = {}
+ for (k,v) in data.iteritems():
+ retval[k] = wrap_binary_data(v)
+ return retval
+ else:
+ return data
+
+
+def unwrap_binary_data(data):
+ """Converts all Binary objects back into strings."""
+ if isinstance(data, xmlrpclib.Binary):
+ # The data is decoded by the xmlproxy, but is stored
+ # in a binary object for us.
+ return str(data)
+ elif isinstance(data, str):
+ return data
+ elif isinstance(data, list) or isinstance(data,tuple):
+ retval = []
+ for value in data:
+ retval.append(unwrap_binary_data(value))
+ return retval
+ elif isinstance(data, dict):
+ retval = {}
+ for (k,v) in data.iteritems():
+ retval[k] = unwrap_binary_data(v)
+ return retval
+ else:
+ return data
+
+class GeneralizedTimeZone(datetime.tzinfo):
+ """This class is a basic timezone wrapper for the offset specified
+ in a Generalized Time. It is dst-ignorant."""
+ def __init__(self,offsetstr="Z"):
+ super(GeneralizedTimeZone, self).__init__()
+
+ self.name = offsetstr
+ self.houroffset = 0
+ self.minoffset = 0
+
+ if offsetstr == "Z":
+ self.houroffset = 0
+ self.minoffset = 0
+ else:
+ if (len(offsetstr) >= 3) and re.match(r'[-+]\d\d', offsetstr):
+ self.houroffset = int(offsetstr[0:3])
+ offsetstr = offsetstr[3:]
+ if (len(offsetstr) >= 2) and re.match(r'\d\d', offsetstr):
+ self.minoffset = int(offsetstr[0:2])
+ offsetstr = offsetstr[2:]
+ if len(offsetstr) > 0:
+ raise ValueError()
+ if self.houroffset < 0:
+ self.minoffset *= -1
+
+ def utcoffset(self, dt):
+ return datetime.timedelta(hours=self.houroffset, minutes=self.minoffset)
+
+ def dst(self, dt):
+ return datetime.timedelta(0)
+
+ def tzname(self, dt):
+ return self.name
+
+
+def parse_generalized_time(timestr):
+ """Parses are Generalized Time string (as specified in X.680),
+ returning a datetime object. Generalized Times are stored inside
+ the krbPasswordExpiration attribute in LDAP.
+
+ This method doesn't attempt to be perfect wrt timezones. If python
+ can't be bothered to implement them, how can we..."""
+
+ if len(timestr) < 8:
+ return None
+ try:
+ date = timestr[:8]
+ time = timestr[8:]
+
+ year = int(date[:4])
+ month = int(date[4:6])
+ day = int(date[6:8])
+
+ hour = min = sec = msec = 0
+ tzone = None
+
+ if (len(time) >= 2) and re.match(r'\d', time[0]):
+ hour = int(time[:2])
+ time = time[2:]
+ if len(time) >= 2 and (time[0] == "," or time[0] == "."):
+ hour_fraction = "."
+ time = time[1:]
+ while (len(time) > 0) and re.match(r'\d', time[0]):
+ hour_fraction += time[0]
+ time = time[1:]
+ total_secs = int(float(hour_fraction) * 3600)
+ min, sec = divmod(total_secs, 60)
+
+ if (len(time) >= 2) and re.match(r'\d', time[0]):
+ min = int(time[:2])
+ time = time[2:]
+ if len(time) >= 2 and (time[0] == "," or time[0] == "."):
+ min_fraction = "."
+ time = time[1:]
+ while (len(time) > 0) and re.match(r'\d', time[0]):
+ min_fraction += time[0]
+ time = time[1:]
+ sec = int(float(min_fraction) * 60)
+
+ if (len(time) >= 2) and re.match(r'\d', time[0]):
+ sec = int(time[:2])
+ time = time[2:]
+ if len(time) >= 2 and (time[0] == "," or time[0] == "."):
+ sec_fraction = "."
+ time = time[1:]
+ while (len(time) > 0) and re.match(r'\d', time[0]):
+ sec_fraction += time[0]
+ time = time[1:]
+ msec = int(float(sec_fraction) * 1000000)
+
+ if (len(time) > 0):
+ tzone = GeneralizedTimeZone(time)
+
+ return datetime.datetime(year, month, day, hour, min, sec, msec, tzone)
+
+ except ValueError:
+ return None
+
+def ipa_generate_password():
+ rndpwd = ''
+ r = random.SystemRandom()
+ for x in range(12):
+ rndpwd += chr(r.randint(32,126))
+ return rndpwd
+
+
+def format_list(items, quote=None, page_width=80):
+ '''Format a list of items formatting them so they wrap to fit the
+ available width. The items will be sorted.
+
+ The items may optionally be quoted. The quote parameter may either be
+ a string, in which case it is added before and after the item. Or the
+ quote parameter may be a pair (either a tuple or list). In this case
+ quote[0] is left hand quote and quote[1] is the right hand quote.
+ '''
+ left_quote = right_quote = ''
+ num_items = len(items)
+ if not num_items: return ""
+
+ if quote is not None:
+ if type(quote) in StringTypes:
+ left_quote = right_quote = quote
+ elif type(quote) is TupleType or type(quote) is ListType:
+ left_quote = quote[0]
+ right_quote = quote[1]
+
+ max_len = max(map(len, items))
+ max_len += len(left_quote) + len(right_quote)
+ num_columns = (page_width + max_len) / (max_len+1)
+ num_rows = (num_items + num_columns - 1) / num_columns
+ items.sort()
+
+ rows = [''] * num_rows
+ i = row = col = 0
+
+ while i < num_items:
+ row = 0
+ if col == 0:
+ separator = ''
+ else:
+ separator = ' '
+
+ while i < num_items and row < num_rows:
+ rows[row] += "%s%*s" % (separator, -max_len, "%s%s%s" % (left_quote, items[i], right_quote))
+ i += 1
+ row += 1
+ col += 1
+ return '\n'.join(rows)
+
+key_value_re = re.compile("(\w+)\s*=\s*(([^\s'\\\"]+)|(?P<quote>['\\\"])((?P=quote)|(.*?[^\\\])(?P=quote)))")
+def parse_key_value_pairs(input):
+ ''' Given a string composed of key=value pairs parse it and return
+ a dict of the key/value pairs. Keys must be a word, a key must be followed
+ by an equal sign (=) and a value. The value may be a single word or may be
+ quoted. Quotes may be either single or double quotes, but must be balanced.
+ Inside the quoted text the same quote used to start the quoted value may be
+ used if it is escaped by preceding it with a backslash (\).
+ White space between the key, the equal sign, and the value is ignored.
+ Values are always strings. Empty values must be specified with an empty
+ quoted string, it's value after parsing will be an empty string.
+
+ Example: The string
+
+ arg0 = '' arg1 = 1 arg2='two' arg3 = "three's a crowd" arg4 = "this is a \" quote"
+
+ will produce
+
+ arg0= arg1=1
+ arg2=two
+ arg3=three's a crowd
+ arg4=this is a " quote
+ '''
+
+ kv_dict = {}
+ for match in key_value_re.finditer(input):
+ key = match.group(1)
+ quote = match.group('quote')
+ if match.group(5):
+ value = match.group(6)
+ if value is None: value = ''
+ value = re.sub('\\\%s' % quote, quote, value)
+ else:
+ value = match.group(2)
+ kv_dict[key] = value
+ return kv_dict
+
+def parse_items(text):
+ '''Given text with items separated by whitespace or comma, return a list of those items'''
+ split_re = re.compile('[ ,\t\n]+')
+ items = split_re.split(text)
+ for item in items[:]:
+ if not item: items.remove(item)
+ return items
+
+def read_pairs_file(filename):
+ comment_re = re.compile('#.*$', re.MULTILINE)
+ if filename == '-':
+ fd = sys.stdin
+ else:
+ fd = open(filename)
+ text = fd.read()
+ text = comment_re.sub('', text) # kill comments
+ pairs = parse_key_value_pairs(text)
+ if fd != sys.stdin: fd.close()
+ return pairs
+
+def read_items_file(filename):
+ comment_re = re.compile('#.*$', re.MULTILINE)
+ if filename == '-':
+ fd = sys.stdin
+ else:
+ fd = open(filename)
+ text = fd.read()
+ text = comment_re.sub('', text) # kill comments
+ items = parse_items(text)
+ if fd != sys.stdin: fd.close()
+ return items
+
+def user_input(prompt, default = None, allow_empty = True):
+ if default == None:
+ while True:
+ ret = raw_input("%s: " % prompt)
+ if allow_empty or ret.strip():
+ return ret
+
+ if isinstance(default, basestring):
+ while True:
+ ret = raw_input("%s [%s]: " % (prompt, default))
+ if not ret and (allow_empty or default):
+ return default
+ elif ret.strip():
+ return ret
+ if isinstance(default, bool):
+ if default:
+ choice = "yes"
+ else:
+ choice = "no"
+ while True:
+ ret = raw_input("%s [%s]: " % (prompt, choice))
+ if not ret:
+ return default
+ elif ret.lower()[0] == "y":
+ return True
+ elif ret.lower()[0] == "n":
+ return False
+ if isinstance(default, int):
+ while True:
+ try:
+ ret = raw_input("%s [%s]: " % (prompt, default))
+ if not ret:
+ return default
+ ret = int(ret)
+ except ValueError:
+ pass
+ else:
+ return ret
+
+def user_input_email(prompt, default = None, allow_empty = False):
+ if default != None and allow_empty:
+ prompt += " (enter \"none\" for empty)"
+ while True:
+ ret = user_input(prompt, default, allow_empty)
+ if allow_empty and ret.lower() == "none":
+ return ""
+ if ipavalidate.Email(ret, not allow_empty):
+ return ret.strip()
+
+def user_input_plain(prompt, default = None, allow_empty = True, allow_spaces = True):
+ while True:
+ ret = user_input(prompt, default, allow_empty)
+ if ipavalidate.Plain(ret, not allow_empty, allow_spaces):
+ return ret
+
+def user_input_path(prompt, default = None, allow_empty = True):
+ if default != None and allow_empty:
+ prompt += " (enter \"none\" for empty)"
+ while True:
+ ret = user_input(prompt, default, allow_empty)
+ if allow_empty and ret.lower() == "none":
+ return ""
+ if ipavalidate.Path(ret, not allow_empty):
+ return ret
+
+def user_input_name(prompt, default = None):
+ while True:
+ ret = user_input(prompt, default, False)
+ try:
+ ipaadminutil.check_name(ret)
+ return ret
+ except ValueError, e:
+ print prompt + " " + str(e)
+
+class AttributeValueCompleter:
+ '''
+ Gets input from the user in the form "lhs operator rhs"
+ TAB completes partial input.
+ lhs completes to a name in @lhs_names
+ The lhs is fully parsed if a lhs_delim delimiter is seen, then TAB will
+ complete to the operator and a default value.
+ Default values for a lhs value can specified as:
+ - a string, all lhs values will use this default
+ - a dict, the lhs value is looked up in the dict to return the default or None
+ - a function with a single arg, the lhs value, it returns the default or None
+
+ After creating the completer you must open it to set the terminal
+ up, Then get a line of input from the user by calling read_input()
+ which returns two values, the lhs and rhs, which might be None if
+ lhs or rhs was not parsed. After you are done getting input you
+ should close the completer to restore the terminal.
+
+ Example: (note this is essentially what the convenience function get_pairs() does)
+
+ This will allow the user to autocomplete foo & foobar, both have
+ defaults defined in a dict. In addition the foobar attribute must
+ be specified before the prompting loop will exit. Also, this
+ example show how to require that each attrbute entered by the user
+ is valid.
+
+ attrs = ['foo', 'foobar']
+ defaults = {'foo' : 'foo_default', 'foobar' : 'foobar_default'}
+ mandatory_attrs = ['foobar']
+
+ c = AttributeValueCompleter(attrs, defaults)
+ c.open()
+ mandatory_attrs_remaining = mandatory_attrs[:]
+
+ while True:
+ if mandatory_attrs_remaining:
+ attribute, value = c.read_input("Enter: ", mandatory_attrs_remaining[0])
+ try:
+ mandatory_attrs_remaining.remove(attribute)
+ except ValueError:
+ pass
+ else:
+ attribute, value = c.read_input("Enter: ")
+ if attribute is None:
+ # Are we done?
+ if mandatory_attrs_remaining:
+ print "ERROR, you must specify: %s" % (','.join(mandatory_attrs_remaining))
+ continue
+ else:
+ break
+ if attribute not in attrs:
+ print "ERROR: %s is not a valid attribute" % (attribute)
+ else:
+ print "got '%s' = '%s'" % (attribute, value)
+
+ c.close()
+ print "exiting..."
+ '''
+
+ def __init__(self, lhs_names, default_value=None, lhs_regexp=r'^\s*(?P<lhs>[^ =]+)', lhs_delims=' =',
+ operator='=', strip_rhs=True):
+ self.lhs_names = lhs_names
+ self.default_value = default_value
+ # lhs_regexp must have named group 'lhs' which returns the contents of the lhs
+ self.lhs_regexp = lhs_regexp
+ self.lhs_re = re.compile(self.lhs_regexp)
+ self.lhs_delims = lhs_delims
+ self.operator = operator
+ self.strip_rhs = strip_rhs
+ self.pairs = None
+ self._reset()
+
+ def _reset(self):
+ self.lhs = None
+ self.lhs_complete = False
+ self.operator_complete = False
+ self.rhs = None
+
+ def open(self):
+ # Save state
+ self.prev_completer = readline.get_completer()
+ self.prev_completer_delims = readline.get_completer_delims()
+
+ # Set up for ourself
+ readline.parse_and_bind("tab: complete")
+ readline.set_completer(self.complete)
+ readline.set_completer_delims(self.lhs_delims)
+
+ def close(self):
+ # Restore previous state
+ readline.set_completer_delims(self.prev_completer_delims)
+ readline.set_completer(self.prev_completer)
+
+ def parse_input(self):
+ '''We are looking for 3 tokens: <lhs,op,rhs>
+ Extract as much of each token as possible.
+ Set flags indicating if token is fully parsed.
+ '''
+ try:
+ self._reset()
+ buf_len = len(self.line_buffer)
+ pos = 0
+ lhs_match = self.lhs_re.search(self.line_buffer, pos)
+ if not lhs_match: return # no lhs content
+ self.lhs = lhs_match.group('lhs') # get lhs contents
+ pos = lhs_match.end('lhs') # new scanning position
+ if pos == buf_len: return # nothing after lhs, lhs incomplete
+ self.lhs_complete = True # something trails the lhs, lhs is complete
+ operator_beg = self.line_buffer.find(self.operator, pos) # locate operator
+ if operator_beg == -1: return # did not find the operator
+ self.operator_complete = True # operator fully parsed
+ operator_end = operator_beg + len(self.operator)
+ pos = operator_end # step over the operator
+ self.rhs = self.line_buffer[pos:]
+ except Exception, e:
+ traceback.print_exc()
+ print "Exception in %s.parse_input(): %s" % (self.__class__.__name__, e)
+
+ def get_default_value(self):
+ '''default_value can be a string, a dict, or a function.
+ If it's a string it's a global default for all attributes.
+ If it's a dict the default is looked up in the dict index by attribute.
+ If it's a function, the function is called with 1 parameter, the attribute
+ and it should return the default value for the attriubte or None'''
+
+ if not self.lhs_complete: raise ValueError("attribute not parsed")
+
+ # If the user previously provided a value let that override the supplied default
+ if self.pairs is not None:
+ prev_value = self.pairs.get(self.lhs)
+ if prev_value is not None: return prev_value
+
+ # No previous user provided value, query for a default
+ default_value_type = type(self.default_value)
+ if default_value_type is DictType:
+ return self.default_value.get(self.lhs, None)
+ elif default_value_type is FunctionType:
+ return self.default_value(self.lhs)
+ elif default_value_type is StringsType:
+ return self.default_value
+ else:
+ return None
+
+ def get_lhs_completions(self, text):
+ if text:
+ self.completions = [lhs for lhs in self.lhs_names if lhs.startswith(text)]
+ else:
+ self.completions = self.lhs_names
+
+ def complete(self, text, state):
+ self.line_buffer= readline.get_line_buffer()
+ self.parse_input()
+ if not self.lhs_complete:
+ # lhs is not complete, set up to complete the lhs
+ if state == 0:
+ beg = readline.get_begidx()
+ end = readline.get_endidx()
+ self.get_lhs_completions(self.line_buffer[beg:end])
+ if state >= len(self.completions): return None
+ return self.completions[state]
+
+
+ elif not self.operator_complete:
+ # lhs is complete, but the operator is not so we complete
+ # by inserting the operator manually.
+ # Also try to complete the default value at this time.
+ readline.insert_text('%s ' % self.operator)
+ default_value = self.get_default_value()
+ if default_value is not None:
+ readline.insert_text(default_value)
+ readline.redisplay()
+ return None
+ else:
+ # lhs and operator are complete, if the the rhs is blank
+ # (either empty or only only whitespace) then attempt
+ # to complete by inserting the default value, otherwise
+ # there is nothing we can complete to so we're done.
+ if self.rhs.strip():
+ return None
+ default_value = self.get_default_value()
+ if default_value is not None:
+ readline.insert_text(default_value)
+ readline.redisplay()
+ return None
+
+ def pre_input_hook(self):
+ readline.insert_text('%s %s ' % (self.initial_lhs, self.operator))
+ readline.redisplay()
+
+ def read_input(self, prompt, initial_lhs=None):
+ self.initial_lhs = initial_lhs
+ try:
+ self._reset()
+ if initial_lhs is None:
+ readline.set_pre_input_hook(None)
+ else:
+ readline.set_pre_input_hook(self.pre_input_hook)
+ self.line_buffer = raw_input(prompt).strip()
+ self.parse_input()
+ if self.strip_rhs and self.rhs is not None:
+ return self.lhs, self.rhs.strip()
+ else:
+ return self.lhs, self.rhs
+ except EOFError:
+ return None, None
+
+ def get_pairs(self, prompt, mandatory_attrs=None, validate_callback=None, must_match=True, value_required=True):
+ self.pairs = {}
+ if mandatory_attrs:
+ mandatory_attrs_remaining = mandatory_attrs[:]
+ else:
+ mandatory_attrs_remaining = []
+
+ print "Enter name = value"
+ print "Press <ENTER> to accept, a blank line terminates input"
+ print "Pressing <TAB> will auto completes name, assignment, and value"
+ print
+ while True:
+ if mandatory_attrs_remaining:
+ attribute, value = self.read_input(prompt, mandatory_attrs_remaining[0])
+ else:
+ attribute, value = self.read_input(prompt)
+ if attribute is None:
+ # Are we done?
+ if mandatory_attrs_remaining:
+ print "ERROR, you must specify: %s" % (','.join(mandatory_attrs_remaining))
+ continue
+ else:
+ break
+ if value is None:
+ if value_required:
+ print "ERROR: you must specify a value for %s" % attribute
+ continue
+ else:
+ if must_match and attribute not in self.lhs_names:
+ print "ERROR: %s is not a valid name" % (attribute)
+ continue
+ if validate_callback is not None:
+ if not validate_callback(attribute, value):
+ print "ERROR: %s is not valid for %s" % (value, attribute)
+ continue
+ try:
+ mandatory_attrs_remaining.remove(attribute)
+ except ValueError:
+ pass
+
+ self.pairs[attribute] = value
+ return self.pairs
+
+class ItemCompleter:
+ '''
+ Prompts the user for items in a list of items with auto completion.
+ TAB completes partial input.
+ More than one item can be specifed during input, whitespace and/or comma's seperate.
+ Example:
+
+ possible_items = ['foo', 'bar']
+ c = ItemCompleter(possible_items)
+ c.open()
+ # Use read_input() to limit input to a single carriage return (e.g. <ENTER>)
+ #items = c.read_input("Enter: ")
+ # Use get_items to iterate until a blank line is entered.
+ items = c.get_items("Enter: ")
+ c.close()
+ print "items=%s" % (items)
+
+ '''
+
+ def __init__(self, items):
+ self.items = items
+ self.initial_input = None
+ self.item_delims = ' \t,'
+ self.split_re = re.compile('[%s]+' % self.item_delims)
+
+ def open(self):
+ # Save state
+ self.prev_completer = readline.get_completer()
+ self.prev_completer_delims = readline.get_completer_delims()
+
+ # Set up for ourself
+ readline.parse_and_bind("tab: complete")
+ readline.set_completer(self.complete)
+ readline.set_completer_delims(self.item_delims)
+
+ def close(self):
+ # Restore previous state
+ readline.set_completer_delims(self.prev_completer_delims)
+ readline.set_completer(self.prev_completer)
+
+ def get_item_completions(self, text):
+ if text:
+ self.completions = [lhs for lhs in self.items if lhs.startswith(text)]
+ else:
+ self.completions = self.items
+
+ def complete(self, text, state):
+ self.line_buffer= readline.get_line_buffer()
+ if state == 0:
+ beg = readline.get_begidx()
+ end = readline.get_endidx()
+ self.get_item_completions(self.line_buffer[beg:end])
+ if state >= len(self.completions): return None
+ return self.completions[state]
+
+ def pre_input_hook(self):
+ readline.insert_text('%s %s ' % (self.initial_input, self.operator))
+ readline.redisplay()
+
+ def read_input(self, prompt, initial_input=None):
+ items = []
+
+ self.initial_input = initial_input
+ try:
+ if initial_input is None:
+ readline.set_pre_input_hook(None)
+ else:
+ readline.set_pre_input_hook(self.pre_input_hook)
+ self.line_buffer = raw_input(prompt).strip()
+ items = self.split_re.split(self.line_buffer)
+ for item in items[:]:
+ if not item: items.remove(item)
+ return items
+ except EOFError:
+ return items
+
+ def get_items(self, prompt, must_match=True):
+ items = []
+
+ print "Enter name [name ...]"
+ print "Press <ENTER> to accept, blank line or control-D terminates input"
+ print "Pressing <TAB> auto completes name"
+ print
+ while True:
+ new_items = self.read_input(prompt)
+ if not new_items: break
+ for item in new_items:
+ if must_match:
+ if item not in self.items:
+ print "ERROR: %s is not valid" % (item)
+ continue
+ if item in items: continue
+ items.append(item)
+
+ return items
+
+def get_gsserror(e):
+ """A GSSError exception looks differently in python 2.4 than it does
+ in python 2.5, deal with it."""
+
+ try:
+ primary = e[0]
+ secondary = e[1]
+ except:
+ primary = e[0][0]
+ secondary = e[0][1]
+
+ return (primary[0], secondary[0])
diff --git a/ipa-python/ipavalidate.py b/ipa-python/ipavalidate.py
new file mode 100644
index 00000000..63e0a761
--- /dev/null
+++ b/ipa-python/ipavalidate.py
@@ -0,0 +1,137 @@
+# Authors: Rob Crittenden <rcritten@redhat.com>
+#
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+import re
+
+def Email(mail, notEmpty=True):
+ """Do some basic validation of an e-mail address.
+ Return True if ok
+ Return False if not
+
+ If notEmpty is True the this will return an error if the field
+ is "" or None.
+ """
+ usernameRE = re.compile(r"^[^ \t\n\r@<>()]+$", re.I)
+ domainRE = re.compile(r"^[a-z0-9][a-z0-9\.\-_]*\.[a-z]+$", re.I)
+
+ if not mail or mail is None:
+ if notEmpty is True:
+ return False
+ else:
+ return True
+
+ mail = mail.strip()
+ s = mail.split('@', 1)
+ try:
+ username, domain=s
+ except ValueError:
+ return False
+ if not usernameRE.search(username):
+ return False
+ if not domainRE.search(domain):
+ return False
+
+ return True
+
+def Plain(text, notEmpty=False, allowSpaces=True):
+ """Do some basic validation of a plain text field
+ Return True if ok
+ Return False if not
+
+ If notEmpty is True the this will return an error if the field
+ is "" or None.
+ """
+ if (text is None) or (not text.strip()):
+ if notEmpty is True:
+ return False
+ else:
+ return True
+
+ if allowSpaces:
+ textRE = re.compile(r"^[a-zA-Z_\-0-9\'\ ]*$")
+ else:
+ textRE = re.compile(r"^[a-zA-Z_\-0-9\']*$")
+ if not textRE.search(text):
+ return False
+
+ return True
+
+def String(text, notEmpty=False):
+ """A string type. This is much looser in what it allows than plain"""
+
+ if text is None or not text.strip():
+ if notEmpty is True:
+ return False
+ else:
+ return True
+
+ return True
+
+def Path(text, notEmpty=False):
+ """Do some basic validation of a path
+ Return True if ok
+ Return False if not
+
+ If notEmpty is True the this will return an error if the field
+ is "" or None.
+ """
+ textRE = re.compile(r"^[a-zA-Z_\-0-9\\ \.\/\\:]*$")
+
+ if not text and notEmpty is True:
+ return False
+
+ if text is None:
+ if notEmpty is True:
+ return False
+ else:
+ return True
+
+ if not textRE.search(text):
+ return False
+
+ return True
+
+def GoodName(text, notEmpty=False):
+ """From shadow-utils:
+
+ User/group names must match gnu e-regex:
+ [a-zA-Z0-9_.][a-zA-Z0-9_.-]{0,30}[a-zA-Z0-9_.$-]?
+
+ as a non-POSIX, extension, allow "$" as the last char for
+ sake of Samba 3.x "add machine script"
+
+ Return True if ok
+ Return False if not
+ """
+ textRE = re.compile(r"^[a-zA-Z0-9_.][a-zA-Z0-9_.-]{0,30}[a-zA-Z0-9_.$-]?$")
+
+ if not text and notEmpty is True:
+ return False
+
+ if text is None:
+ if notEmpty is True:
+ return False
+ else:
+ return True
+
+ m = textRE.match(text)
+ if not m or text != m.group(0):
+ return False
+
+ return True
diff --git a/ipa-python/krbtransport.py b/ipa-python/krbtransport.py
new file mode 100644
index 00000000..3d13fed0
--- /dev/null
+++ b/ipa-python/krbtransport.py
@@ -0,0 +1,51 @@
+# Authors: Rob Crittenden <rcritten@redhat.com>
+#
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+import httplib
+import xmlrpclib
+import kerberos
+
+class KerbTransport(xmlrpclib.SafeTransport):
+ """Handles Kerberos Negotiation authentication to an XML-RPC server."""
+
+ def get_host_info(self, host):
+
+ host, extra_headers, x509 = xmlrpclib.Transport.get_host_info(self, host)
+
+ # Set the remote host principal
+ h = host
+ hostinfo = h.split(':')
+ service = "HTTP@" + hostinfo[0]
+
+ try:
+ rc, vc = kerberos.authGSSClientInit(service);
+ except kerberos.GSSError, e:
+ raise kerberos.GSSError(e)
+
+ try:
+ kerberos.authGSSClientStep(vc, "");
+ except kerberos.GSSError, e:
+ raise kerberos.GSSError(e)
+
+ extra_headers = [
+ ("Authorization", "negotiate %s" % kerberos.authGSSClientResponse(vc) )
+ ]
+
+ return host, extra_headers, x509
+
diff --git a/ipa-python/radius_util.py b/ipa-python/radius_util.py
new file mode 100644
index 00000000..3d2e83e1
--- /dev/null
+++ b/ipa-python/radius_util.py
@@ -0,0 +1,366 @@
+# Authors: John Dennis <jdennis@redhat.com>
+#
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+import sys
+import os
+import re
+import ldap
+import getpass
+import ldap.filter
+
+from ipa import ipautil
+from ipa.entity import Entity
+import ipa.ipavalidate as ipavalidate
+
+
+__all__ = [
+ 'RADIUS_PKG_NAME',
+ 'RADIUS_PKG_CONFIG_DIR',
+ 'RADIUS_SERVICE_NAME',
+ 'RADIUS_USER',
+ 'RADIUS_IPA_KEYTAB_FILEPATH',
+ 'RADIUS_LDAP_ATTR_MAP_FILEPATH',
+ 'RADIUSD_CONF_FILEPATH',
+ 'RADIUSD_CONF_TEMPLATE_FILEPATH',
+ 'RADIUSD',
+
+ 'RadiusClient',
+ 'RadiusProfile',
+
+ 'clients_container',
+ 'radius_clients_basedn',
+ 'radius_client_filter',
+ 'radius_client_dn',
+
+ 'profiles_container',
+ 'radius_profiles_basedn',
+ 'radius_profile_filter',
+ 'radius_profile_dn',
+
+ 'radius_client_ldap_attr_to_radius_attr',
+ 'radius_client_attr_to_ldap_attr',
+
+ 'radius_profile_ldap_attr_to_radius_attr',
+ 'radius_profile_attr_to_ldap_attr',
+
+ 'get_secret',
+ 'validate_ip_addr',
+ 'validate_secret',
+ 'validate_name',
+ 'validate_nastype',
+ 'validate_desc',
+ 'validate',
+ ]
+
+#------------------------------------------------------------------------------
+
+RADIUS_PKG_NAME = 'freeradius'
+RADIUS_PKG_CONFIG_DIR = '/etc/raddb'
+
+RADIUS_SERVICE_NAME = 'radius'
+RADIUS_USER = 'radiusd'
+
+RADIUS_IPA_KEYTAB_FILEPATH = os.path.join(RADIUS_PKG_CONFIG_DIR, 'ipa.keytab')
+RADIUS_LDAP_ATTR_MAP_FILEPATH = os.path.join(RADIUS_PKG_CONFIG_DIR, 'ldap.attrmap')
+RADIUSD_CONF_FILEPATH = os.path.join(RADIUS_PKG_CONFIG_DIR, 'radiusd.conf')
+RADIUSD_CONF_TEMPLATE_FILEPATH = os.path.join(ipautil.PLUGINS_SHARE_DIR, 'radius.radiusd.conf.template')
+
+RADIUSD = '/usr/sbin/radiusd'
+
+#------------------------------------------------------------------------------
+
+dotted_octet_re = re.compile(r"^(\d+)\.(\d+)\.(\d+)\.(\d+)(/(\d+))?$")
+dns_re = re.compile(r"^[a-zA-Z][a-zA-Z0-9.-]+$")
+# secret, name, nastype all have 31 char max in freeRADIUS, max ip address len is 255
+valid_secret_len = (1,31)
+valid_name_len = (1,31)
+valid_nastype_len = (1,31)
+valid_ip_addr_len = (1,255)
+
+valid_ip_addr_msg = '''\
+IP address must be either a DNS name (letters,digits,dot,hyphen, beginning with
+a letter),or a dotted octet followed by an optional mask (e.g 192.168.1.0/24)'''
+
+valid_desc_msg = "Description must text string"
+
+#------------------------------------------------------------------------------
+
+class RadiusClient(Entity):
+
+ def __init2__(self):
+ pass
+
+class RadiusProfile(Entity):
+
+ def __init2__(self):
+ pass
+
+
+#------------------------------------------------------------------------------
+
+def reverse_map_dict(src_dict):
+ reverse_dict = {}
+
+ for k,v in src_dict.items():
+ if reverse_dict.has_key(v):
+ raise ValueError("reverse_map_dict: collision on (%s) with values (%s),(%s)" % \
+ v, reverse_dict[v], src_dict[k])
+ reverse_dict[v] = k
+ return reverse_dict
+
+#------------------------------------------------------------------------------
+
+radius_client_ldap_attr_to_radius_attr = ipautil.CIDict({
+ 'radiusClientIPAddress' : 'Client-IP-Address',
+ 'radiusClientSecret' : 'Secret',
+ 'radiusClientNASType' : 'NAS-Type',
+ 'radiusClientShortName' : 'Name',
+ 'description' : 'Description',
+ })
+
+radius_client_attr_to_ldap_attr = reverse_map_dict(radius_client_ldap_attr_to_radius_attr)
+
+#------------------------------------------------------------------------------
+
+radius_profile_ldap_attr_to_radius_attr = ipautil.CIDict({
+ 'uid' : 'UID',
+ 'radiusArapFeatures' : 'Arap-Features',
+ 'radiusArapSecurity' : 'Arap-Security',
+ 'radiusArapZoneAccess' : 'Arap-Zone-Access',
+ 'radiusAuthType' : 'Auth-Type',
+ 'radiusCallbackId' : 'Callback-Id',
+ 'radiusCallbackNumber' : 'Callback-Number',
+ 'radiusCalledStationId' : 'Called-Station-Id',
+ 'radiusCallingStationId' : 'Calling-Station-Id',
+ 'radiusClass' : 'Class',
+ 'radiusClientIPAddress' : 'Client-IP-Address',
+ 'radiusExpiration' : 'Expiration',
+ 'radiusFilterId' : 'Filter-Id',
+ 'radiusFramedAppleTalkLink' : 'Framed-AppleTalk-Link',
+ 'radiusFramedAppleTalkNetwork' : 'Framed-AppleTalk-Network',
+ 'radiusFramedAppleTalkZone' : 'Framed-AppleTalk-Zone',
+ 'radiusFramedCompression' : 'Framed-Compression',
+ 'radiusFramedIPAddress' : 'Framed-IP-Address',
+ 'radiusFramedIPNetmask' : 'Framed-IP-Netmask',
+ 'radiusFramedIPXNetwork' : 'Framed-IPX-Network',
+ 'radiusFramedMTU' : 'Framed-MTU',
+ 'radiusFramedProtocol' : 'Framed-Protocol',
+ 'radiusFramedRoute' : 'Framed-Route',
+ 'radiusFramedRouting' : 'Framed-Routing',
+ 'radiusGroupName' : 'Group-Name',
+ 'radiusHint' : 'Hint',
+ 'radiusHuntgroupName' : 'Huntgroup-Name',
+ 'radiusIdleTimeout' : 'Idle-Timeout',
+ 'radiusLoginIPHost' : 'Login-IP-Host',
+ 'radiusLoginLATGroup' : 'Login-LAT-Group',
+ 'radiusLoginLATNode' : 'Login-LAT-Node',
+ 'radiusLoginLATPort' : 'Login-LAT-Port',
+ 'radiusLoginLATService' : 'Login-LAT-Service',
+ 'radiusLoginService' : 'Login-Service',
+ 'radiusLoginTCPPort' : 'Login-TCP-Port',
+ 'radiusLoginTime' : 'Login-Time',
+ 'radiusNASIpAddress' : 'NAS-IP-Address',
+ 'radiusPasswordRetry' : 'Password-Retry',
+ 'radiusPortLimit' : 'Port-Limit',
+ 'radiusProfileDn' : 'Profile-Dn',
+ 'radiusPrompt' : 'Prompt',
+ 'radiusProxyToRealm' : 'Proxy-To-Realm',
+ 'radiusRealm' : 'Realm',
+ 'radiusReplicateToRealm' : 'Replicate-To-Realm',
+ 'radiusReplyMessage' : 'Reply-Message',
+ 'radiusServiceType' : 'Service-Type',
+ 'radiusSessionTimeout' : 'Session-Timeout',
+ 'radiusSimultaneousUse' : 'Simultaneous-Use',
+ 'radiusStripUserName' : 'Strip-User-Name',
+ 'radiusTerminationAction' : 'Termination-Action',
+ 'radiusTunnelAssignmentId' : 'Tunnel-Assignment-Id',
+ 'radiusTunnelClientEndpoint' : 'Tunnel-Client-Endpoint',
+ 'radiusTunnelMediumType' : 'Tunnel-Medium-Type',
+ 'radiusTunnelPassword' : 'Tunnel-Password',
+ 'radiusTunnelPreference' : 'Tunnel-Preference',
+ 'radiusTunnelPrivateGroupId' : 'Tunnel-Private-Group-Id',
+ 'radiusTunnelServerEndpoint' : 'Tunnel-Server-Endpoint',
+ 'radiusTunnelType' : 'Tunnel-Type',
+ 'radiusUserCategory' : 'User-Category',
+ 'radiusVSA' : 'VSA',
+})
+
+radius_profile_attr_to_ldap_attr = reverse_map_dict(radius_profile_ldap_attr_to_radius_attr)
+
+#------------------------------------------------------------------------------
+
+clients_container = 'cn=clients,cn=radius'
+
+def radius_clients_basedn(container, suffix):
+ if container is None: container = clients_container
+ return '%s,%s' % (container, suffix)
+
+def radius_client_filter(ip_addr):
+ return "(&(radiusClientIPAddress=%s)(objectclass=radiusClientProfile))" % \
+ ldap.filter.escape_filter_chars(ip_addr)
+
+def radius_client_dn(client, container, suffix):
+ if container is None: container = clients_container
+ return 'radiusClientIPAddress=%s,%s,%s' % (ldap.dn.escape_dn_chars(client), container, suffix)
+
+# --
+
+profiles_container = 'cn=profiles,cn=radius'
+
+def radius_profiles_basedn(container, suffix):
+ if container is None: container = profiles_container
+ return '%s,%s' % (container, suffix)
+
+def radius_profile_filter(uid):
+ return "(&(uid=%s)(objectclass=radiusprofile))" % \
+ ldap.filter.escape_filter_chars(uid)
+
+def radius_profile_dn(uid, container, suffix):
+ if container is None: container = profiles_container
+ return 'uid=%s,%s,%s' % (ldap.dn.escape_dn_chars(uid), container, suffix)
+
+
+#------------------------------------------------------------------------------
+
+def get_ldap_attr_translations():
+ comment_re = re.compile('#.*$')
+ radius_attr_to_ldap_attr = {}
+ ldap_attr_to_radius_attr = {}
+ try:
+ f = open(LDAP_ATTR_MAP_FILEPATH)
+ for line in f.readlines():
+ line = comment_re.sub('', line).strip()
+ if not line: continue
+ attr_type, radius_attr, ldap_attr = line.split()
+ print 'type="%s" radius="%s" ldap="%s"' % (attr_type, radius_attr, ldap_attr)
+ radius_attr_to_ldap_attr[radius_attr] = {'ldap_attr':ldap_attr, 'attr_type':attr_type}
+ ldap_attr_to_radius_attr[ldap_attr] = {'radius_attr':radius_attr, 'attr_type':attr_type}
+ f.close()
+ except Exception, e:
+ logging.error('cold not read radius ldap attribute map file (%s): %s', LDAP_ATTR_MAP_FILEPATH, e)
+ pass # FIXME
+
+ #for k,v in radius_attr_to_ldap_attr.items():
+ # print '%s --> %s' % (k,v)
+ #for k,v in ldap_attr_to_radius_attr.items():
+ # print '%s --> %s' % (k,v)
+
+def get_secret():
+ valid = False
+ while (not valid):
+ secret = getpass.getpass("Enter Secret: ")
+ confirm = getpass.getpass("Confirm Secret: ")
+ if (secret != confirm):
+ print "Secrets do not match"
+ continue
+ valid = True
+ return secret
+
+#------------------------------------------------------------------------------
+
+def valid_ip_addr(text):
+
+ # is it a dotted octet? If so there should be 4 integers seperated
+ # by a dot and each integer should be between 0 and 255
+ # there may be an optional mask preceded by a slash (e.g. 1.2.3.4/24)
+ match = dotted_octet_re.search(text)
+ if match:
+ # dotted octet notation
+ i = 1
+ while i <= 4:
+ octet = int(match.group(i))
+ if octet > 255: return False
+ i += 1
+ if match.group(5):
+ mask = int(match.group(6))
+ if mask <= 32:
+ return True
+ else:
+ return False
+ return True
+ else:
+ # DNS name, can contain letters, numbers, dot and hypen, must start with a letter
+ if dns_re.search(text): return True
+ return False
+
+def validate_length(value, limits):
+ length = len(value)
+ if length < limits[0] or length > limits[1]:
+ return False
+ return True
+
+def valid_length_msg(name, limits):
+ return "%s length must be at least %d and not more than %d" % (name, limits[0], limits[1])
+
+def err_msg(variable, variable_name=None):
+ if variable_name is None: variable_name = 'value'
+ print "ERROR: %s = %s" % (variable_name, variable)
+
+#------------------------------------------------------------------------------
+
+def validate_ip_addr(ip_addr, variable_name=None):
+ if not validate_length(ip_addr, valid_ip_addr_len):
+ err_msg(ip_addr, variable_name)
+ print valid_length_msg('ip address', valid_ip_addr_len)
+ return False
+ if not valid_ip_addr(ip_addr):
+ err_msg(ip_addr, variable_name)
+ print valid_ip_addr_msg
+ return False
+ return True
+
+def validate_secret(secret, variable_name=None):
+ if not validate_length(secret, valid_secret_len):
+ err_msg(secret, variable_name)
+ print valid_length_msg('secret', valid_secret_len)
+ return False
+ return True
+
+def validate_name(name, variable_name=None):
+ if not validate_length(name, valid_name_len):
+ err_msg(name, variable_name)
+ print valid_length_msg('name', valid_name_len)
+ return False
+ return True
+
+def validate_nastype(nastype, variable_name=None):
+ if not validate_length(nastype, valid_nastype_len):
+ err_msg(nastype, variable_name)
+ print valid_length_msg('NAS Type', valid_nastype_len)
+ return False
+ return True
+
+def validate_desc(desc, variable_name=None):
+ if not ipavalidate.Plain(desc):
+ print valid_desc_msg
+ return False
+ return True
+
+def validate(attribute, value):
+ if attribute == 'Client-IP-Address':
+ return validate_ip_addr(value, attribute)
+ if attribute == 'Secret':
+ return validate_secret(value, attribute)
+ if attribute == 'NAS-Type':
+ return validate_nastype(value, attribute)
+ if attribute == 'Name':
+ return validate_name(value, attribute)
+ if attribute == 'Description':
+ return validate_desc(value, attribute)
+ return True
diff --git a/ipa-python/rpcclient.py b/ipa-python/rpcclient.py
new file mode 100644
index 00000000..a8001767
--- /dev/null
+++ b/ipa-python/rpcclient.py
@@ -0,0 +1,906 @@
+# Authors: Rob Crittenden <rcritten@redhat.com>
+#
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+import xmlrpclib
+import socket
+import config
+import errno
+from krbtransport import KerbTransport
+from kerberos import GSSError
+from ipa import ipaerror, ipautil
+from ipa import config
+
+# Some errors to catch
+# http://cvs.fedora.redhat.com/viewcvs/ldapserver/ldap/servers/plugins/pam_passthru/README?root=dirsec&rev=1.6&view=auto
+
+class RPCClient:
+
+ def __init__(self, verbose=False):
+ self.server = None
+ self.verbose = verbose
+ config.init_config()
+
+ def server_url(self, server):
+ """Build the XML-RPC server URL from our configuration"""
+ url = "https://" + server + "/ipa/xml"
+ if self.verbose:
+ print "Connecting to IPA server: %s" % url
+ return url
+
+ def setup_server(self):
+ """Create our XML-RPC server connection using kerberos
+ authentication"""
+ if not self.server:
+ serverlist = config.config.get_server()
+
+ # Try each server until we succeed or run out of servers to try
+ # Guaranteed by ipa.config to have at least 1 in the list
+ for s in serverlist:
+ try:
+ self.server = s
+ remote = xmlrpclib.ServerProxy(self.server_url(s), KerbTransport())
+ result = remote.ping()
+ break
+ except socket.error, e:
+ if (e[0] == errno.ECONNREFUSED) or (e[0] == errno.ECONNREFUSED) or (e[0] == errno.EHOSTDOWN) or (e[0] == errno.EHOSTUNREACH):
+ continue
+ else:
+ raise e
+ except GSSError:
+ continue
+
+ return xmlrpclib.ServerProxy(self.server_url(self.server), KerbTransport(), verbose=self.verbose)
+
+# Higher-level API
+
+ def get_aci_entry(self, sattrs=None):
+ """Returns the entry containing access control ACIs."""
+ server = self.setup_server()
+ if sattrs is None:
+ sattrs = "__NONE__"
+ try:
+ result = server.get_aci_entry(sattrs)
+ except xmlrpclib.Fault, fault:
+ raise ipaerror.gen_exception(fault.faultCode, fault.faultString)
+ except socket.error, (value, msg):
+ raise xmlrpclib.Fault(value, msg)
+
+ return ipautil.unwrap_binary_data(result)
+
+
+# General searches
+
+ def get_entry_by_dn(self,dn,sattrs=None):
+ """Get a specific entry. If sattrs is not None then only those
+ attributes will be returned, otherwise all available
+ attributes are returned. The result is a dict."""
+ server = self.setup_server()
+ if sattrs is None:
+ sattrs = "__NONE__"
+ try:
+ result = server.get_entry_by_dn(dn, sattrs)
+ except xmlrpclib.Fault, fault:
+ raise ipaerror.gen_exception(fault.faultCode, fault.faultString)
+ except socket.error, (value, msg):
+ raise xmlrpclib.Fault(value, msg)
+
+ return ipautil.unwrap_binary_data(result)
+
+ def get_entry_by_cn(self,cn,sattrs=None):
+ """Get a specific entry by cn. If sattrs is not None then only those
+ attributes will be returned, otherwise all available
+ attributes are returned. The result is a dict."""
+ server = self.setup_server()
+ if sattrs is None:
+ sattrs = "__NONE__"
+ try:
+ result = server.get_entry_by_cn(cn, sattrs)
+ except xmlrpclib.Fault, fault:
+ raise ipaerror.gen_exception(fault.faultCode, fault.faultString)
+ except socket.error, (value, msg):
+ raise xmlrpclib.Fault(value, msg)
+
+ return ipautil.unwrap_binary_data(result)
+
+ def update_entry(self,oldentry,newentry):
+ """Update an existing entry. oldentry and newentry are dicts of attributes"""
+ server = self.setup_server()
+
+ try:
+ result = server.update_entry(ipautil.wrap_binary_data(oldentry),
+ ipautil.wrap_binary_data(newentry))
+ except xmlrpclib.Fault, fault:
+ raise ipaerror.gen_exception(fault.faultCode, fault.faultString)
+ except socket.error, (value, msg):
+ raise xmlrpclib.Fault(value, msg)
+
+ return ipautil.unwrap_binary_data(result)
+
+
+# User support
+
+ def get_user_by_uid(self,uid,sattrs=None):
+ """Get a specific user. If sattrs is not None then only those
+ attributes will be returned, otherwise all available
+ attributes are returned. The result is a dict."""
+ server = self.setup_server()
+ if sattrs is None:
+ sattrs = "__NONE__"
+ try:
+ result = server.get_user_by_uid(uid, sattrs)
+ except xmlrpclib.Fault, fault:
+ raise ipaerror.gen_exception(fault.faultCode, fault.faultString)
+ except socket.error, (value, msg):
+ raise xmlrpclib.Fault(value, msg)
+
+ return ipautil.unwrap_binary_data(result)
+
+ def get_user_by_principal(self,principal,sattrs=None):
+ """Get a specific user. If sattrs is not None then only those
+ attributes will be returned, otherwise all available
+ attributes are returned. The result is a dict."""
+ server = self.setup_server()
+ if sattrs is None:
+ sattrs = "__NONE__"
+ try:
+ result = server.get_user_by_principal(principal, sattrs)
+ except xmlrpclib.Fault, fault:
+ raise ipaerror.gen_exception(fault.faultCode, fault.faultString)
+ except socket.error, (value, msg):
+ raise xmlrpclib.Fault(value, msg)
+
+ return ipautil.unwrap_binary_data(result)
+
+ def get_user_by_email(self,email,sattrs=None):
+ """Get a specific user's entry. Return as a dict of values.
+ Multi-valued fields are represented as lists. The result is a
+ dict.
+ """
+ server = self.setup_server()
+ if sattrs is None:
+ sattrs = "__NONE__"
+ try:
+ result = server.get_user_by_email(email, sattrs)
+ except xmlrpclib.Fault, fault:
+ raise ipaerror.gen_exception(fault.faultCode, fault.faultString)
+ except socket.error, (value, msg):
+ raise xmlrpclib.Fault(value, msg)
+
+ return ipautil.unwrap_binary_data(result)
+
+ def get_users_by_manager(self,manager_dn,sattrs=None):
+ """Gets the users that report to a manager.
+ If sattrs is not None then only those
+ attributes will be returned, otherwise all available
+ attributes are returned. The result is a list of dicts."""
+ server = self.setup_server()
+ if sattrs is None:
+ sattrs = "__NONE__"
+ try:
+ result = server.get_users_by_manager(manager_dn, sattrs)
+ except xmlrpclib.Fault, fault:
+ raise ipaerror.gen_exception(fault.faultCode, fault.faultString)
+ except socket.error, (value, msg):
+ raise xmlrpclib.Fault(value, msg)
+
+ return ipautil.unwrap_binary_data(result)
+
+ def add_user(self,user,user_container=None):
+ """Add a new user. Takes as input a dict where the key is the
+ attribute name and the value is either a string or in the case
+ of a multi-valued field a list of values"""
+ server = self.setup_server()
+
+ if user_container is None:
+ user_container = "__NONE__"
+
+ try:
+ result = server.add_user(ipautil.wrap_binary_data(user),
+ user_container)
+ except xmlrpclib.Fault, fault:
+ raise ipaerror.gen_exception(fault.faultCode, fault.faultString)
+ except socket.error, (value, msg):
+ raise xmlrpclib.Fault(value, msg)
+
+ return ipautil.unwrap_binary_data(result)
+
+ def get_custom_fields(self):
+ """Get custom user fields."""
+ server = self.setup_server()
+
+ try:
+ result = server.get_custom_fields()
+ except xmlrpclib.Fault, fault:
+ raise ipaerror.gen_exception(fault.faultCode, fault.faultString)
+ except socket.error, (value, msg):
+ raise xmlrpclib.Fault(value, msg)
+
+ return ipautil.unwrap_binary_data(result)
+
+ def set_custom_fields(self, schema):
+ """Set custom user fields."""
+ server = self.setup_server()
+
+ try:
+ result = server.set_custom_fields(schema)
+ except xmlrpclib.Fault, fault:
+ raise ipaerror.gen_exception(fault.faultCode, fault.faultString)
+ except socket.error, (value, msg):
+ raise xmlrpclib.Fault(value, msg)
+
+ return ipautil.unwrap_binary_data(result)
+
+ def get_all_users (self):
+ """Return a list containing a dict for each existing user."""
+
+ server = self.setup_server()
+ try:
+ result = server.get_all_users()
+ except xmlrpclib.Fault, fault:
+ raise ipaerror.gen_exception(fault.faultCode, fault.faultString)
+ except socket.error, (value, msg):
+ raise xmlrpclib.Fault(value, msg)
+
+ return ipautil.unwrap_binary_data(result)
+
+ def find_users (self, criteria, sattrs=None, sizelimit=-1, timelimit=-1):
+ """Return a list: counter followed by a dict for each user that
+ matches the criteria. If the results are truncated, counter will
+ be set to -1"""
+
+ server = self.setup_server()
+ try:
+ # None values are not allowed in XML-RPC
+ if sattrs is None:
+ sattrs = "__NONE__"
+ result = server.find_users(criteria, sattrs, sizelimit, timelimit)
+ except xmlrpclib.Fault, fault:
+ raise ipaerror.gen_exception(fault.faultCode, fault.faultString)
+ except socket.error, (value, msg):
+ raise xmlrpclib.Fault(value, msg)
+
+ return ipautil.unwrap_binary_data(result)
+
+ def update_user(self,olduser,newuser):
+ """Update an existing user. olduser and newuser are dicts of attributes"""
+ server = self.setup_server()
+
+ try:
+ result = server.update_user(ipautil.wrap_binary_data(olduser),
+ ipautil.wrap_binary_data(newuser))
+ except xmlrpclib.Fault, fault:
+ raise ipaerror.gen_exception(fault.faultCode, fault.faultString)
+ except socket.error, (value, msg):
+ raise xmlrpclib.Fault(value, msg)
+
+ return ipautil.unwrap_binary_data(result)
+
+ def delete_user(self,uid):
+ """Delete a user. uid is the uid of the user to delete."""
+ server = self.setup_server()
+
+ try:
+ result = server.delete_user(uid)
+ except xmlrpclib.Fault, fault:
+ raise ipaerror.gen_exception(fault.faultCode, fault.faultString)
+ except socket.error, (value, msg):
+ raise xmlrpclib.Fault(value, msg)
+
+ return result
+
+ def modifyPassword(self,principal,oldpass,newpass):
+ """Modify a user's password"""
+ server = self.setup_server()
+
+ if oldpass is None:
+ oldpass = "__NONE__"
+
+ try:
+ result = server.modifyPassword(principal,oldpass,newpass)
+ except xmlrpclib.Fault, fault:
+ raise ipaerror.gen_exception(fault.faultCode, fault.faultString)
+ except socket.error, (value, msg):
+ raise xmlrpclib.Fault(value, msg)
+
+ return result
+
+ def mark_user_active(self,uid):
+ """Mark a user as active"""
+ server = self.setup_server()
+
+ try:
+ result = server.mark_user_active(uid)
+ except xmlrpclib.Fault, fault:
+ raise ipaerror.gen_exception(fault.faultCode, fault.faultString)
+ except socket.error, (value, msg):
+ raise xmlrpclib.Fault(value, msg)
+
+ return ipautil.unwrap_binary_data(result)
+
+ def mark_user_inactive(self,uid):
+ """Mark a user as inactive"""
+ server = self.setup_server()
+
+ try:
+ result = server.mark_user_inactive(uid)
+ except xmlrpclib.Fault, fault:
+ raise ipaerror.gen_exception(fault.faultCode, fault.faultString)
+ except socket.error, (value, msg):
+ raise xmlrpclib.Fault(value, msg)
+
+ return ipautil.unwrap_binary_data(result)
+
+
+# Group support
+
+ def get_groups_by_member(self,member_dn,sattrs=None):
+ """Gets the groups that member_dn belongs to.
+ If sattrs is not None then only those
+ attributes will be returned, otherwise all available
+ attributes are returned. The result is a list of dicts."""
+ server = self.setup_server()
+ if sattrs is None:
+ sattrs = "__NONE__"
+ try:
+ result = server.get_groups_by_member(member_dn, sattrs)
+ except xmlrpclib.Fault, fault:
+ raise ipaerror.gen_exception(fault.faultCode, fault.faultString)
+ except socket.error, (value, msg):
+ raise xmlrpclib.Fault(value, msg)
+
+ return ipautil.unwrap_binary_data(result)
+
+ def add_group(self,group,group_container=None):
+ """Add a new group. Takes as input a dict where the key is the
+ attribute name and the value is either a string or in the case
+ of a multi-valued field a list of values"""
+ server = self.setup_server()
+
+ if group_container is None:
+ group_container = "__NONE__"
+
+ try:
+ result = server.add_group(ipautil.wrap_binary_data(group),
+ group_container)
+ except xmlrpclib.Fault, fault:
+ raise ipaerror.gen_exception(fault.faultCode, fault.faultString)
+ except socket.error, (value, msg):
+ raise xmlrpclib.Fault(value, msg)
+
+ return ipautil.unwrap_binary_data(result)
+
+ def find_groups (self, criteria, sattrs=None, sizelimit=-1, timelimit=-1):
+ """Return a list containing a Group object for each group that matches
+ the criteria."""
+
+ server = self.setup_server()
+ try:
+ # None values are not allowed in XML-RPC
+ if sattrs is None:
+ sattrs = "__NONE__"
+ result = server.find_groups(criteria, sattrs, sizelimit, timelimit)
+ except xmlrpclib.Fault, fault:
+ raise ipaerror.gen_exception(fault.faultCode, fault.faultString)
+ except socket.error, (value, msg):
+ raise xmlrpclib.Fault(value, msg)
+
+ return ipautil.unwrap_binary_data(result)
+
+ def add_member_to_group(self, member_dn, group_dn):
+ """Add a new member to an existing group.
+ """
+ server = self.setup_server()
+ try:
+ result = server.add_member_to_group(member_dn, group_dn)
+ except xmlrpclib.Fault, fault:
+ raise ipaerror.gen_exception(fault.faultCode, fault.faultString)
+ except socket.error, (value, msg):
+ raise xmlrpclib.Fault(value, msg)
+
+ return ipautil.unwrap_binary_data(result)
+
+ def add_members_to_group(self, member_dns, group_dn):
+ """Add several members to an existing group.
+ member_dns is a list of the dns to add
+
+ Returns a list of the dns that were not added.
+ """
+ server = self.setup_server()
+ try:
+ result = server.add_members_to_group(member_dns, group_dn)
+ except xmlrpclib.Fault, fault:
+ raise ipaerror.gen_exception(fault.faultCode, fault.faultString)
+ except socket.error, (value, msg):
+ raise xmlrpclib.Fault(value, msg)
+
+ return ipautil.unwrap_binary_data(result)
+
+ def remove_member_from_group(self, member_dn, group_dn):
+ """Remove a member from an existing group.
+ """
+ server = self.setup_server()
+ try:
+ result = server.remove_member_from_group(member_dn, group_dn)
+ except xmlrpclib.Fault, fault:
+ raise ipaerror.gen_exception(fault.faultCode, fault.faultString)
+ except socket.error, (value, msg):
+ raise xmlrpclib.Fault(value, msg)
+
+ return ipautil.unwrap_binary_data(result)
+
+ def remove_members_from_group(self, member_dns, group_dn):
+ """Remove several members from an existing group.
+
+ Returns a list of the dns that were not removed.
+ """
+ server = self.setup_server()
+ try:
+ result = server.remove_members_from_group(member_dns, group_dn)
+ except xmlrpclib.Fault, fault:
+ raise ipaerror.gen_exception(fault.faultCode, fault.faultString)
+ except socket.error, (value, msg):
+ raise xmlrpclib.Fault(value, msg)
+
+ return ipautil.unwrap_binary_data(result)
+
+ def add_user_to_group(self, user_uid, group_dn):
+ """Add a user to an existing group.
+ """
+ server = self.setup_server()
+ try:
+ result = server.add_user_to_group(user_uid, group_dn)
+ except xmlrpclib.Fault, fault:
+ raise ipaerror.gen_exception(fault.faultCode, fault.faultString)
+ except socket.error, (value, msg):
+ raise xmlrpclib.Fault(value, msg)
+
+ return ipautil.unwrap_binary_data(result)
+
+ def add_users_to_group(self, user_uids, group_dn):
+ """Add several users to an existing group.
+ user_uids is a list of the uids of the users to add
+
+ Returns a list of the user uids that were not added.
+ """
+ server = self.setup_server()
+ try:
+ result = server.add_users_to_group(user_uids, group_dn)
+ except xmlrpclib.Fault, fault:
+ raise ipaerror.gen_exception(fault.faultCode, fault.faultString)
+ except socket.error, (value, msg):
+ raise xmlrpclib.Fault(value, msg)
+
+ return ipautil.unwrap_binary_data(result)
+
+ def remove_user_from_group(self, user_uid, group_dn):
+ """Remove a user from an existing group.
+ """
+ server = self.setup_server()
+ try:
+ result = server.remove_user_from_group(user_uid, group_dn)
+ except xmlrpclib.Fault, fault:
+ raise ipaerror.gen_exception(fault.faultCode, fault.faultString)
+ except socket.error, (value, msg):
+ raise xmlrpclib.Fault(value, msg)
+
+ return ipautil.unwrap_binary_data(result)
+
+ def remove_users_from_group(self, user_uids, group_dn):
+ """Remove several users from an existing group.
+ user_uids is a list of the uids of the users to remove
+
+ Returns a list of the user uids that were not removed.
+ """
+ server = self.setup_server()
+ try:
+ result = server.remove_users_from_group(user_uids, group_dn)
+ except xmlrpclib.Fault, fault:
+ raise ipaerror.gen_exception(fault.faultCode, fault.faultString)
+ except socket.error, (value, msg):
+ raise xmlrpclib.Fault(value, msg)
+
+ return ipautil.unwrap_binary_data(result)
+
+ def add_groups_to_user(self, group_dns, user_dn):
+ """Given a list of group dn's add them to the user.
+
+ Returns a list of the group dns that were not added.
+ """
+ server = self.setup_server()
+ try:
+ result = server.add_groups_to_user(group_dns, user_dn)
+ except xmlrpclib.Fault, fault:
+ raise ipaerror.gen_exception(fault.faultCode, fault.faultString)
+ except socket.error, (value, msg):
+ raise xmlrpclib.Fault(value, msg)
+
+ return ipautil.unwrap_binary_data(result)
+
+ def remove_groups_from_user(self, group_dns, user_dn):
+ """Given a list of group dn's remove them from the user.
+
+ Returns a list of the group dns that were not removed.
+ """
+ server = self.setup_server()
+ try:
+ result = server.remove_groups_from_user(group_dns, user_dn)
+ except xmlrpclib.Fault, fault:
+ raise ipaerror.gen_exception(fault.faultCode, fault.faultString)
+ except socket.error, (value, msg):
+ raise xmlrpclib.Fault(value, msg)
+
+ return ipautil.unwrap_binary_data(result)
+
+ def update_group(self,oldgroup,newgroup):
+ """Update an existing group. oldgroup and newgroup are dicts of attributes"""
+ server = self.setup_server()
+
+ try:
+ result = server.update_group(ipautil.wrap_binary_data(oldgroup),
+ ipautil.wrap_binary_data(newgroup))
+ except xmlrpclib.Fault, fault:
+ raise ipaerror.gen_exception(fault.faultCode, fault.faultString)
+ except socket.error, (value, msg):
+ raise xmlrpclib.Fault(value, msg)
+
+ return ipautil.unwrap_binary_data(result)
+
+ def delete_group(self,group_dn):
+ """Delete a group. group_dn is the dn of the group to be deleted."""
+ server = self.setup_server()
+
+ try:
+ result = server.delete_group(group_dn)
+ except xmlrpclib.Fault, fault:
+ raise ipaerror.gen_exception(fault.faultCode, fault.faultString)
+ except socket.error, (value, msg):
+ raise xmlrpclib.Fault(value, msg)
+
+ return ipautil.unwrap_binary_data(result)
+
+ def add_group_to_group(self, group_cn, tgroup_cn):
+ """Add a group to an existing group.
+ group_cn is a cn of the group to add
+ tgroup_cn is the cn of the group to be added to
+ """
+ server = self.setup_server()
+ try:
+ result = server.add_group_to_group(group_cn, tgroup_cn)
+ except xmlrpclib.Fault, fault:
+ raise ipaerror.gen_exception(fault.faultCode, fault.faultString)
+ except socket.error, (value, msg):
+ raise xmlrpclib.Fault(value, msg)
+
+ return ipautil.unwrap_binary_data(result)
+
+ def attrs_to_labels(self,attrs):
+ """Convert a list of LDAP attributes into a more readable form."""
+
+ server = self.setup_server()
+ try:
+ result = server.attrs_to_labels(attrs)
+ except xmlrpclib.Fault, fault:
+ raise ipaerror.gen_exception(fault.faultCode, fault.faultString)
+ except socket.error, (value, msg):
+ raise xmlrpclib.Fault(value, msg)
+
+ return ipautil.unwrap_binary_data(result)
+
+ def get_all_attrs(self):
+ """We have a list of hardcoded attributes -> readable labels. Return
+ that complete list if someone wants it.
+ """
+
+ server = self.setup_server()
+ try:
+ result = server.get_all_attrs()
+ except xmlrpclib.Fault, fault:
+ raise ipaerror.gen_exception(fault.faultCode, fault.faultString)
+ except socket.error, (value, msg):
+ raise xmlrpclib.Fault(value, msg)
+
+ return ipautil.unwrap_binary_data(result)
+
+ def group_members(self, groupdn, attr_list=None, memberstype=0):
+ """Do a memberOf search of groupdn and return the attributes in
+ attr_list (an empty list returns everything)."""
+
+ if attr_list is None:
+ attr_list = "__NONE__"
+
+ server = self.setup_server()
+ try:
+ result = server.group_members(groupdn, attr_list, memberstype)
+ except xmlrpclib.Fault, fault:
+ raise ipaerror.gen_exception(fault.faultCode, fault.faultString)
+ except socket.error, (value, msg):
+ raise xmlrpclib.Fault(value, msg)
+
+ return ipautil.unwrap_binary_data(result)
+
+ def mark_group_active(self,cn):
+ """Mark a group as active"""
+ server = self.setup_server()
+
+ try:
+ result = server.mark_group_active(cn)
+ except xmlrpclib.Fault, fault:
+ raise ipaerror.gen_exception(fault.faultCode, fault.faultString)
+ except socket.error, (value, msg):
+ raise xmlrpclib.Fault(value, msg)
+
+ return ipautil.unwrap_binary_data(result)
+
+ def mark_group_inactive(self,cn):
+ """Mark a group as inactive"""
+ server = self.setup_server()
+
+ try:
+ result = server.mark_group_inactive(cn)
+ except xmlrpclib.Fault, fault:
+ raise ipaerror.gen_exception(fault.faultCode, fault.faultString)
+ except socket.error, (value, msg):
+ raise xmlrpclib.Fault(value, msg)
+
+ return ipautil.unwrap_binary_data(result)
+
+# Configuration support
+
+ def get_ipa_config(self):
+ """Get the IPA configuration"""
+ server = self.setup_server()
+ try:
+ result = server.get_ipa_config()
+ except xmlrpclib.Fault, fault:
+ raise ipaerror.gen_exception(fault.faultCode, fault.faultString)
+ except socket.error, (value, msg):
+ raise xmlrpclib.Fault(value, msg)
+
+ return ipautil.unwrap_binary_data(result)
+
+ def update_ipa_config(self, oldconfig, newconfig):
+ """Update the IPA configuration"""
+ server = self.setup_server()
+ try:
+ result = server.update_ipa_config(ipautil.wrap_binary_data(oldconfig), ipautil.wrap_binary_data(newconfig))
+ except xmlrpclib.Fault, fault:
+ raise ipaerror.gen_exception(fault.faultCode, fault.faultString)
+ except socket.error, (value, msg):
+ raise xmlrpclib.Fault(value, msg)
+
+ return ipautil.unwrap_binary_data(result)
+
+ def get_password_policy(self):
+ """Get the IPA password policy"""
+ server = self.setup_server()
+ try:
+ result = server.get_password_policy()
+ except xmlrpclib.Fault, fault:
+ raise ipaerror.gen_exception(fault.faultCode, fault.faultString)
+ except socket.error, (value, msg):
+ raise xmlrpclib.Fault(value, msg)
+
+ return ipautil.unwrap_binary_data(result)
+
+ def update_password_policy(self, oldpolicy, newpolicy):
+ """Update the IPA password policy"""
+ server = self.setup_server()
+ try:
+ result = server.update_password_policy(ipautil.wrap_binary_data(oldpolicy), ipautil.wrap_binary_data(newpolicy))
+ except xmlrpclib.Fault, fault:
+ raise ipaerror.gen_exception(fault.faultCode, fault.faultString)
+ except socket.error, (value, msg):
+ raise xmlrpclib.Fault(value, msg)
+
+ return ipautil.unwrap_binary_data(result)
+
+ def add_service_principal(self, princ_name, force):
+ server = self.setup_server()
+
+ try:
+ result = server.add_service_principal(princ_name, force)
+ except xmlrpclib.Fault, fault:
+ raise ipaerror.gen_exception(fault.faultCode, fault.faultString)
+ except socket.error, (value, msg):
+ raise xmlrpclib.Fault(value, msg)
+
+ return ipautil.unwrap_binary_data(result)
+
+ def delete_service_principal(self, principal_dn):
+ server = self.setup_server()
+
+ try:
+ result = server.delete_service_principal(principal_dn)
+ except xmlrpclib.Fault, fault:
+ raise ipaerror.gen_exception(fault.faultCode, fault.faultString)
+ except socket.error, (value, msg):
+ raise xmlrpclib.Fault(value, msg)
+
+ return ipautil.unwrap_binary_data(result)
+
+ def find_service_principal (self, criteria, sattrs=None, sizelimit=-1, timelimit=-1):
+ """Return a list: counter followed by a Entity object for each host that
+ matches the criteria. If the results are truncated, counter will
+ be set to -1"""
+
+ server = self.setup_server()
+ try:
+ # None values are not allowed in XML-RPC
+ if sattrs is None:
+ sattrs = "__NONE__"
+ result = server.find_service_principal(criteria, sattrs, sizelimit, timelimit)
+ except xmlrpclib.Fault, fault:
+ raise ipaerror.gen_exception(fault.faultCode, fault.faultString)
+ except socket.error, (value, msg):
+ raise xmlrpclib.Fault(value, msg)
+
+ return ipautil.unwrap_binary_data(result)
+
+ def get_keytab(self, princ_name):
+ server = self.setup_server()
+
+ try:
+ result = server.get_keytab(princ_name)
+ except xmlrpclib.Fault, fault:
+ raise ipaerror.gen_exception(fault.faultCode, fault.faultString)
+ except socket.error, (value, msg):
+ raise xmlrpclib.Fault(value, msg)
+
+ return ipautil.unwrap_binary_data(result)
+
+# radius support
+
+ def get_radius_client_by_ip_addr(self, ip_addr, container, sattrs=None):
+ server = self.setup_server()
+ if container is None: container = "__NONE__"
+ if sattrs is None: sattrs = "__NONE__"
+ try:
+ result = server.get_radius_client_by_ip_addr(ip_addr, container, sattrs)
+ except xmlrpclib.Fault, fault:
+ raise ipaerror.gen_exception(fault.faultCode, fault.faultString)
+ except socket.error, (value, msg):
+ raise xmlrpclib.Fault(value, msg)
+
+ return ipautil.unwrap_binary_data(result)
+
+ def add_radius_client(self, client, container=None):
+ server = self.setup_server()
+
+ if container is None: container = "__NONE__"
+
+ try:
+ result = server.add_radius_client(ipautil.wrap_binary_data(client), container)
+ except xmlrpclib.Fault, fault:
+ raise ipaerror.gen_exception(fault.faultCode, fault.faultString)
+ except socket.error, (value, msg):
+ raise xmlrpclib.Fault(value, msg)
+
+ return ipautil.unwrap_binary_data(result)
+
+ def update_radius_client(self, oldclient, newclient):
+ server = self.setup_server()
+
+ try:
+ result = server.update_radius_client(ipautil.wrap_binary_data(oldclient),
+ ipautil.wrap_binary_data(newclient))
+ except xmlrpclib.Fault, fault:
+ raise ipaerror.gen_exception(fault.faultCode, fault.faultString)
+ except socket.error, (value, msg):
+ raise xmlrpclib.Fault(value, msg)
+
+ return ipautil.unwrap_binary_data(result)
+
+
+ def delete_radius_client(self, ip_addr, container=None):
+ server = self.setup_server()
+ if container is None: container = "__NONE__"
+
+ try:
+ result = server.delete_radius_client(ip_addr, container)
+ except xmlrpclib.Fault, fault:
+ raise ipaerror.gen_exception(fault.faultCode, fault.faultString)
+ except socket.error, (value, msg):
+ raise xmlrpclib.Fault(value, msg)
+
+ return ipautil.unwrap_binary_data(result)
+
+ def find_radius_clients(self, criteria, container=None, sattrs=None, sizelimit=-1, timelimit=-1):
+ server = self.setup_server()
+ if container is None: container = "__NONE__"
+ try:
+ # None values are not allowed in XML-RPC
+ if sattrs is None:
+ sattrs = "__NONE__"
+ result = server.find_radius_clients(criteria, container, sattrs, sizelimit, timelimit)
+ except xmlrpclib.Fault, fault:
+ raise ipaerror.gen_exception(fault.faultCode, fault.faultString)
+ except socket.error, (value, msg):
+ raise xmlrpclib.Fault(value, msg)
+
+ return ipautil.unwrap_binary_data(result)
+
+ def get_radius_profile_by_uid(self, ip_addr, user_profile, sattrs=None):
+ server = self.setup_server()
+ if user_profile is None: user_profile = "__NONE__"
+ if sattrs is None: sattrs = "__NONE__"
+ try:
+ result = server.get_radius_profile_by_uid(ip_addr, user_profile, sattrs)
+ except xmlrpclib.Fault, fault:
+ raise ipaerror.gen_exception(fault.faultCode, fault.faultString)
+ except socket.error, (value, msg):
+ raise xmlrpclib.Fault(value, msg)
+
+ return ipautil.unwrap_binary_data(result)
+
+ def add_radius_profile(self, profile, user_profile=None):
+ server = self.setup_server()
+
+ if user_profile is None: user_profile = "__NONE__"
+
+ try:
+ result = server.add_radius_profile(ipautil.wrap_binary_data(profile), user_profile)
+ except xmlrpclib.Fault, fault:
+ raise ipaerror.gen_exception(fault.faultCode, fault.faultString)
+ except socket.error, (value, msg):
+ raise xmlrpclib.Fault(value, msg)
+
+ return ipautil.unwrap_binary_data(result)
+
+ def update_radius_profile(self, oldprofile, newprofile):
+ server = self.setup_server()
+
+ try:
+ result = server.update_radius_profile(ipautil.wrap_binary_data(oldprofile),
+ ipautil.wrap_binary_data(newprofile))
+ except xmlrpclib.Fault, fault:
+ raise ipaerror.gen_exception(fault.faultCode, fault.faultString)
+ except socket.error, (value, msg):
+ raise xmlrpclib.Fault(value, msg)
+
+ return ipautil.unwrap_binary_data(result)
+
+
+ def delete_radius_profile(self, ip_addr, user_profile=None):
+ server = self.setup_server()
+ if user_profile is None: user_profile = "__NONE__"
+
+ try:
+ result = server.delete_radius_profile(ip_addr, user_profile)
+ except xmlrpclib.Fault, fault:
+ raise ipaerror.gen_exception(fault.faultCode, fault.faultString)
+ except socket.error, (value, msg):
+ raise xmlrpclib.Fault(value, msg)
+
+ return ipautil.unwrap_binary_data(result)
+
+ def find_radius_profiles(self, criteria, user_profile=None, sattrs=None, sizelimit=-1, timelimit=-1):
+ server = self.setup_server()
+ if user_profile is None: user_profile = "__NONE__"
+ try:
+ # None values are not allowed in XML-RPC
+ if sattrs is None:
+ sattrs = "__NONE__"
+ result = server.find_radius_profiles(criteria, user_profile, sattrs, sizelimit, timelimit)
+ except xmlrpclib.Fault, fault:
+ raise ipaerror.gen_exception(fault.faultCode, fault.faultString)
+ except socket.error, (value, msg):
+ raise xmlrpclib.Fault(value, msg)
+
+ return ipautil.unwrap_binary_data(result)
+
diff --git a/ipa-python/setup.py.in b/ipa-python/setup.py.in
new file mode 100644
index 00000000..19940f38
--- /dev/null
+++ b/ipa-python/setup.py.in
@@ -0,0 +1,77 @@
+#!/usr/bin/env python
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+"""FreeIPA python support library
+
+FreeIPA is a server for identity, policy, and audit.
+"""
+
+DOCLINES = __doc__.split("\n")
+
+import os
+import sys
+import distutils.sysconfig
+
+CLASSIFIERS = """\
+Development Status :: 4 - Beta
+Intended Audience :: System Environment/Base
+License :: GPL
+Programming Language :: Python
+Operating System :: POSIX
+Operating System :: Unix
+"""
+
+# BEFORE importing distutils, remove MANIFEST. distutils doesn't properly
+# update it when the contents of directories change.
+if os.path.exists('MANIFEST'): os.remove('MANIFEST')
+
+def setup_package():
+
+ from distutils.core import setup
+
+ old_path = os.getcwd()
+ local_path = os.path.dirname(os.path.abspath(sys.argv[0]))
+ os.chdir(local_path)
+ sys.path.insert(0,local_path)
+
+ try:
+ setup(
+ name = "ipa",
+ version = "__VERSION__",
+ license = "GPL",
+ author = "Karl MacMillan, et.al.",
+ author_email = "kmacmill@redhat.com",
+ maintainer = "freeIPA Developers",
+ maintainer_email = "freeipa-devel@redhat.com",
+ url = "http://www.freeipa.org/",
+ description = DOCLINES[0],
+ long_description = "\n".join(DOCLINES[2:]),
+ download_url = "http://www.freeipa.org/page/Downloads",
+ classifiers=filter(None, CLASSIFIERS.split('\n')),
+ platforms = ["Linux", "Solaris", "Unix"],
+ package_dir = {'ipa': ''},
+ packages = [ "ipa" ],
+ data_files = [('/etc/ipa', ['ipa.conf'])]
+ )
+ finally:
+ del sys.path[0]
+ os.chdir(old_path)
+ return
+
+if __name__ == '__main__':
+ setup_package()
diff --git a/ipa-python/sysrestore.py b/ipa-python/sysrestore.py
new file mode 100644
index 00000000..5d5692be
--- /dev/null
+++ b/ipa-python/sysrestore.py
@@ -0,0 +1,317 @@
+# Authors: Mark McLoughlin <markmc@redhat.com>
+#
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+#
+# This module provides a very simple API which allows
+# ipa-xxx-install --uninstall to restore certain
+# parts of the system configuration to the way it was
+# before ipa-server-install was first run
+
+import os
+import os.path
+import errno
+import shutil
+import logging
+import ConfigParser
+import random
+import string
+
+from ipa import ipautil
+
+SYSRESTORE_PATH = "/tmp"
+SYSRESTORE_INDEXFILE = "sysrestore.index"
+SYSRESTORE_STATEFILE = "sysrestore.state"
+
+class FileStore:
+ """Class for handling backup and restore of files"""
+
+ def __init__(self, path = SYSRESTORE_PATH):
+ """Create a _StoreFiles object, that uses @path as the
+ base directory.
+
+ The file @path/sysrestore.index is used to store information
+ about the original location of the saved files.
+ """
+ self._path = path
+ self._index = self._path + "/" + SYSRESTORE_INDEXFILE
+
+ self.random = random.Random()
+
+ self.files = {}
+ self._load()
+
+ def _load(self):
+ """Load the file list from the index file. @files will
+ be an empty dictionary if the file doesn't exist.
+ """
+
+ logging.debug("Loading Index file from '%s'", self._index)
+
+ self.files = {}
+
+ p = ConfigParser.SafeConfigParser()
+ p.read(self._index)
+
+ for section in p.sections():
+ if section == "files":
+ for (key, value) in p.items(section):
+ self.files[key] = value
+
+
+ def save(self):
+ """Save the file list to @_index. If @files is an empty
+ dict, then @_index should be removed.
+ """
+ logging.debug("Saving Index File to '%s'", self._index)
+
+ if len(self.files) == 0:
+ logging.debug(" -> no files, removing file")
+ if os.path.exists(self._index):
+ os.remove(self._index)
+ return
+
+ p = ConfigParser.SafeConfigParser()
+
+ p.add_section('files')
+ for (key, value) in self.files.items():
+ p.set('files', key, str(value))
+
+ f = file(self._index, "w")
+ p.write(f)
+ f.close()
+
+ def backup_file(self, path):
+ """Create a copy of the file at @path - so long as a copy
+ does not already exist - which will be restored to its
+ original location by restore_files().
+ """
+ logging.debug("Backing up system configuration file '%s'", path)
+
+ if not os.path.isabs(path):
+ raise ValueError("Absolute path required")
+
+ if not os.path.isfile(path):
+ logging.debug(" -> Not backing up - '%s' doesn't exist", path)
+ return
+
+ (reldir, file) = os.path.split(path)
+
+ filename = ""
+ for i in range(8):
+ h = "%02x" % self.random.randint(0,255)
+ filename += h
+ filename += "-"+file
+
+ backup_path = os.path.join(self._path, filename)
+ if os.path.exists(backup_path):
+ logging.debug(" -> Not backing up - already have a copy of '%s'", path)
+ return
+
+ shutil.copy2(path, backup_path)
+
+ stat = os.stat(path)
+
+ self.files[filename] = string.join([str(stat.st_mode),str(stat.st_uid),str(stat.st_gid),path], ',')
+ self.save()
+
+ def restore_file(self, path):
+ """Restore the copy of a file at @path to its original
+ location and delete the copy.
+
+ Returns #True if the file was restored, #False if there
+ was no backup file to restore
+ """
+
+ logging.debug("Restoring system configuration file '%s'", path)
+
+ if not os.path.isabs(path):
+ raise ValueError("Absolute path required")
+
+ mode = None
+ uid = None
+ gid = None
+ filename = None
+
+ for (key, value) in self.files.items():
+ (mode,uid,gid,filepath) = string.split(value, ',', 3)
+ if (filepath == path):
+ filename = key
+ break
+
+ if not filename:
+ raise ValueError("No such file name in the index")
+
+ backup_path = os.path.join(self._path, filename)
+ if not os.path.exists(backup_path):
+ logging.debug(" -> Not restoring - '%s' doesn't exist", backup_path)
+ return False
+
+ shutil.move(backup_path, path)
+ os.chown(path, int(uid), int(gid))
+ os.chmod(path, int(mode))
+
+ ipautil.run(["/sbin/restorecon", path])
+
+ del self.files[filename]
+ self.save()
+
+ return True
+
+ def restore_all_files(self):
+ """Restore the files in the inbdex to their original
+ location and delete the copy.
+
+ Returns #True if the file was restored, #False if there
+ was no backup file to restore
+ """
+
+ if len(self.files) == 0:
+ return False
+
+ for (filename, value) in self.files.items():
+
+ (mode,uid,gid,path) = string.split(value, ',', 3)
+
+ backup_path = os.path.join(self._path, filename)
+ if not os.path.exists(backup_path):
+ logging.debug(" -> Not restoring - '%s' doesn't exist", backup_path)
+
+ shutil.move(backup_path, path)
+ os.chown(path, int(uid), int(gid))
+ os.chmod(path, int(mode))
+
+ ipautil.run(["/sbin/restorecon", path])
+
+ #force file to be deleted
+ self.files = {}
+ self.save()
+
+ return True
+
+class StateFile:
+ """A metadata file for recording system state which can
+ be backed up and later restored. The format is something
+ like:
+
+ [httpd]
+ running=True
+ enabled=False
+ """
+
+ def __init__(self, path = SYSRESTORE_PATH):
+ """Create a StateFile object, loading from @path.
+
+ The dictionary @modules, a member of the returned object,
+ is where the state can be modified. @modules is indexed
+ using a module name to return another dictionary containing
+ key/value pairs with the saved state of that module.
+
+ The keys in these latter dictionaries are arbitrary strings
+ and the values may either be strings or booleans.
+ """
+ self._path = path+"/"+SYSRESTORE_STATEFILE
+
+ self.modules = {}
+
+ self._load()
+
+ def _load(self):
+ """Load the modules from the file @_path. @modules will
+ be an empty dictionary if the file doesn't exist.
+ """
+ logging.debug("Loading StateFile from '%s'", self._path)
+
+ self.modules = {}
+
+ p = ConfigParser.SafeConfigParser()
+ p.read(self._path)
+
+ for module in p.sections():
+ self.modules[module] = {}
+ for (key, value) in p.items(module):
+ if value == str(True):
+ value = True
+ elif value == str(False):
+ value = False
+ self.modules[module][key] = value
+
+ def save(self):
+ """Save the modules to @_path. If @modules is an empty
+ dict, then @_path should be removed.
+ """
+ logging.debug("Saving StateFile to '%s'", self._path)
+
+ for module in self.modules.keys():
+ if len(self.modules[module]) == 0:
+ del self.modules[module]
+
+ if len(self.modules) == 0:
+ logging.debug(" -> no modules, removing file")
+ if os.path.exists(self._path):
+ os.remove(self._path)
+ return
+
+ p = ConfigParser.SafeConfigParser()
+
+ for module in self.modules.keys():
+ p.add_section(module)
+ for (key, value) in self.modules[module].items():
+ p.set(module, key, str(value))
+
+ f = file(self._path, "w")
+ p.write(f)
+ f.close()
+
+ def backup_state(self, module, key, value):
+ """Backup an item of system state from @module, identified
+ by the string @key and with the value @value. @value may be
+ a string or boolean.
+ """
+ if not (isinstance(value, str) or isinstance(value, bool)):
+ raise ValueError("Only strings or booleans supported")
+
+ if not self.modules.has_key(module):
+ self.modules[module] = {}
+
+ if not self.modules.has_key(key):
+ self.modules[module][key] = value
+
+ self.save()
+
+ def restore_state(self, module, key):
+ """Return the value of an item of system state from @module,
+ identified by the string @key, and remove it from the backed
+ up system state.
+
+ If the item doesn't exist, #None will be returned, otherwise
+ the original string or boolean value is returned.
+ """
+
+ if not self.modules.has_key(module):
+ return None
+
+ if not self.modules[module].has_key(key):
+ return None
+
+ value = self.modules[module][key]
+ del self.modules[module][key]
+
+ self.save()
+
+ return value
diff --git a/ipa-python/test/test_aci.py b/ipa-python/test/test_aci.py
new file mode 100644
index 00000000..fb9d84c7
--- /dev/null
+++ b/ipa-python/test/test_aci.py
@@ -0,0 +1,127 @@
+#! /usr/bin/python -E
+#
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+import sys
+sys.path.insert(0, ".")
+
+import unittest
+import aci
+import urllib
+
+
+class TestACI(unittest.TestCase):
+ acitemplate = ('(targetattr="%s")' +
+ '(targetfilter="(memberOf=%s)")' +
+ '(version 3.0;' +
+ 'acl "%s";' +
+ 'allow (write) ' +
+ 'groupdn="ldap:///%s";)')
+
+ def setUp(self):
+ self.aci = aci.ACI()
+
+ def tearDown(self):
+ pass
+
+ def testExport(self):
+ self.aci.source_group = 'cn=foo, dc=freeipa, dc=org'
+ self.aci.dest_group = 'cn=bar, dc=freeipa, dc=org'
+ self.aci.name = 'this is a "name'
+ self.aci.attrs = ['field1', 'field2', 'field3']
+
+ exportaci = self.aci.export_to_string()
+ aci = TestACI.acitemplate % ('field1 || field2 || field3',
+ self.aci.dest_group,
+ 'this is a "name',
+ self.aci.source_group)
+
+ self.assertEqual(aci, exportaci)
+
+ def testURLEncodedExport(self):
+ self.aci.source_group = 'cn=foo " bar, dc=freeipa, dc=org'
+ self.aci.dest_group = 'cn=bar, dc=freeipa, dc=org'
+ self.aci.name = 'this is a "name'
+ self.aci.attrs = ['field1', 'field2', 'field3']
+
+ exportaci = self.aci.export_to_string()
+ aci = TestACI.acitemplate % ('field1 || field2 || field3',
+ self.aci.dest_group,
+ 'this is a "name',
+ urllib.quote(self.aci.source_group, "/=, "))
+
+ self.assertEqual(aci, exportaci)
+
+ def testSimpleParse(self):
+ attr_str = 'field3 || field4 || field5'
+ dest_dn = 'cn=dest\\"group, dc=freeipa, dc=org'
+ name = 'my name'
+ src_dn = 'cn=srcgroup, dc=freeipa, dc=org'
+
+ acistr = TestACI.acitemplate % (attr_str, dest_dn, name, src_dn)
+ self.aci.parse_acistr(acistr)
+
+ self.assertEqual(['field3', 'field4', 'field5'], self.aci.attrs)
+ self.assertEqual(dest_dn, self.aci.dest_group)
+ self.assertEqual(name, self.aci.name)
+ self.assertEqual(src_dn, self.aci.source_group)
+
+ def testUrlEncodedParse(self):
+ attr_str = 'field3 || field4 || field5'
+ dest_dn = 'cn=dest\\"group, dc=freeipa, dc=org'
+ name = 'my name'
+ src_dn = 'cn=src " group, dc=freeipa, dc=org'
+
+ acistr = TestACI.acitemplate % (attr_str, dest_dn, name,
+ urllib.quote(src_dn, "/=, "))
+ self.aci.parse_acistr(acistr)
+
+ self.assertEqual(['field3', 'field4', 'field5'], self.aci.attrs)
+ self.assertEqual(dest_dn, self.aci.dest_group)
+ self.assertEqual(name, self.aci.name)
+ self.assertEqual(src_dn, self.aci.source_group)
+
+ def testInvalidParse(self):
+ try:
+ self.aci.parse_acistr('foo bar')
+ self.fail('Should have failed to parse')
+ except SyntaxError:
+ pass
+
+ try:
+ self.aci.parse_acistr('')
+ self.fail('Should have failed to parse')
+ except SyntaxError:
+ pass
+
+ attr_str = 'field3 || field4 || field5'
+ dest_dn = 'cn=dest\\"group, dc=freeipa, dc=org'
+ name = 'my name'
+ src_dn = 'cn=srcgroup, dc=freeipa, dc=org'
+
+ acistr = TestACI.acitemplate % (attr_str, dest_dn, name, src_dn)
+ acistr += 'trailing garbage'
+ try:
+ self.aci.parse_acistr('')
+ self.fail('Should have failed to parse')
+ except SyntaxError:
+ pass
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/ipa-python/test/test_ipautil.py b/ipa-python/test/test_ipautil.py
new file mode 100644
index 00000000..60d53a27
--- /dev/null
+++ b/ipa-python/test/test_ipautil.py
@@ -0,0 +1,309 @@
+#! /usr/bin/python -E
+#
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+import sys
+sys.path.insert(0, ".")
+
+import unittest
+import datetime
+
+import ipautil
+
+
+class TestCIDict(unittest.TestCase):
+ def setUp(self):
+ self.cidict = ipautil.CIDict()
+ self.cidict["Key1"] = "val1"
+ self.cidict["key2"] = "val2"
+ self.cidict["KEY3"] = "VAL3"
+
+ def tearDown(self):
+ pass
+
+ def testLen(self):
+ self.assertEqual(3, len(self.cidict))
+
+ def test__GetItem(self):
+ self.assertEqual("val1", self.cidict["Key1"])
+ self.assertEqual("val1", self.cidict["key1"])
+ self.assertEqual("val2", self.cidict["KEY2"])
+ self.assertEqual("VAL3", self.cidict["key3"])
+ self.assertEqual("VAL3", self.cidict["KEY3"])
+ try:
+ self.cidict["key4"]
+ fail("should have raised KeyError")
+ except KeyError:
+ pass
+
+ def testGet(self):
+ self.assertEqual("val1", self.cidict.get("Key1"))
+ self.assertEqual("val1", self.cidict.get("key1"))
+ self.assertEqual("val2", self.cidict.get("KEY2"))
+ self.assertEqual("VAL3", self.cidict.get("key3"))
+ self.assertEqual("VAL3", self.cidict.get("KEY3"))
+ self.assertEqual("default", self.cidict.get("key4", "default"))
+
+ def test__SetItem(self):
+ self.cidict["key4"] = "val4"
+ self.assertEqual("val4", self.cidict["key4"])
+ self.cidict["KEY4"] = "newval4"
+ self.assertEqual("newval4", self.cidict["key4"])
+
+ def testDel(self):
+ self.assert_(self.cidict.has_key("Key1"))
+ del(self.cidict["Key1"])
+ self.failIf(self.cidict.has_key("Key1"))
+
+ self.assert_(self.cidict.has_key("key2"))
+ del(self.cidict["KEY2"])
+ self.failIf(self.cidict.has_key("key2"))
+
+ def testClear(self):
+ self.assertEqual(3, len(self.cidict))
+ self.cidict.clear()
+ self.assertEqual(0, len(self.cidict))
+
+ def testCopy(self):
+ """A copy is no longer a CIDict, but should preserve the case of
+ the keys as they were inserted."""
+ copy = self.cidict.copy()
+ self.assertEqual(3, len(copy))
+ self.assert_(copy.has_key("Key1"))
+ self.assertEqual("val1", copy["Key1"])
+ self.failIf(copy.has_key("key1"))
+
+ def testHasKey(self):
+ self.assert_(self.cidict.has_key("KEY1"))
+ self.assert_(self.cidict.has_key("key2"))
+ self.assert_(self.cidict.has_key("key3"))
+
+ def testItems(self):
+ items = self.cidict.items()
+ self.assertEqual(3, len(items))
+ items_set = set(items)
+ self.assert_(("Key1", "val1") in items_set)
+ self.assert_(("key2", "val2") in items_set)
+ self.assert_(("KEY3", "VAL3") in items_set)
+
+ def testIterItems(self):
+ items = []
+ for (k,v) in self.cidict.iteritems():
+ items.append((k,v))
+ self.assertEqual(3, len(items))
+ items_set = set(items)
+ self.assert_(("Key1", "val1") in items_set)
+ self.assert_(("key2", "val2") in items_set)
+ self.assert_(("KEY3", "VAL3") in items_set)
+
+ def testIterKeys(self):
+ keys = []
+ for k in self.cidict.iterkeys():
+ keys.append(k)
+ self.assertEqual(3, len(keys))
+ keys_set = set(keys)
+ self.assert_("Key1" in keys_set)
+ self.assert_("key2" in keys_set)
+ self.assert_("KEY3" in keys_set)
+
+ def testIterValues(self):
+ values = []
+ for k in self.cidict.itervalues():
+ values.append(k)
+ self.assertEqual(3, len(values))
+ values_set = set(values)
+ self.assert_("val1" in values_set)
+ self.assert_("val2" in values_set)
+ self.assert_("VAL3" in values_set)
+
+ def testKeys(self):
+ keys = self.cidict.keys()
+ self.assertEqual(3, len(keys))
+ keys_set = set(keys)
+ self.assert_("Key1" in keys_set)
+ self.assert_("key2" in keys_set)
+ self.assert_("KEY3" in keys_set)
+
+ def testValues(self):
+ values = self.cidict.values()
+ self.assertEqual(3, len(values))
+ values_set = set(values)
+ self.assert_("val1" in values_set)
+ self.assert_("val2" in values_set)
+ self.assert_("VAL3" in values_set)
+
+ def testUpdate(self):
+ newdict = { "KEY2": "newval2",
+ "key4": "val4" }
+ self.cidict.update(newdict)
+ self.assertEqual(4, len(self.cidict))
+
+ items = self.cidict.items()
+ self.assertEqual(4, len(items))
+ items_set = set(items)
+ self.assert_(("Key1", "val1") in items_set)
+ # note the update "overwrites" the case of the key2
+ self.assert_(("KEY2", "newval2") in items_set)
+ self.assert_(("KEY3", "VAL3") in items_set)
+ self.assert_(("key4", "val4") in items_set)
+
+ def testSetDefault(self):
+ self.assertEqual("val1", self.cidict.setdefault("KEY1", "default"))
+
+ self.failIf(self.cidict.has_key("KEY4"))
+ self.assertEqual("default", self.cidict.setdefault("KEY4", "default"))
+ self.assert_(self.cidict.has_key("KEY4"))
+ self.assertEqual("default", self.cidict["key4"])
+
+ self.failIf(self.cidict.has_key("KEY5"))
+ self.assertEqual(None, self.cidict.setdefault("KEY5"))
+ self.assert_(self.cidict.has_key("KEY5"))
+ self.assertEqual(None, self.cidict["key5"])
+
+ def testPop(self):
+ self.assertEqual("val1", self.cidict.pop("KEY1", "default"))
+ self.failIf(self.cidict.has_key("key1"))
+
+ self.assertEqual("val2", self.cidict.pop("KEY2"))
+ self.failIf(self.cidict.has_key("key2"))
+
+ self.assertEqual("default", self.cidict.pop("key4", "default"))
+ try:
+ self.cidict.pop("key4")
+ fail("should have raised KeyError")
+ except KeyError:
+ pass
+
+ def testPopItem(self):
+ items = set(self.cidict.items())
+ self.assertEqual(3, len(self.cidict))
+
+ item = self.cidict.popitem()
+ self.assertEqual(2, len(self.cidict))
+ self.assert_(item in items)
+ items.discard(item)
+
+ item = self.cidict.popitem()
+ self.assertEqual(1, len(self.cidict))
+ self.assert_(item in items)
+ items.discard(item)
+
+ item = self.cidict.popitem()
+ self.assertEqual(0, len(self.cidict))
+ self.assert_(item in items)
+ items.discard(item)
+
+class TestTimeParser(unittest.TestCase):
+ def setUp(self):
+ pass
+
+ def tearDown(self):
+ pass
+
+ def testSimple(self):
+ timestr = "20070803"
+
+ time = ipautil.parse_generalized_time(timestr)
+ self.assertEqual(2007, time.year)
+ self.assertEqual(8, time.month)
+ self.assertEqual(3, time.day)
+ self.assertEqual(0, time.hour)
+ self.assertEqual(0, time.minute)
+ self.assertEqual(0, time.second)
+
+ def testHourMinSec(self):
+ timestr = "20051213141205"
+
+ time = ipautil.parse_generalized_time(timestr)
+ self.assertEqual(2005, time.year)
+ self.assertEqual(12, time.month)
+ self.assertEqual(13, time.day)
+ self.assertEqual(14, time.hour)
+ self.assertEqual(12, time.minute)
+ self.assertEqual(5, time.second)
+
+ def testFractions(self):
+ timestr = "2003092208.5"
+
+ time = ipautil.parse_generalized_time(timestr)
+ self.assertEqual(2003, time.year)
+ self.assertEqual(9, time.month)
+ self.assertEqual(22, time.day)
+ self.assertEqual(8, time.hour)
+ self.assertEqual(30, time.minute)
+ self.assertEqual(0, time.second)
+
+ timestr = "199203301544,25"
+
+ time = ipautil.parse_generalized_time(timestr)
+ self.assertEqual(1992, time.year)
+ self.assertEqual(3, time.month)
+ self.assertEqual(30, time.day)
+ self.assertEqual(15, time.hour)
+ self.assertEqual(44, time.minute)
+ self.assertEqual(15, time.second)
+
+ timestr = "20060401185912,8"
+
+ time = ipautil.parse_generalized_time(timestr)
+ self.assertEqual(2006, time.year)
+ self.assertEqual(4, time.month)
+ self.assertEqual(1, time.day)
+ self.assertEqual(18, time.hour)
+ self.assertEqual(59, time.minute)
+ self.assertEqual(12, time.second)
+ self.assertEqual(800000, time.microsecond)
+
+ def testTimeZones(self):
+ timestr = "20051213141205Z"
+
+ time = ipautil.parse_generalized_time(timestr)
+ self.assertEqual(0, time.tzinfo.houroffset)
+ self.assertEqual(0, time.tzinfo.minoffset)
+ offset = time.tzinfo.utcoffset(None)
+ self.assertEqual(0, offset.seconds)
+
+ timestr = "20051213141205+0500"
+
+ time = ipautil.parse_generalized_time(timestr)
+ self.assertEqual(5, time.tzinfo.houroffset)
+ self.assertEqual(0, time.tzinfo.minoffset)
+ offset = time.tzinfo.utcoffset(None)
+ self.assertEqual(5 * 60 * 60, offset.seconds)
+
+ timestr = "20051213141205-0500"
+
+ time = ipautil.parse_generalized_time(timestr)
+ self.assertEqual(-5, time.tzinfo.houroffset)
+ self.assertEqual(0, time.tzinfo.minoffset)
+ # NOTE - the offset is always positive - it's minutes
+ # _east_ of UTC
+ offset = time.tzinfo.utcoffset(None)
+ self.assertEqual((24 - 5) * 60 * 60, offset.seconds)
+
+ timestr = "20051213141205-0930"
+
+ time = ipautil.parse_generalized_time(timestr)
+ self.assertEqual(-9, time.tzinfo.houroffset)
+ self.assertEqual(-30, time.tzinfo.minoffset)
+ offset = time.tzinfo.utcoffset(None)
+ self.assertEqual(((24 - 9) * 60 * 60) - (30 * 60), offset.seconds)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/ipa-python/test/test_ipavalidate.py b/ipa-python/test/test_ipavalidate.py
new file mode 100644
index 00000000..8b79fbf0
--- /dev/null
+++ b/ipa-python/test/test_ipavalidate.py
@@ -0,0 +1,97 @@
+#! /usr/bin/python -E
+#
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+import sys
+sys.path.insert(0, ".")
+
+import unittest
+
+import ipavalidate
+
+class TestValidate(unittest.TestCase):
+ def setUp(self):
+ pass
+
+ def tearDown(self):
+ pass
+
+ def test_validEmail(self):
+ self.assertEqual(True, ipavalidate.Email("test@freeipa.org"))
+ self.assertEqual(True, ipavalidate.Email("", notEmpty=False))
+
+ def test_invalidEmail(self):
+ self.assertEqual(False, ipavalidate.Email("test"))
+ self.assertEqual(False, ipavalidate.Email("test@freeipa"))
+ self.assertEqual(False, ipavalidate.Email("test@.com"))
+ self.assertEqual(False, ipavalidate.Email(""))
+ self.assertEqual(False, ipavalidate.Email(None))
+
+ def test_validPlain(self):
+ self.assertEqual(True, ipavalidate.Plain("Joe User"))
+ self.assertEqual(True, ipavalidate.Plain("Joe O'Malley"))
+ self.assertEqual(True, ipavalidate.Plain("", notEmpty=False))
+ self.assertEqual(True, ipavalidate.Plain(None, notEmpty=False))
+ self.assertEqual(True, ipavalidate.Plain("JoeUser", allowSpaces=False))
+ self.assertEqual(True, ipavalidate.Plain("JoeUser", allowSpaces=True))
+
+ def test_invalidPlain(self):
+ self.assertEqual(False, ipavalidate.Plain("Joe (User)"))
+ self.assertEqual(False, ipavalidate.Plain("Joe C. User"))
+ self.assertEqual(False, ipavalidate.Plain("", notEmpty=True))
+ self.assertEqual(False, ipavalidate.Plain(None, notEmpty=True))
+ self.assertEqual(False, ipavalidate.Plain("Joe User", allowSpaces=False))
+ self.assertEqual(False, ipavalidate.Plain("Joe C. User"))
+
+ def test_validString(self):
+ self.assertEqual(True, ipavalidate.String("Joe User"))
+ self.assertEqual(True, ipavalidate.String("Joe O'Malley"))
+ self.assertEqual(True, ipavalidate.String("", notEmpty=False))
+ self.assertEqual(True, ipavalidate.String(None, notEmpty=False))
+ self.assertEqual(True, ipavalidate.String("Joe C. User"))
+
+ def test_invalidString(self):
+ self.assertEqual(False, ipavalidate.String("", notEmpty=True))
+ self.assertEqual(False, ipavalidate.String(None, notEmpty=True))
+
+ def test_validPath(self):
+ self.assertEqual(True, ipavalidate.Path("/"))
+ self.assertEqual(True, ipavalidate.Path("/home/user"))
+ self.assertEqual(True, ipavalidate.Path("../home/user"))
+ self.assertEqual(True, ipavalidate.Path("", notEmpty=False))
+ self.assertEqual(True, ipavalidate.Path(None, notEmpty=False))
+
+ def test_invalidPath(self):
+ self.assertEqual(False, ipavalidate.Path("(foo)"))
+ self.assertEqual(False, ipavalidate.Path("", notEmpty=True))
+ self.assertEqual(False, ipavalidate.Path(None, notEmpty=True))
+
+ def test_validName(self):
+ self.assertEqual(True, ipavalidate.GoodName("foo"))
+ self.assertEqual(True, ipavalidate.GoodName("1foo"))
+ self.assertEqual(True, ipavalidate.GoodName("foo.bar"))
+ self.assertEqual(True, ipavalidate.GoodName("foo.bar$"))
+
+ def test_invalidName(self):
+ self.assertEqual(False, ipavalidate.GoodName("foo bar"))
+ self.assertEqual(False, ipavalidate.GoodName("foo%bar"))
+ self.assertEqual(False, ipavalidate.GoodName("*foo"))
+ self.assertEqual(False, ipavalidate.GoodName("$foo.bar$"))
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/ipa-python/user.py b/ipa-python/user.py
new file mode 100644
index 00000000..d638cc4a
--- /dev/null
+++ b/ipa-python/user.py
@@ -0,0 +1,24 @@
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+from ipa.entity import Entity
+
+class User(Entity):
+
+ def __init2__(self):
+ pass
+
diff --git a/ipa-python/version.py.in b/ipa-python/version.py.in
new file mode 100644
index 00000000..fdb689f0
--- /dev/null
+++ b/ipa-python/version.py.in
@@ -0,0 +1,25 @@
+# Authors: Rob Crittenden <rcritten@redhat.com>
+#
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+# The full version including strings
+VERSION="__VERSION__"
+
+# Just the numeric portion of the version so one can do direct numeric
+# comparisons to see if the API is compatible.
+NUM_VERSION=__NUM_VERSION__
diff --git a/ipa-radius-admintools/Makefile b/ipa-radius-admintools/Makefile
new file mode 100644
index 00000000..0bb7824d
--- /dev/null
+++ b/ipa-radius-admintools/Makefile
@@ -0,0 +1,24 @@
+SBINDIR = $(DESTDIR)/usr/sbin
+
+
+all: ;
+
+install:
+ install -m 755 ipa-addradiusclient $(SBINDIR)
+ install -m 755 ipa-modradiusclient $(SBINDIR)
+ install -m 755 ipa-delradiusclient $(SBINDIR)
+ install -m 755 ipa-findradiusclient $(SBINDIR)
+ install -m 755 ipa-addradiusprofile $(SBINDIR)
+ install -m 755 ipa-modradiusprofile $(SBINDIR)
+ install -m 755 ipa-delradiusprofile $(SBINDIR)
+ install -m 755 ipa-findradiusprofile $(SBINDIR)
+
+clean:
+ rm -f *~ *.pyc
+
+distclean: clean
+ rm -f ipa-radius-admintools.spec
+
+maintainer-clean: distclean
+
+test:
diff --git a/ipa-radius-admintools/ipa-addradiusclient b/ipa-radius-admintools/ipa-addradiusclient
new file mode 100644
index 00000000..4f97c648
--- /dev/null
+++ b/ipa-radius-admintools/ipa-addradiusclient
@@ -0,0 +1,197 @@
+#! /usr/bin/python -E
+# Authors: John Dennis <jdennis@redhat.com>
+#
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+import sys
+import os
+from optparse import OptionParser
+
+import ipa.ipaclient as ipaclient
+import ipa.ipautil as ipautil
+import ipa.config
+import ipa.ipaerror
+import ipa.radius_util as radius_util
+
+import xmlrpclib
+import kerberos
+import ldap
+
+#------------------------------------------------------------------------------
+
+radius_attrs = radius_util.radius_client_attr_to_ldap_attr.keys()
+radius_attr_to_ldap_attr = radius_util.radius_client_attr_to_ldap_attr
+ldap_attr_to_radius_attr = radius_util.radius_client_ldap_attr_to_radius_attr
+mandatory_radius_attrs = ['Client-IP-Address', 'Secret']
+distinguished_attr = 'Client-IP-Address'
+
+#------------------------------------------------------------------------------
+
+def help_option_callback(option, opt_str, value, parser, *args, **kwargs):
+ parser.print_help()
+ print
+ print "Valid interative attributes are:"
+ print ipautil.format_list(radius_attrs, quote='"')
+ print
+ print "Required attributes are:"
+ print ipautil.format_list(mandatory_radius_attrs, quote='"')
+ sys.exit(0)
+
+def main():
+ pairs = {}
+
+ opt_parser = OptionParser(add_help_option=False)
+
+ opt_parser.add_option("-a", "--Client-IP-Address", dest="ip_addr",
+ help="RADIUS client ip address")
+ opt_parser.add_option("-s", "--Secret", dest="secret",
+ help="RADIUS client ip address")
+ opt_parser.add_option("-n", "--Name", dest="name",
+ help="RADIUS client name")
+ opt_parser.add_option("-t", "--NAS-Type", dest="nastype",
+ help="RADIUS client NAS Type")
+ opt_parser.add_option("-d", "--Description", dest="desc",
+ help="description of the RADIUS client")
+
+ opt_parser.add_option("-h", "--help", action="callback", callback=help_option_callback,
+ help="detailed help information")
+ opt_parser.add_option("-i", "--interactive", dest="interactive", action='store_true', default=False,
+ help="interactive mode, prompts with auto-completion")
+ opt_parser.add_option("-p", "--pair", dest="pairs", action='append',
+ help="specify one or more attribute=value pair(s), value may be optionally quoted, pairs are delimited by whitespace")
+ opt_parser.add_option("-f", "--file", dest="pair_file",
+ help="attribute=value pair(s) are read from file, value may be optionally quoted, pairs are delimited by whitespace. Reads from stdin if file is -")
+ opt_parser.add_option("-v", "--verbose", dest="verbose", action='store_true',
+ help="print information")
+
+ opt_parser.set_usage("Usage: %s [options] %s" % (distinguished_attr, os.path.basename(sys.argv[0])))
+
+ ipa.config.add_standard_options(opt_parser)
+ options, args = opt_parser.parse_args()
+
+ if len(args) < 1:
+ opt_parser.error('missing %s' % (distinguished_attr))
+
+ ipa.config.init_config(options)
+
+ ip_addr = args[0]
+ pairs[distinguished_attr] = ip_addr
+
+ # Get pairs from a file or stdin
+ if options.pair_file:
+ try:
+ av = ipautil.read_pairs_file(options.pair_file)
+ pairs.update(av)
+ except Exception, e:
+ print "ERROR, could not read pairs (%s)" % (e)
+
+ # Get pairs specified on the command line as a named argument
+ if options.ip_addr: pairs[distinguished_attr] = options.ip_addr
+ if options.secret: pairs['Secret'] = options.secret
+ if options.name: pairs['Name'] = options.name
+ if options.nastype: pairs['NAS-Type'] = options.nastype
+ if options.desc: pairs['Description'] = options.desc
+
+ # Get pairs specified on the command line as a pair argument
+ if options.pairs:
+ for p in options.pairs:
+ av = ipautil.parse_key_value_pairs(p)
+ pairs.update(av)
+
+ # Get pairs interactively
+ if options.interactive:
+ # Prompt first for mandatory attributes which have not been previously specified
+ prompted_mandatory_attrs = []
+ existing_attrs = pairs.keys()
+ for attr in mandatory_radius_attrs:
+ if not attr in existing_attrs:
+ prompted_mandatory_attrs.append(attr)
+
+ c = ipautil.AttributeValueCompleter(radius_attrs, pairs)
+ c.open()
+ av = c.get_pairs("Enter: ", prompted_mandatory_attrs, radius_util.validate)
+ pairs.update(av)
+ c.close()
+
+ # FIXME: validation should be moved to xmlrpc server
+
+ # Data collection done, assure mandatory data has been specified
+
+ if pairs.has_key(distinguished_attr) and pairs[distinguished_attr] != ip_addr:
+ print "ERROR, %s specified on command line (%s) does not match value found in pairs (%s)" % \
+ (distinguished_attr, ip_addr, pairs[distinguished_attr])
+ return 1
+
+ valid = True
+ for attr in mandatory_radius_attrs:
+ if not pairs.has_key(attr):
+ valid = False
+ print "ERROR, %s is mandatory, but has not been specified" % (attr)
+ if not valid:
+ return 1
+
+ # Make sure each attribute is a member of the set of valid attributes
+ valid = True
+ for attr,value in pairs.items():
+ if attr not in radius_attrs:
+ valid = False
+ print "ERROR, %s is not a valid attribute" % (attr)
+ if not valid:
+ print "Valid attributes are:"
+ print ipautil.format_list(radius_attrs, quote='"')
+ return 1
+
+ # Makse sure each value is valid
+ valid = True
+ for attr,value in pairs.items():
+ if not radius_util.validate(attr, value):
+ valid = False
+ if not valid:
+ return 1
+
+ # Dump what we've got so far
+ if options.verbose:
+ print "Pairs:"
+ for attr,value in pairs.items():
+ print "\t%s = %s" % (attr, value)
+
+ radius_entity = radius_util.RadiusClient()
+ for attr,value in pairs.items():
+ radius_entity.setValue(radius_attr_to_ldap_attr[attr], value)
+
+ try:
+ ipa_client = ipaclient.IPAClient()
+ ipa_client.add_radius_client(radius_entity)
+ print "successfully added"
+ except xmlrpclib.Fault, f:
+ print f.faultString
+ return 1
+ except kerberos.GSSError, e:
+ print "Could not initialize GSSAPI: %s/%s" % (e[0][0][0], e[0][1][0])
+ return 1
+ except xmlrpclib.ProtocolError, e:
+ print "Unable to connect to IPA server: %s" % (e.errmsg)
+ return 1
+ except ipa.ipaerror.IPAError, e:
+ print "%s" % (e.message)
+ return 1
+
+ return 0
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/ipa-radius-admintools/ipa-addradiusprofile b/ipa-radius-admintools/ipa-addradiusprofile
new file mode 100644
index 00000000..aa3bf9c1
--- /dev/null
+++ b/ipa-radius-admintools/ipa-addradiusprofile
@@ -0,0 +1,196 @@
+#! /usr/bin/python -E
+# Authors: John Dennis <jdennis@redhat.com>
+#
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+import sys
+import os
+from optparse import OptionParser
+
+import ipa.ipaclient as ipaclient
+import ipa.ipautil as ipautil
+import ipa.config
+import ipa.ipaerror
+import ipa.radius_util as radius_util
+
+import xmlrpclib
+import kerberos
+import ldap
+
+#------------------------------------------------------------------------------
+
+radius_attrs = radius_util.radius_profile_attr_to_ldap_attr.keys()
+radius_attr_to_ldap_attr = radius_util.radius_profile_attr_to_ldap_attr
+ldap_attr_to_radius_attr = radius_util.radius_profile_ldap_attr_to_radius_attr
+mandatory_radius_attrs = ['UID']
+distinguished_attr = 'UID'
+
+#------------------------------------------------------------------------------
+
+def help_option_callback(option, opt_str, value, parser, *args, **kwargs):
+ parser.print_help()
+ print
+ print "Valid interative attributes are:"
+ print ipautil.format_list(radius_attrs, quote='"')
+ print
+ print "Required attributes are:"
+ print ipautil.format_list(mandatory_radius_attrs, quote='"')
+ sys.exit(0)
+
+def main():
+ pairs = {}
+
+ opt_parser = OptionParser(add_help_option=False)
+
+ opt_parser.add_option("-u", "--uid", dest="uid",
+ help="RADIUS profile identifier")
+ opt_parser.add_option("-s", "--shared", dest="shared", default=False, action='store_true',
+ help="profile is shared")
+ opt_parser.add_option("-d", "--Description", dest="desc",
+ help="description of the RADIUS client")
+
+ opt_parser.add_option("-h", "--help", action="callback", callback=help_option_callback,
+ help="detailed help information")
+ opt_parser.add_option("-i", "--interactive", dest="interactive", action='store_true', default=False,
+ help="interactive mode, prompts with auto-completion")
+ opt_parser.add_option("-p", "--pair", dest="pairs", action='append',
+ help="specify one or more attribute=value pair(s), value may be optionally quoted, pairs are delimited by whitespace")
+ opt_parser.add_option("-f", "--file", dest="pair_file",
+ help="attribute=value pair(s) are read from file, value may be optionally quoted, pairs are delimited by whitespace. Reads from stdin if file is -")
+ opt_parser.add_option("-v", "--verbose", dest="verbose", action='store_true',
+ help="print information")
+
+ opt_parser.set_usage("Usage: %s [options] %s" % (distinguished_attr, os.path.basename(sys.argv[0])))
+
+ ipa.config.add_standard_options(opt_parser)
+ options, args = opt_parser.parse_args()
+
+ if len(args) < 1:
+ opt_parser.error('missing %s' % (distinguished_attr))
+
+ ipa.config.init_config(options)
+
+ uid = args[0]
+ user_profile = not options.shared
+ pairs[distinguished_attr] = uid
+
+ # Per user profiles are pre-created (i.e. objectclass radiusprofile is always added for each user)
+ if user_profile:
+ print "ERROR, you cannot add a per-user radius profile, it pre-exists"
+ return 1
+
+ # Get pairs from a file or stdin
+ if options.pair_file:
+ try:
+ av = ipautil.read_pairs_file(options.pair_file)
+ pairs.update(av)
+ except Exception, e:
+ print "ERROR, could not read pairs (%s)" % (e)
+
+ # Get pairs specified on the command line as a named argument
+ if options.uid: pairs['UID'] = options.uid
+ if options.desc: pairs['Description'] = options.desc
+
+ # Get pairs specified on the command line as a pair argument
+ if options.pairs:
+ for p in options.pairs:
+ av = ipautil.parse_key_value_pairs(p)
+ pairs.update(av)
+
+ # Get pairs interactively
+ if options.interactive:
+ # Prompt first for mandatory attributes which have not been previously specified
+ prompted_mandatory_attrs = []
+ existing_attrs = pairs.keys()
+ for attr in mandatory_radius_attrs:
+ if not attr in existing_attrs:
+ prompted_mandatory_attrs.append(attr)
+
+ c = ipautil.AttributeValueCompleter(radius_attrs, pairs)
+ c.open()
+ av = c.get_pairs("Enter: ", prompted_mandatory_attrs, radius_util.validate)
+ pairs.update(av)
+ c.close()
+
+ # FIXME: validation should be moved to xmlrpc server
+
+ # Data collection done, assure mandatory data has been specified
+
+ if pairs.has_key(distinguished_attr) and pairs[distinguished_attr] != uid:
+ print "ERROR, %s specified on command line (%s) does not match value found in pairs (%s)" % \
+ (distinguished_attr, uid, pairs[distinguished_attr])
+ return 1
+
+ valid = True
+ for attr in mandatory_radius_attrs:
+ if not pairs.has_key(attr):
+ valid = False
+ print "ERROR, %s is mandatory, but has not been specified" % (attr)
+ if not valid:
+ return 1
+
+ # Make sure each attribute is a member of the set of valid attributes
+ valid = True
+ for attr,value in pairs.items():
+ if attr not in radius_attrs:
+ valid = False
+ print "ERROR, %s is not a valid attribute" % (attr)
+ if not valid:
+ print "Valid attributes are:"
+ print ipautil.format_list(radius_attrs, quote='"')
+ return 1
+
+ # Makse sure each value is valid
+ valid = True
+ for attr,value in pairs.items():
+ if not radius_util.validate(attr, value):
+ valid = False
+ if not valid:
+ return 1
+
+ # Dump what we've got so far
+ if options.verbose:
+ print "Pairs:"
+ for attr,value in pairs.items():
+ print "\t%s = %s" % (attr, value)
+
+ radius_entity = radius_util.RadiusProfile()
+ for attr,value in pairs.items():
+ radius_entity.setValue(radius_attr_to_ldap_attr[attr], value)
+
+ try:
+ ipa_client = ipaclient.IPAClient()
+ ipa_client.add_radius_profile(radius_entity)
+ print "successfully added"
+ except xmlrpclib.Fault, f:
+ print f.faultString
+ return 1
+ except kerberos.GSSError, e:
+ print "Could not initialize GSSAPI: %s/%s" % (e[0][0][0], e[0][1][0])
+ return 1
+ except xmlrpclib.ProtocolError, e:
+ print "Unable to connect to IPA server: %s" % (e.errmsg)
+ return 1
+ except ipa.ipaerror.IPAError, e:
+ print "%s" % (e.message)
+ return 1
+
+ return 0
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/ipa-radius-admintools/ipa-delradiusclient b/ipa-radius-admintools/ipa-delradiusclient
new file mode 100644
index 00000000..ee5e2e79
--- /dev/null
+++ b/ipa-radius-admintools/ipa-delradiusclient
@@ -0,0 +1,79 @@
+#! /usr/bin/python -E
+# Authors: John Dennis <jdennis@redhat.com>
+#
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+import os
+import sys
+from optparse import OptionParser
+import ipa
+import ipa.ipaclient as ipaclient
+import ipa.ipavalidate as ipavalidate
+import ipa.config
+import ipa.ipaerror
+import ipa.radius_util as radius_util
+
+import xmlrpclib
+import kerberos
+import ldap
+
+#------------------------------------------------------------------------------
+
+def help_option_callback(option, opt_str, value, parser, *args, **kwargs):
+ parser.print_help()
+ sys.exit(0)
+
+
+def main():
+ opt_parser = OptionParser(add_help_option=False)
+
+ opt_parser.add_option("-h", "--help", action="callback", callback=help_option_callback,
+ help="detailed help information")
+ opt_parser.set_usage("Usage: %s [options] Client-IP-Address" % (os.path.basename(sys.argv[0])))
+
+ ipa.config.add_standard_options(opt_parser)
+ options, args = opt_parser.parse_args()
+
+ if len(args) < 1:
+ opt_parser.error("missing Client-IP-Address")
+
+ ipa.config.init_config(options)
+
+ ip_addr = args[0]
+
+ try:
+ ipa_client = ipaclient.IPAClient()
+ ipa_client.delete_radius_client(ip_addr)
+ print "successfully deleted"
+ except xmlrpclib.Fault, f:
+ print f.faultString
+ return 1
+ except kerberos.GSSError, e:
+ print "Could not initialize GSSAPI: %s/%s" % (e[0][0][0], e[0][1][0])
+ return 1
+ except xmlrpclib.ProtocolError, e:
+ print "Unable to connect to IPA server: %s" % (e.errmsg)
+ return 1
+ except ipa.ipaerror.IPAError, e:
+ print "%s" % (e.message)
+ return 1
+
+ return 0
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/ipa-radius-admintools/ipa-delradiusprofile b/ipa-radius-admintools/ipa-delradiusprofile
new file mode 100644
index 00000000..d5be88a7
--- /dev/null
+++ b/ipa-radius-admintools/ipa-delradiusprofile
@@ -0,0 +1,87 @@
+#! /usr/bin/python -E
+# Authors: John Dennis <jdennis@redhat.com>
+#
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+import os
+import sys
+from optparse import OptionParser
+import ipa
+import ipa.ipaclient as ipaclient
+import ipa.ipavalidate as ipavalidate
+import ipa.config
+import ipa.ipaerror
+import ipa.radius_util as radius_util
+
+import xmlrpclib
+import kerberos
+import ldap
+
+#------------------------------------------------------------------------------
+
+def help_option_callback(option, opt_str, value, parser, *args, **kwargs):
+ parser.print_help()
+ sys.exit(0)
+
+
+def main():
+ opt_parser = OptionParser(add_help_option=False)
+
+ opt_parser.add_option("-s", "--shared", dest="shared", default=False, action='store_true',
+ help="profile is shared")
+ opt_parser.add_option("-h", "--help", action="callback", callback=help_option_callback,
+ help="detailed help information")
+ opt_parser.set_usage("Usage: %s [options] UID" % (os.path.basename(sys.argv[0])))
+
+ ipa.config.add_standard_options(opt_parser)
+ options, args = opt_parser.parse_args()
+
+ if len(args) < 1:
+ opt_parser.error("missing UID")
+
+ ipa.config.init_config(options)
+
+ uid = args[0]
+ user_profile = not options.shared
+
+ # Per user profiles are pre-created (i.e. objectclass radiusprofile is always added for each user)
+ if user_profile:
+ print "ERROR, you cannot delete a per-user radius profile, it always exists"
+ return 1
+
+ try:
+ ipa_client = ipaclient.IPAClient()
+ ipa_client.delete_radius_profile(uid, user_profile)
+ print "successfully deleted"
+ except xmlrpclib.Fault, f:
+ print f.faultString
+ return 1
+ except kerberos.GSSError, e:
+ print "Could not initialize GSSAPI: %s/%s" % (e[0][0][0], e[0][1][0])
+ return 1
+ except xmlrpclib.ProtocolError, e:
+ print "Unable to connect to IPA server: %s" % (e.errmsg)
+ return 1
+ except ipa.ipaerror.IPAError, e:
+ print "%s" % (e.message)
+ return 1
+
+ return 0
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/ipa-radius-admintools/ipa-findradiusclient b/ipa-radius-admintools/ipa-findradiusclient
new file mode 100644
index 00000000..12bb9bcf
--- /dev/null
+++ b/ipa-radius-admintools/ipa-findradiusclient
@@ -0,0 +1,106 @@
+#! /usr/bin/python -E
+# Authors: John Dennis <jdennis@redhat.com>
+#
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+import os
+import sys
+from optparse import OptionParser
+import ipa
+from ipa import radius_util
+import ipa.ipaclient as ipaclient
+import ipa.ipavalidate as ipavalidate
+import ipa.config
+import ipa.ipaerror
+import ipa.ipautil
+
+import xmlrpclib
+import kerberos
+import ldap
+
+#------------------------------------------------------------------------------
+
+attrs = radius_util.radius_client_ldap_attr_to_radius_attr.keys()
+
+#------------------------------------------------------------------------------
+
+def parse_options():
+ return options, args
+
+#------------------------------------------------------------------------------
+
+# FIXME
+def help_option_callback(option, opt_str, value, parser, *args, **kwargs):
+ parser.print_help()
+ print
+ print "Note: Client-IP-Address may contain wildcards, to get all clients use '*'"
+ sys.exit(0)
+
+def main():
+ opt_parser = OptionParser(add_help_option=False)
+ opt_parser.add_option("-h", "--help", action="callback", callback=help_option_callback,
+ help="detailed help information")
+
+ ipa.config.add_standard_options(opt_parser)
+ options, args = opt_parser.parse_args()
+
+ opt_parser.set_usage("Usage: %s [options] Client-IP-Address [Client-IP-Address ...]" % (os.path.basename(sys.argv[0])))
+
+ if len(args) < 1:
+ opt_parser.error("missing Client-IP-Address(es)")
+
+ ipa.config.init_config(options)
+
+ ip_addrs = args
+
+ try:
+ ipa_client = ipaclient.IPAClient()
+ radius_clients = ipa_client.find_radius_clients(ip_addrs, sattrs=attrs)
+ counter = radius_clients[0]
+ radius_clients = radius_clients[1:]
+
+ if counter == 0:
+ print "No entries found for", ip_addrs
+ return 2
+
+ for radius_client in radius_clients:
+ client_attrs = radius_client.attrList()
+ client_attrs.sort()
+
+ print "%s:" % radius_client.getValues(radius_util.radius_client_attr_to_ldap_attr['Client-IP-Address'])
+ for attr in client_attrs:
+ value = radius_client.getValues(attr)
+ print "\t%s = %s" % (radius_util.radius_client_ldap_attr_to_radius_attr[attr], value)
+
+ except xmlrpclib.Fault, f:
+ print f.faultString
+ return 1
+ except kerberos.GSSError, e:
+ print "Could not initialize GSSAPI: %s/%s" % (e[0][0][0], e[0][1][0])
+ return 1
+ except xmlrpclib.ProtocolError, e:
+ print "Unable to connect to IPA server: %s" % (e.errmsg)
+ return 1
+ except ipa.ipaerror.IPAError, e:
+ print "%s" % (e.message)
+ return 1
+
+ return 0
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/ipa-radius-admintools/ipa-findradiusprofile b/ipa-radius-admintools/ipa-findradiusprofile
new file mode 100644
index 00000000..01efa2c0
--- /dev/null
+++ b/ipa-radius-admintools/ipa-findradiusprofile
@@ -0,0 +1,109 @@
+#! /usr/bin/python -E
+# Authors: John Dennis <jdennis@redhat.com>
+#
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+import os
+import sys
+from optparse import OptionParser
+import ipa
+from ipa import radius_util
+import ipa.ipaclient as ipaclient
+import ipa.ipavalidate as ipavalidate
+import ipa.config
+import ipa.ipaerror
+import ipa.ipautil
+
+import xmlrpclib
+import kerberos
+import ldap
+
+#------------------------------------------------------------------------------
+
+attrs = radius_util.radius_profile_ldap_attr_to_radius_attr.keys()
+
+#------------------------------------------------------------------------------
+
+def parse_options():
+ return options, args
+
+#------------------------------------------------------------------------------
+
+# FIXME
+def help_option_callback(option, opt_str, value, parser, *args, **kwargs):
+ parser.print_help()
+ print
+ print "Note: UID may contain wildcards, to get all profiles use '*'"
+ sys.exit(0)
+
+def main():
+ opt_parser = OptionParser(add_help_option=False)
+ opt_parser.add_option("-s", "--shared", dest="shared", default=False, action='store_true',
+ help="profile is shared")
+ opt_parser.add_option("-h", "--help", action="callback", callback=help_option_callback,
+ help="detailed help information")
+
+ ipa.config.add_standard_options(opt_parser)
+ options, args = opt_parser.parse_args()
+
+ opt_parser.set_usage("Usage: %s [options] UID [UID ...]" % (os.path.basename(sys.argv[0])))
+
+ if len(args) < 1:
+ opt_parser.error("missing UID(es)")
+
+ ipa.config.init_config(options)
+
+ uids = args
+ user_profile = not options.shared
+
+ try:
+ ipa_client = ipaclient.IPAClient()
+ radius_profiles = ipa_client.find_radius_profiles(uids, user_profile, sattrs=attrs)
+ counter = radius_profiles[0]
+ radius_profiles = radius_profiles[1:]
+
+ if counter == 0:
+ print "No entries found for", uids
+ return 2
+
+ for radius_profile in radius_profiles:
+ profile_attrs = radius_profile.attrList()
+ profile_attrs.sort()
+
+ print "%s:" % radius_profile.getValues(radius_util.radius_profile_attr_to_ldap_attr['UID'])
+ for attr in profile_attrs:
+ value = radius_profile.getValues(attr)
+ print "\t%s = %s" % (radius_util.radius_profile_ldap_attr_to_radius_attr[attr], value)
+
+ except xmlrpclib.Fault, f:
+ print f.faultString
+ return 1
+ except kerberos.GSSError, e:
+ print "Could not initialize GSSAPI: %s/%s" % (e[0][0][0], e[0][1][0])
+ return 1
+ except xmlrpclib.ProtocolError, e:
+ print "Unable to connect to IPA server: %s" % (e.errmsg)
+ return 1
+ except ipa.ipaerror.IPAError, e:
+ print "%s" % (e.message)
+ return 1
+
+ return 0
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/ipa-radius-admintools/ipa-modradiusclient b/ipa-radius-admintools/ipa-modradiusclient
new file mode 100644
index 00000000..2a37929e
--- /dev/null
+++ b/ipa-radius-admintools/ipa-modradiusclient
@@ -0,0 +1,275 @@
+#! /usr/bin/python -E
+# Authors: John Dennis <jdennis@redhat.com>
+#
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+import sys
+import os
+from optparse import OptionParser
+from sets import Set
+
+import ipa.ipaclient as ipaclient
+import ipa.ipautil as ipautil
+import ipa.config
+import ipa.ipaerror
+import ipa.radius_util as radius_util
+
+import xmlrpclib
+import kerberos
+import ldap
+
+#------------------------------------------------------------------------------
+
+radius_attrs = radius_util.radius_client_attr_to_ldap_attr.keys()
+radius_attr_to_ldap_attr = radius_util.radius_client_attr_to_ldap_attr
+ldap_attr_to_radius_attr = radius_util.radius_client_ldap_attr_to_radius_attr
+mandatory_radius_attrs = ['Client-IP-Address', 'Secret']
+distinguished_attr = 'Client-IP-Address'
+
+#------------------------------------------------------------------------------
+
+def help_option_callback(option, opt_str, value, parser, *args, **kwargs):
+ parser.print_help()
+ print
+ print "Valid interative attributes are:"
+ print ipautil.format_list(radius_attrs, quote='"')
+ print
+ print "Required attributes are:"
+ print ipautil.format_list(mandatory_radius_attrs, quote='"')
+ sys.exit(0)
+
+def main():
+ opt_parser = OptionParser(add_help_option=False)
+
+ opt_parser.add_option("-a", "--Client-IP-Address", dest="ip_addr",
+ help="RADIUS client ip address")
+ opt_parser.add_option("-s", "--Secret", dest="secret",
+ help="RADIUS client ip address")
+ opt_parser.add_option("-n", "--Name", dest="name",
+ help="RADIUS client name")
+ opt_parser.add_option("-t", "--NAS-Type", dest="nastype",
+ help="RADIUS client NAS Type")
+ opt_parser.add_option("-d", "--Description", dest="desc",
+ help="description of the RADIUS client")
+
+ opt_parser.add_option("-D", "--delete-attrs", dest="delete_attrs", action='store_true', default=False,
+ help="delete the specified attributes")
+ opt_parser.add_option("-h", "--help", action="callback", callback=help_option_callback,
+ help="detailed help information")
+ opt_parser.add_option("-i", "--interactive", dest="interactive", action='store_true', default=False,
+ help="interactive mode, prompts with auto-completion")
+ opt_parser.add_option("-A", "--attr", dest="attrs", action='append',
+ help="If adding or modifying then this argument specifies one or more attribute=value pair(s), value may be optionally quoted, pairs are seperated by whitespace. If deleting attributes then this argument specifies one or more attribute names seperated by whitespace or commas")
+ opt_parser.add_option("-f", "--file", dest="data_file",
+ help="If adding or modifying then attribute=value pair(s) are read from file, value may be optionally quoted, pairs are delimited by whitespace. If deleting attributes then attributes are read from file, attributes are seperated by whitespace or commas. Reads from stdin if file is -")
+ opt_parser.add_option("-v", "--verbose", dest="verbose", action='store_true',
+ help="print information")
+
+ opt_parser.set_usage("Usage: %s [options] %s" % (distinguished_attr, os.path.basename(sys.argv[0])))
+
+ ipa.config.add_standard_options(opt_parser)
+ options, args = opt_parser.parse_args()
+
+ if len(args) < 1:
+ opt_parser.error('missing %s' % (distinguished_attr))
+
+ ipa.config.init_config(options)
+
+ ip_addr = args[0]
+
+ # Verify entity previously exists and get current values
+ ipa_client = ipaclient.IPAClient()
+ try:
+ radius_entity = ipa_client.get_radius_client_by_ip_addr(ip_addr)
+ except ipa.ipaerror.exception_for(ipa.ipaerror.LDAP_NOT_FOUND):
+ print "client %s not found" % ip_addr
+ return 1
+ except ipa.ipaerror.IPAError, e:
+ print "%s" % e.message
+ return 1
+ except kerberos.GSSError, e:
+ print "Could not initialize GSSAPI: %s/%s" % (e[0][0][0], e[0][1][0])
+ return 1
+
+ # Deleteing attributes is fundamentally different than adding/modifying an attribute.
+ # When adding/modifying there is always a value the attribute is paired with,
+ # so handle the two cases independently.
+ if options.delete_attrs:
+ attrs = Set()
+ # Get attrs from a file or stdin
+ if options.data_file:
+ try:
+ items = ipautil.read_items_file(options.data_file)
+ attrs.update(items)
+ except Exception, e:
+ print "ERROR, could not read attrs (%s)" % (e)
+
+ # Get attrs specified on the command line as a named argument
+ if options.secret is not None: attrs.add('Secret')
+ if options.name is not None: attrs.add('Name')
+ if options.nastype is not None: attrs.add('NAS-Type')
+ if options.desc is not None: attrs.add('Description')
+
+ # Get attrs specified on the command line as a attr argument
+ if options.attrs:
+ for a in options.attrs:
+ items = ipautil.parse_items(a)
+ attrs.update(items)
+
+ # Get attrs interactively
+ if options.interactive:
+ deletable_attrs = []
+ for radius_attr in radius_attrs:
+ if radius_attr in mandatory_radius_attrs: continue
+ if radius_entity.hasAttr(radius_attr_to_ldap_attr[radius_attr]):
+ deletable_attrs.append(radius_attr)
+
+ if deletable_attrs:
+ c = ipautil.ItemCompleter(deletable_attrs)
+ c.open()
+ items = c.get_items("Enter: ")
+ attrs.update(items)
+ c.close()
+
+ # Data collection done, assure no mandatory attrs are in the delete list
+ valid = True
+ for attr in mandatory_radius_attrs:
+ if attr in attrs:
+ valid = False
+ print "ERROR, %s is mandatory, but is set to be deleted" % (attr)
+ if not valid:
+ return 1
+
+ # Make sure each attribute is a member of the set of valid attributes
+ valid = True
+ for attr in attrs:
+ if attr not in radius_attrs:
+ valid = False
+ print "ERROR, %s is not a valid attribute" % (attr)
+ if not valid:
+ print "Valid attributes are:"
+ print ipautil.format_list(radius_attrs, quote='"')
+ return 1
+
+ # Dump what we've got so far
+ if options.verbose:
+ print "Attributes:"
+ for attr in attrs:
+ print "\t%s" % (attr)
+
+ for attr in attrs:
+ radius_entity.delValue(radius_attr_to_ldap_attr[attr])
+
+ else:
+ pairs = {}
+ pairs[distinguished_attr] = ip_addr
+
+ # Populate the pair list with pre-existing values
+ for attr in radius_attrs:
+ value = radius_entity.getValues(radius_attr_to_ldap_attr[attr])
+ if value is None: continue
+ pairs[attr] = value
+
+ # Get pairs from a file or stdin
+ if options.data_file:
+ try:
+ av = ipautil.read_pairs_file(options.data_file)
+ pairs.update(av)
+ except Exception, e:
+ print "ERROR, could not read pairs (%s)" % (e)
+
+ # Get pairs specified on the command line as a named argument
+ if options.ip_addr is not None: pairs[distinguished_attr] = options.ip_addr
+ if options.secret is not None: pairs['Secret'] = options.secret
+ if options.name is not None: pairs['Name'] = options.name
+ if options.nastype is not None: pairs['NAS-Type'] = options.nastype
+ if options.desc is not None: pairs['Description'] = options.desc
+
+ # Get pairs specified on the command line as a pair argument
+ if options.attrs:
+ for p in options.attrs:
+ av = ipautil.parse_key_value_pairs(p)
+ pairs.update(av)
+
+ # Get pairs interactively
+ if options.interactive:
+ prompted_attrs = radius_attrs[:]
+ prompted_attrs.remove(distinguished_attr)
+ c = ipautil.AttributeValueCompleter(prompted_attrs, pairs)
+ c.open()
+ av = c.get_pairs("Enter: ", validate_callback=radius_util.validate)
+ pairs.update(av)
+ c.close()
+
+ # FIXME: validation should be moved to xmlrpc server
+
+ # Data collection done, assure mandatory data has been specified
+
+ if pairs.has_key(distinguished_attr) and pairs[distinguished_attr] != ip_addr:
+ print "ERROR, %s specified on command line (%s) does not match value found in pairs (%s)" % \
+ (distinguished_attr, ip_addr, pairs[distinguished_attr])
+ return 1
+
+ # Make sure each attribute is a member of the set of valid attributes
+ valid = True
+ for attr,value in pairs.items():
+ if attr not in radius_attrs:
+ valid = False
+ print "ERROR, %s is not a valid attribute" % (attr)
+ if not valid:
+ print "Valid attributes are:"
+ print ipautil.format_list(radius_attrs, quote='"')
+ return 1
+
+ # Makse sure each value is valid
+ valid = True
+ for attr,value in pairs.items():
+ if not radius_util.validate(attr, value):
+ valid = False
+ if not valid:
+ return 1
+
+ # Dump what we've got so far
+ if options.verbose:
+ print "Pairs:"
+ for attr,value in pairs.items():
+ print "\t%s = %s" % (attr, value)
+
+ for attr,value in pairs.items():
+ radius_entity.setValue(radius_attr_to_ldap_attr[attr], value)
+
+ try:
+ ipa_client.update_radius_client(radius_entity)
+ print "successfully modified"
+ except xmlrpclib.Fault, f:
+ print f.faultString
+ return 1
+ except kerberos.GSSError, e:
+ print "Could not initialize GSSAPI: %s/%s" % (e[0][0][0], e[0][1][0])
+ return 1
+ except xmlrpclib.ProtocolError, e:
+ print "Unable to connect to IPA server: %s" % (e.errmsg)
+ return 1
+ except ipa.ipaerror.IPAError, e:
+ print "%s" % (e.message)
+ return 1
+
+ return 0
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/ipa-radius-admintools/ipa-modradiusprofile b/ipa-radius-admintools/ipa-modradiusprofile
new file mode 100644
index 00000000..20586be5
--- /dev/null
+++ b/ipa-radius-admintools/ipa-modradiusprofile
@@ -0,0 +1,265 @@
+#! /usr/bin/python -E
+# Authors: John Dennis <jdennis@redhat.com>
+#
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+import sys
+import os
+from optparse import OptionParser
+from sets import Set
+
+import ipa.ipaclient as ipaclient
+import ipa.ipautil as ipautil
+import ipa.config
+import ipa.ipaerror
+import ipa.radius_util as radius_util
+
+import xmlrpclib
+import kerberos
+import ldap
+
+#------------------------------------------------------------------------------
+
+radius_attrs = radius_util.radius_profile_attr_to_ldap_attr.keys()
+radius_attr_to_ldap_attr = radius_util.radius_profile_attr_to_ldap_attr
+ldap_attr_to_radius_attr = radius_util.radius_profile_ldap_attr_to_radius_attr
+mandatory_radius_attrs = ['UID']
+distinguished_attr = 'UID'
+
+#------------------------------------------------------------------------------
+
+def help_option_callback(option, opt_str, value, parser, *args, **kwargs):
+ parser.print_help()
+ print
+ print "Valid interative attributes are:"
+ print ipautil.format_list(radius_attrs, quote='"')
+ print
+ print "Required attributes are:"
+ print ipautil.format_list(mandatory_radius_attrs, quote='"')
+ sys.exit(0)
+
+def main():
+ opt_parser = OptionParser(add_help_option=False)
+
+ opt_parser.add_option("-u", "--uid", dest="uid",
+ help="RADIUS profile identifier")
+ opt_parser.add_option("-s", "--shared", dest="shared", default=False, action='store_true',
+ help="profile is shared")
+ opt_parser.add_option("-d", "--Description", dest="desc",
+ help="description of the RADIUS client")
+
+ opt_parser.add_option("-D", "--delete-attrs", dest="delete_attrs", action='store_true', default=False,
+ help="delete the specified attributes")
+ opt_parser.add_option("-h", "--help", action="callback", callback=help_option_callback,
+ help="detailed help information")
+ opt_parser.add_option("-i", "--interactive", dest="interactive", action='store_true', default=False,
+ help="interactive mode, prompts with auto-completion")
+ opt_parser.add_option("-A", "--attr", dest="attrs", action='append',
+ help="If adding or modifying then this argument specifies one or more attribute=value pair(s), value may be optionally quoted, pairs are seperated by whitespace. If deleting attributes then this argument specifies one or more attribute names seperated by whitespace or commas")
+ opt_parser.add_option("-f", "--file", dest="data_file",
+ help="If adding or modifying then attribute=value pair(s) are read from file, value may be optionally quoted, pairs are delimited by whitespace. If deleting attributes then attributes are read from file, attributes are seperated by whitespace or commas. Reads from stdin if file is -")
+ opt_parser.add_option("-v", "--verbose", dest="verbose", action='store_true',
+ help="print information")
+
+ opt_parser.set_usage("Usage: %s [options] %s" % (distinguished_attr, os.path.basename(sys.argv[0])))
+
+ ipa.config.add_standard_options(opt_parser)
+ options, args = opt_parser.parse_args()
+
+ if len(args) < 1:
+ opt_parser.error('missing %s' % (distinguished_attr))
+
+ ipa.config.init_config(options)
+
+ uid = args[0]
+ user_profile = not options.shared
+
+ # Verify entity previously exists and get current values
+ ipa_client = ipaclient.IPAClient()
+ try:
+ radius_entity = ipa_client.get_radius_profile_by_uid(uid, user_profile)
+ except ipa.ipaerror.exception_for(ipa.ipaerror.LDAP_NOT_FOUND):
+ print "profile %s not found" % uid
+ return 1
+ except ipa.ipaerror.IPAError, e:
+ print "%s" % e.message
+ return 1
+ except kerberos.GSSError, e:
+ print "Could not initialize GSSAPI: %s/%s" % (e[0][0][0], e[0][1][0])
+ return 1
+
+ # Deleteing attributes is fundamentally different than adding/modifying an attribute.
+ # When adding/modifying there is always a value the attribute is paired with,
+ # so handle the two cases independently.
+ if options.delete_attrs:
+ attrs = Set()
+ # Get attrs from a file or stdin
+ if options.data_file:
+ try:
+ items = ipautil.read_items_file(options.data_file)
+ attrs.update(items)
+ except Exception, e:
+ print "ERROR, could not read attrs (%s)" % (e)
+
+ # Get attrs specified on the command line as a named argument
+ if options.desc is not None: attrs.add('Description')
+
+ # Get attrs specified on the command line as a attr argument
+ if options.attrs:
+ for a in options.attrs:
+ items = ipautil.parse_items(a)
+ attrs.update(items)
+
+ # Get attrs interactively
+ if options.interactive:
+ deletable_attrs = []
+ for radius_attr in radius_attrs:
+ if radius_attr in mandatory_radius_attrs: continue
+ if radius_entity.hasAttr(radius_attr_to_ldap_attr[radius_attr]):
+ deletable_attrs.append(radius_attr)
+
+ if deletable_attrs:
+ c = ipautil.ItemCompleter(deletable_attrs)
+ c.open()
+ items = c.get_items("Enter: ")
+ attrs.update(items)
+ c.close()
+
+ # Data collection done, assure no mandatory attrs are in the delete list
+ valid = True
+ for attr in mandatory_radius_attrs:
+ if attr in attrs:
+ valid = False
+ print "ERROR, %s is mandatory, but is set to be deleted" % (attr)
+ if not valid:
+ return 1
+
+ # Make sure each attribute is a member of the set of valid attributes
+ valid = True
+ for attr in attrs:
+ if attr not in radius_attrs:
+ valid = False
+ print "ERROR, %s is not a valid attribute" % (attr)
+ if not valid:
+ print "Valid attributes are:"
+ print ipautil.format_list(radius_attrs, quote='"')
+ return 1
+
+ # Dump what we've got so far
+ if options.verbose:
+ print "Attributes:"
+ for attr in attrs:
+ print "\t%s" % (attr)
+
+ for attr in attrs:
+ radius_entity.delValue(radius_attr_to_ldap_attr[attr])
+
+ else:
+ pairs = {}
+ pairs[distinguished_attr] = uid
+
+ # Populate the pair list with pre-existing values
+ for attr in radius_attrs:
+ value = radius_entity.getValues(radius_attr_to_ldap_attr[attr])
+ if value is None: continue
+ pairs[attr] = value
+
+ # Get pairs from a file or stdin
+ if options.data_file:
+ try:
+ av = ipautil.read_pairs_file(options.data_file)
+ pairs.update(av)
+ except Exception, e:
+ print "ERROR, could not read pairs (%s)" % (e)
+
+ # Get pairs specified on the command line as a named argument
+ if options.desc is not None: pairs['Description'] = options.desc
+
+ # Get pairs specified on the command line as a pair argument
+ if options.attrs:
+ for p in options.attrs:
+ av = ipautil.parse_key_value_pairs(p)
+ pairs.update(av)
+
+ # Get pairs interactively
+ if options.interactive:
+ prompted_attrs = radius_attrs[:]
+ prompted_attrs.remove(distinguished_attr)
+ c = ipautil.AttributeValueCompleter(prompted_attrs, pairs)
+ c.open()
+ av = c.get_pairs("Enter: ", validate_callback=radius_util.validate)
+ pairs.update(av)
+ c.close()
+
+ # FIXME: validation should be moved to xmlrpc server
+
+ # Data collection done, assure mandatory data has been specified
+
+ if pairs.has_key(distinguished_attr) and pairs[distinguished_attr] != uid:
+ print "ERROR, %s specified on command line (%s) does not match value found in pairs (%s)" % \
+ (distinguished_attr, uid, pairs[distinguished_attr])
+ return 1
+
+ # Make sure each attribute is a member of the set of valid attributes
+ valid = True
+ for attr,value in pairs.items():
+ if attr not in radius_attrs:
+ valid = False
+ print "ERROR, %s is not a valid attribute" % (attr)
+ if not valid:
+ print "Valid attributes are:"
+ print ipautil.format_list(radius_attrs, quote='"')
+ return 1
+
+ # Makse sure each value is valid
+ valid = True
+ for attr,value in pairs.items():
+ if not radius_util.validate(attr, value):
+ valid = False
+ if not valid:
+ return 1
+
+ # Dump what we've got so far
+ if options.verbose:
+ print "Pairs:"
+ for attr,value in pairs.items():
+ print "\t%s = %s" % (attr, value)
+
+ for attr,value in pairs.items():
+ radius_entity.setValue(radius_attr_to_ldap_attr[attr], value)
+
+ try:
+ ipa_client.update_radius_profile(radius_entity)
+ print "successfully modified"
+ except xmlrpclib.Fault, f:
+ print f.faultString
+ return 1
+ except kerberos.GSSError, e:
+ print "Could not initialize GSSAPI: %s/%s" % (e[0][0][0], e[0][1][0])
+ return 1
+ except xmlrpclib.ProtocolError, e:
+ print "Unable to connect to IPA server: %s" % (e.errmsg)
+ return 1
+ except ipa.ipaerror.IPAError, e:
+ print "%s" % (e.message)
+ return 1
+
+ return 0
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/ipa-radius-admintools/ipa-radius-admintools.spec.in b/ipa-radius-admintools/ipa-radius-admintools.spec.in
new file mode 100644
index 00000000..f0066aca
--- /dev/null
+++ b/ipa-radius-admintools/ipa-radius-admintools.spec.in
@@ -0,0 +1,53 @@
+Name: ipa-radius-admintools
+Version: __VERSION__
+Release: __RELEASE__%{?dist}
+Summary: IPA authentication server - radius admin tools
+
+Group: System Environment/Base
+License: GPLv2
+URL: http://www.freeipa.org
+Source0: %{name}-%{version}.tgz
+BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n)
+BuildArch: noarch
+
+Requires: python python-krbV ipa-python ipa-admintools
+
+%description
+IPA is a server for identity, policy, and audit.
+
+%prep
+%setup -q
+
+%install
+rm -rf %{buildroot}
+mkdir -p %{buildroot}%{_sbindir}
+
+make install DESTDIR=%{buildroot}
+
+
+%clean
+rm -rf %{buildroot}
+
+
+%files
+%defattr(-,root,root,-)
+%{_sbindir}/ipa*
+
+%changelog
+* Thu Apr 3 2008 Rob Crittenden <rcritten@redhat.com> - 1.0.0-1
+- Version bump for release
+
+* Thu Feb 21 2008 Rob Crittenden <rcritten@redhat.com> - 0.99.0-1
+- Version bump for release
+
+* Thu Jan 31 2008 Rob Crittenden <rcritten@redhat.com> - 0.6.0-3
+- Marked with wrong license. IPA is GPLv2.
+
+* Thu Jan 17 2008 Rob Crittenden <rcritten@redhat.com> - 0.6.0-2
+- Fixed License in specfile
+
+* Fri Dec 21 2007 Karl MacMillan <kmacmill@redhat.com> - 0.6.0-1
+- Version bump for release
+
+* Tue Dec 18 2007 Karl MacMillan <kmacmill@redhat.com> - 0.5.0
+- Initial rpm version
diff --git a/ipa-radius-server/Makefile b/ipa-radius-server/Makefile
new file mode 100644
index 00000000..85617b9b
--- /dev/null
+++ b/ipa-radius-server/Makefile
@@ -0,0 +1,23 @@
+PLUGINS_SHARE = $(DESTDIR)/usr/share/ipa/plugins
+PLUGINS_PYTHON = $(DESTDIR)/usr/share/ipa/ipaserver/plugins
+SBINDIR = $(DESTDIR)/usr/sbin
+
+all:
+
+install:
+ -mkdir -p $(PLUGINS_SHARE)
+ -mkdir -p $(PLUGINS_PYTHON)
+ -mkdir -p $(SBINDIR)
+ install -m 644 plugins/*.py $(PLUGINS_PYTHON)
+ install -m 644 share/*.template $(PLUGINS_SHARE)
+ install -m 755 ipa-radius-install $(SBINDIR)
+
+clean:
+ rm -fr *.pyc *~
+
+distclean: clean
+ rm -fr ipa-radius-server.spec
+
+maintainer-clean: distclean
+
+test:
diff --git a/ipa-radius-server/ipa-radius-install b/ipa-radius-server/ipa-radius-install
new file mode 100644
index 00000000..8101536d
--- /dev/null
+++ b/ipa-radius-server/ipa-radius-install
@@ -0,0 +1,71 @@
+#! /usr/bin/python -E
+# Authors: Karl MacMillan <kmacmillan@mentalrootkit.com>
+#
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+import sys
+
+import traceback, logging, krbV
+
+from ipaserver import installutils
+from ipaserver.plugins import radiusinstance
+
+from ipa import ipautil
+
+def get_host_name():
+ hostname = installutils.get_fqdn()
+ try:
+ installutils.verify_fqdn(hostname)
+ except RuntimeError, e:
+ logging.error(str(e))
+ sys.exit(1)
+
+ return hostname
+
+def get_realm_name():
+ c = krbV.default_context()
+ return c.default_realm
+
+def main():
+ if not ipautil.file_exists("/etc/ipa/ipa.conf"):
+ print "This system does not appear to have IPA configured."
+ print "Has ipa-server-install been run?"
+ if not ipautil.user_input("Continue with radius install?", False):
+ sys.exit(1)
+
+ installutils.standard_logging_setup("iparadius-install.log", False)
+
+ host_name = get_host_name()
+
+ realm_name = get_realm_name()
+
+ # Create a radius instance
+ radius = radiusinstance.RadiusInstance()
+ # FIXME: ldap_server should be derived, not hardcoded to localhost, also should it be a URL?
+ radius.create_instance(realm_name, host_name, 'localhost')
+
+
+try:
+ main()
+except Exception, e:
+ message = "Unexpected error - see iparadius-install.log for details:\n %s" % str(e)
+ print message
+ message = str(e)
+ for str in traceback.format_tb(sys.exc_info()[2]):
+ message = message + "\n" + str
+ logging.debug(message)
diff --git a/ipa-radius-server/ipa-radius-server.spec.in b/ipa-radius-server/ipa-radius-server.spec.in
new file mode 100755
index 00000000..b29dfc78
--- /dev/null
+++ b/ipa-radius-server/ipa-radius-server.spec.in
@@ -0,0 +1,61 @@
+Name: ipa-radius-server
+Version: __VERSION__
+Release: __RELEASE__%{?dist}
+Summary: IPA authentication server - radius plugin
+
+Group: System Environment/Base
+License: GPLv2
+URL: http://www.freeipa.org
+Source0: %{name}-%{version}.tgz
+BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n)
+BuildArch: noarch
+
+Requires: python
+Requires: ipa-server
+Requires: freeradius
+
+%description
+Radius plugin for an IPA server
+
+%prep
+%setup -q
+
+%install
+rm -rf %{buildroot}
+mkdir -p %{buildroot}%{_sbindir}
+
+make install DESTDIR=%{buildroot}
+
+
+%clean
+rm -rf %{buildroot}
+
+
+%files
+%defattr(-,root,root,-)
+%{_sbindir}/ipa*
+
+%dir %{_usr}/share/ipa/plugins
+%{_usr}/share/ipa/plugins/*
+
+%dir %{_usr}/share/ipa/ipaserver/plugins
+%{_usr}/share/ipa/ipaserver/plugins/*
+
+%changelog
+* Thu Apr 3 2008 Rob Crittenden <rcritten@redhat.com> - 1.0.0-1
+- Version bump for release
+
+* Thu Feb 21 2008 Rob Crittenden <rcritten@redhat.com> - 0.99.0-1
+- Version bump for release
+
+* Thu Jan 31 2008 Rob Crittenden <rcritten@redhat.com> - 0.6.0-3
+- Marked with wrong license. IPA is GPLv2.
+
+* Thu Jan 17 2008 Rob Crittenden <rcritten@redhat.com> - 0.6.0-2
+- Fixed License in specfile
+
+* Fri Dec 21 2007 Karl MacMillan <kmacmill@redhat.com> - 0.6.0-1
+- Version bump for release
+
+* Wed Dec 12 2007 Karl MacMillan <kmacmill@redhat.com> - 0.5.0-1
+- Initial version
diff --git a/ipa-radius-server/plugins/__init__.py b/ipa-radius-server/plugins/__init__.py
new file mode 100644
index 00000000..636bc1a8
--- /dev/null
+++ b/ipa-radius-server/plugins/__init__.py
@@ -0,0 +1 @@
+# intentionally empty
diff --git a/ipa-radius-server/plugins/radiusinstance.py b/ipa-radius-server/plugins/radiusinstance.py
new file mode 100644
index 00000000..1dd5e669
--- /dev/null
+++ b/ipa-radius-server/plugins/radiusinstance.py
@@ -0,0 +1,170 @@
+# Authors: John Dennis <jdennis@redhat.com>
+#
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+import subprocess
+import string
+import tempfile
+import shutil
+import logging
+import pwd
+import time
+import sys
+from ipa import ipautil
+from ipa import radius_util
+
+from ipaserver import service
+
+import os
+import re
+
+IPA_RADIUS_VERSION = '0.0.0'
+
+# FIXME there should a utility to get the user base dn
+from ipaserver.funcs import DefaultUserContainer, DefaultGroupContainer
+
+#-------------------------------------------------------------------------------
+
+def get_radius_version():
+ version = None
+ try:
+ p = subprocess.Popen([radius_util.RADIUSD, '-v'], stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE)
+ stdout, stderr = p.communicate()
+ status = p.returncode
+
+ if status == 0:
+ match = re.search("radiusd: FreeRADIUS Version (.+), for host", stdout)
+ if match:
+ version = match.group(1)
+ except Exception, e:
+ pass
+ return version
+
+
+#-------------------------------------------------------------------------------
+
+class RadiusInstance(service.Service):
+ def __init__(self):
+ service.Service.__init__(self, "radiusd")
+ self.fqdn = None
+ self.realm = None
+ self.principal = None
+
+ def create_instance(self, realm_name, host_name, ldap_server):
+ self.realm = realm_name.upper()
+ self.suffix = ipautil.realm_to_suffix(self.realm)
+ self.fqdn = host_name
+ self.ldap_server = ldap_server
+ self.principal = "%s/%s@%s" % (radius_util.RADIUS_SERVICE_NAME, self.fqdn, self.realm)
+ self.basedn = self.suffix
+ self.user_basedn = "%s,%s" % (DefaultUserContainer, self.basedn) # FIXME, should be utility to get this
+ self.radius_version = get_radius_version()
+
+ try:
+ self.stop()
+ except:
+ # It could have been not running
+ pass
+
+ self.step("create radiusd keytab", self.__create_radius_keytab)
+ self.step("configuring radiusd.conf for radius instance", self.__radiusd_conf)
+ self.step("starting radiusd", self.__start_instance)
+ self.step("configuring radiusd to start on boot", self.chkconfig_on)
+
+ # FIXME:
+ # self.step("setting ldap encrypted attributes", self.__set_ldap_encrypted_attributes)
+
+ self.start_creation("Configuring radiusd")
+
+ def __start_instance(self):
+ try:
+ self.start()
+ except:
+ logging.error("radiusd service failed to start")
+
+ def __radiusd_conf(self):
+ version = 'IPA_RADIUS_VERSION=%s FREE_RADIUS_VERSION=%s' % (IPA_RADIUS_VERSION, self.radius_version)
+ sub_dict = {'CONFIG_FILE_VERSION_INFO' : version,
+ 'LDAP_SERVER' : self.ldap_server,
+ 'RADIUS_KEYTAB' : radius_util.RADIUS_IPA_KEYTAB_FILEPATH,
+ 'RADIUS_PRINCIPAL' : self.principal,
+ 'RADIUS_USER_BASE_DN' : self.user_basedn,
+ 'ACCESS_ATTRIBUTE' : '',
+ 'ACCESS_ATTRIBUTE_DEFAULT' : 'TRUE',
+ 'CLIENTS_BASEDN' : radius_util.radius_clients_basedn(None, self.suffix),
+ 'SUFFIX' : self.suffix,
+ }
+ try:
+ radiusd_conf = ipautil.template_file(radius_util.RADIUSD_CONF_TEMPLATE_FILEPATH, sub_dict)
+ radiusd_fd = open(radius_util.RADIUSD_CONF_FILEPATH, 'w+')
+ radiusd_fd.write(radiusd_conf)
+ radiusd_fd.close()
+ except Exception, e:
+ logging.error("could not create %s: %s", radius_util.RADIUSD_CONF_FILEPATH, e)
+
+ def __create_radius_keytab(self):
+ try:
+ if ipautil.file_exists(radius_util.RADIUS_IPA_KEYTAB_FILEPATH):
+ os.remove(radius_util.RADIUS_IPA_KEYTAB_FILEPATH)
+ except os.error:
+ logging.error("Failed to remove %s", radius_util.RADIUS_IPA_KEYTAB_FILEPATH)
+
+ (kwrite, kread, kerr) = os.popen3("/usr/kerberos/sbin/kadmin.local")
+ kwrite.write("addprinc -randkey %s\n" % (self.principal))
+ kwrite.flush()
+ kwrite.write("ktadd -k %s %s\n" % (radius_util.RADIUS_IPA_KEYTAB_FILEPATH, self.principal))
+ kwrite.flush()
+ kwrite.close()
+ kread.close()
+ kerr.close()
+
+ # give kadmin time to actually write the file before we go on
+ retry = 0
+ while not ipautil.file_exists(radius_util.RADIUS_IPA_KEYTAB_FILEPATH):
+ time.sleep(1)
+ retry += 1
+ if retry > 15:
+ print "Error timed out waiting for kadmin to finish operations\n"
+ sys.exit(1)
+ try:
+ pent = pwd.getpwnam(radius_util.RADIUS_USER)
+ os.chown(radius_util.RADIUS_IPA_KEYTAB_FILEPATH, pent.pw_uid, pent.pw_gid)
+ except Exception, e:
+ logging.error("could not chown on %s to %s: %s", radius_util.RADIUS_IPA_KEYTAB_FILEPATH, radius_util.RADIUS_USER, e)
+
+ def __ldap_mod(self, ldif):
+ txt = iputil.template_file(ipautil.SHARE_DIR + ldif, self.sub_dict)
+ fd = ipautil.write_tmp_file(txt)
+
+ args = ["/usr/bin/ldapmodify", "-h", "127.0.0.1", "-xv",
+ "-D", "cn=Directory Manager", "-w", self.dm_password, "-f", fd.name]
+
+ try:
+ ipautil.run(args)
+ except ipautil.CalledProcessError, e:
+ logging.critical("Failed to load %s: %s" % (ldif, str(e)))
+
+ fd.close()
+
+ #FIXME, should use IPAdmin method
+ def __set_ldap_encrypted_attributes(self):
+ self.__ldap_mod("encrypted_attribute.ldif", {"ENCRYPTED_ATTRIBUTE" : "radiusClientSecret"})
+
+#-------------------------------------------------------------------------------
+
diff --git a/ipa-radius-server/share/radius.radiusd.conf.template b/ipa-radius-server/share/radius.radiusd.conf.template
new file mode 100644
index 00000000..3bc4927d
--- /dev/null
+++ b/ipa-radius-server/share/radius.radiusd.conf.template
@@ -0,0 +1,285 @@
+#
+# WARNING: This file is automatically generated, do not edit
+#
+# $CONFIG_FILE_VERSION_INFO
+#
+prefix = /usr
+exec_prefix = /usr
+sysconfdir = /etc
+localstatedir = /var
+sbindir = /usr/sbin
+logdir = $${localstatedir}/log/radius
+raddbdir = $${sysconfdir}/raddb
+radacctdir = $${logdir}/radacct
+confdir = $${raddbdir}
+run_dir = $${localstatedir}/run/radiusd
+db_dir = $${localstatedir}/lib/radiusd
+log_file = $${logdir}/radius.log
+libdir = /usr/lib
+pidfile = $${run_dir}/radiusd.pid
+user = radiusd
+group = radiusd
+max_request_time = 30
+delete_blocked_requests = no
+cleanup_delay = 5
+max_requests = 1024
+bind_address = *
+port = 0
+hostname_lookups = no
+allow_core_dumps = no
+regular_expressions = yes
+extended_expressions = yes
+log_stripped_names = no
+log_auth = no
+log_auth_badpass = no
+log_auth_goodpass = no
+usercollide = no
+lower_user = no
+lower_pass = no
+nospace_user = no
+nospace_pass = no
+checkrad = $${sbindir}/checkrad
+security {
+ max_attributes = 200
+ reject_delay = 1
+ status_server = no
+}
+proxy_requests = yes
+$$INCLUDE $${confdir}/proxy.conf
+$$INCLUDE $${confdir}/clients.conf
+snmp = no
+$$INCLUDE $${confdir}/snmp.conf
+thread pool {
+ start_servers = 5
+ max_servers = 32
+ min_spare_servers = 3
+ max_spare_servers = 10
+ max_requests_per_server = 0
+}
+modules {
+ chap {
+ authtype = CHAP
+ }
+ pam {
+ pam_auth = radiusd
+ }
+ unix {
+ cache = no
+ cache_reload = 600
+ shadow = /etc/shadow
+ radwtmp = $${logdir}/radwtmp
+ }
+$$INCLUDE $${confdir}/eap.conf
+ mschap {
+ }
+ ldap {
+ server = "$LDAP_SERVER"
+ use_sasl = yes
+ sasl_mech = "GSSAPI"
+ krb_keytab = "$RADIUS_KEYTAB"
+ krb_principal = "$RADIUS_PRINCIPAL"
+ basedn = "$RADIUS_USER_BASE_DN"
+ filter = "(uid=%{Stripped-User-Name:-%{User-Name}})"
+ base_filter = "(objectclass=radiusprofile)"
+ start_tls = no
+ profile_attribute = "radiusProfileDn"
+ default_profile = "uid=ipa_default,cn=profiles,cn=radius,cn=services,cn=etc,$SUFFIX
+ # FIXME: we'll want to toggle the access_attr feature on/off,
+ # but it needs a control, so disable it for now.
+ #access_attr = "$ACCESS_ATTRIBUTE"
+ #access_attr_used_for_allow = "$ACCESS_ATTRIBUTE_DEFAULT"
+ dictionary_mapping = $${raddbdir}/ldap.attrmap
+ ldap_connections_number = 5
+ edir_account_policy_check=no
+ timeout = 4
+ timelimit = 3
+ net_timeout = 1
+ clients_basedn = "$CLIENTS_BASEDN"
+ }
+ realm IPASS {
+ format = prefix
+ delimiter = "/"
+ ignore_default = no
+ ignore_null = no
+ }
+ realm suffix {
+ format = suffix
+ delimiter = "@"
+ ignore_default = no
+ ignore_null = no
+ }
+ realm realmpercent {
+ format = suffix
+ delimiter = "%"
+ ignore_default = no
+ ignore_null = no
+ }
+ realm ntdomain {
+ format = prefix
+ delimiter = "\\"
+ ignore_default = no
+ ignore_null = no
+ }
+ checkval {
+ item-name = Calling-Station-Id
+ check-name = Calling-Station-Id
+ data-type = string
+ }
+ preprocess {
+ huntgroups = $${confdir}/huntgroups
+ hints = $${confdir}/hints
+ with_ascend_hack = no
+ ascend_channels_per_line = 23
+ with_ntdomain_hack = no
+ with_specialix_jetstream_hack = no
+ with_cisco_vsa_hack = no
+ }
+ files {
+ usersfile = $${confdir}/users
+ acctusersfile = $${confdir}/acct_users
+ preproxy_usersfile = $${confdir}/preproxy_users
+ compat = no
+ }
+ detail {
+ detailfile = $${radacctdir}/%{Client-IP-Address}/detail-%Y%m%d
+ detailperm = 0600
+ }
+ acct_unique {
+ key = "User-Name, Acct-Session-Id, NAS-IP-Address, Client-IP-Address, NAS-Port"
+ }
+ radutmp {
+ filename = $${logdir}/radutmp
+ username = %{User-Name}
+ case_sensitive = yes
+ check_with_nas = yes
+ perm = 0600
+ callerid = "yes"
+ }
+ radutmp sradutmp {
+ filename = $${logdir}/sradutmp
+ perm = 0644
+ callerid = "no"
+ }
+ attr_filter {
+ attrsfile = $${confdir}/attrs
+ }
+ counter daily {
+ filename = $${db_dir}/db.daily
+ key = User-Name
+ count-attribute = Acct-Session-Time
+ reset = daily
+ counter-name = Daily-Session-Time
+ check-name = Max-Daily-Session
+ allowed-servicetype = Framed-User
+ cache-size = 5000
+ }
+ sqlcounter dailycounter {
+ counter-name = Daily-Session-Time
+ check-name = Max-Daily-Session
+ reply-name = Session-Timeout
+ sqlmod-inst = sql
+ key = User-Name
+ reset = daily
+ query = "SELECT SUM(AcctSessionTime - \
+ GREATEST((%b - UNIX_TIMESTAMP(AcctStartTime)), 0)) \
+ FROM radacct WHERE UserName='%{%k}' AND \
+ UNIX_TIMESTAMP(AcctStartTime) + AcctSessionTime > '%b'"
+ }
+ sqlcounter monthlycounter {
+ counter-name = Monthly-Session-Time
+ check-name = Max-Monthly-Session
+ reply-name = Session-Timeout
+ sqlmod-inst = sql
+ key = User-Name
+ reset = monthly
+ query = "SELECT SUM(AcctSessionTime - \
+ GREATEST((%b - UNIX_TIMESTAMP(AcctStartTime)), 0)) \
+ FROM radacct WHERE UserName='%{%k}' AND \
+ UNIX_TIMESTAMP(AcctStartTime) + AcctSessionTime > '%b'"
+ }
+ always fail {
+ rcode = fail
+ }
+ always reject {
+ rcode = reject
+ }
+ always ok {
+ rcode = ok
+ simulcount = 0
+ mpp = no
+ }
+ expr {
+ }
+ digest {
+ }
+ exec {
+ wait = yes
+ input_pairs = request
+ }
+ exec echo {
+ wait = yes
+ program = "/bin/echo %{User-Name}"
+ input_pairs = request
+ output_pairs = reply
+ }
+ ippool main_pool {
+ range-start = 192.168.1.1
+ range-stop = 192.168.3.254
+ netmask = 255.255.255.0
+ cache-size = 800
+ session-db = $${db_dir}/db.ippool
+ ip-index = $${db_dir}/db.ipindex
+ override = no
+ maximum-timeout = 0
+ }
+ krb5 {
+ keytab = "$RADIUS_KEYTAB"
+ service_principal = "$RADIUS_PRINCIPAL"
+ }
+}
+instantiate {
+ exec
+ expr
+}
+authorize {
+ preprocess
+ chap
+ mschap
+ suffix
+ eap
+ #files
+ ldap
+}
+authenticate {
+ Auth-Type CHAP {
+ chap
+ }
+ Auth-Type MS-CHAP {
+ mschap
+ }
+ eap
+ Auth-Type Kerberos {
+ krb5
+ }
+}
+preacct {
+ preprocess
+ acct_unique
+ suffix
+ files
+}
+accounting {
+ detail
+ unix
+ radutmp
+}
+session {
+ radutmp
+}
+post-auth {
+}
+pre-proxy {
+}
+post-proxy {
+ eap
+}
diff --git a/ipa-server/AUTHORS b/ipa-server/AUTHORS
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/ipa-server/AUTHORS
diff --git a/ipa-server/Makefile.am b/ipa-server/Makefile.am
new file mode 100644
index 00000000..5c85ebae
--- /dev/null
+++ b/ipa-server/Makefile.am
@@ -0,0 +1,91 @@
+# This file will be processed with automake-1.7 to create Makefile.in
+
+AUTOMAKE_OPTIONS = 1.7
+
+NULL =
+
+SUBDIRS = \
+ ipa-gui \
+ ipa-install \
+ ipa-kpasswd \
+ ipaserver \
+ ipa-slapi-plugins \
+ xmlrpc-server \
+ man \
+ $(NULL)
+
+sbin_SCRIPTS = \
+ ipa-upgradeconfig \
+ ipa-fix-CVE-2008-3274 \
+ ipa-ldap-updater \
+ ipa-compat-manage \
+ $(NULL)
+
+install-exec-local:
+ mkdir -p $(DESTDIR)$(localstatedir)/lib/ipa/sysrestore
+ chmod 700 $(DESTDIR)$(localstatedir)/lib/ipa/sysrestore
+ mkdir -p $(DESTDIR)$(localstatedir)/cache/ipa/sessions
+ chmod 700 $(DESTDIR)$(localstatedir)/cache/ipa/sessions
+
+uninstall-local:
+ -rmdir $(DESTDIR)$(localstatedir)/lib/ipa/sysrestore
+ -rmdir $(DESTDIR)$(localstatedir)/lib/ipa
+ -rmdir $(DESTDIR)$(localstatedir)/cache/ipa/sessions
+ -rmdir $(DESTDIR)$(localstatedir)/cache/ipa
+
+EXTRA_DIST = \
+ ipa-server.spec \
+ COPYING \
+ AUTHORS \
+ INSTALL \
+ README \
+ HACKING \
+ NEWS \
+ ChangeLog \
+ $(sbin_SCRIPTS) \
+ $(NULL)
+
+DISTCLEANFILES = \
+ $(NULL)
+
+MAINTAINERCLEANFILES = \
+ *~ \
+ intltool-*.in \
+ compile \
+ configure \
+ COPYING \
+ INSTALL \
+ install-sh \
+ missing \
+ mkinstalldirs \
+ config.guess \
+ ltmain.sh \
+ config.sub \
+ depcomp \
+ Makefile.in \
+ config.h.* \
+ aclocal.m4 \
+ version.m4 \
+ ipa-server.spec \
+ py-compile \
+ $(NULL)
+
+# Creating ChangeLog from hg log (taken from cairo/Makefile.am):
+
+ChangeLog: $(srcdir)/ChangeLog
+
+$(srcdir)/ChangeLog:
+ @if test -d "$(srcdir)/../.hg"; then \
+ (cd "$(srcdir)" && \
+ ./missing --run hg log --verbose) | fmt --split-only > $@.tmp \
+ && mv -f $@.tmp $@ \
+ || ($(RM) $@.tmp; \
+ echo Failed to generate ChangeLog, your ChangeLog may be outdated >&2; \
+ (test -f $@ || echo hg log is required to generate this file >> $@)); \
+ else \
+ test -f $@ || \
+ (echo A hg checkout and hg -log is required to generate ChangeLog >&2 && \
+ echo A hg checkout and hg log is required to generate this file >> $@); \
+ fi
+
+.PHONY: ChangeLog $(srcdir)/ChangeLog
diff --git a/ipa-server/NEWS b/ipa-server/NEWS
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/ipa-server/NEWS
diff --git a/ipa-server/README b/ipa-server/README
new file mode 100644
index 00000000..15e5a816
--- /dev/null
+++ b/ipa-server/README
@@ -0,0 +1,20 @@
+IPA uses Kerberos with an LDAP storage backend and some custom plugins
+to help manage users and passwords.
+
+A UI interface is provided to make user administration and self-service
+possible. A set of command-line utilities that should provide the same
+capabilities is in ipa-admintools.
+
+Firefox
+-------
+
+The Gecko engine provides an interface for managing a user's configuration
+in Javascript. Naturally this is highly protected and the user gets an
+appropriately dire warning when you try to do this. It also requires
+signed javascript.
+
+During installation a signing certificate is created that creates
+and signs /usr/share/ipa/html/configure.jar which contains the javascript
+to update the browser configuration. User's are directed to go to
+/errors/preferencs.html to load this javascript and apply the changes.
+
diff --git a/ipa-server/autogen.sh b/ipa-server/autogen.sh
new file mode 100755
index 00000000..c95b6dbc
--- /dev/null
+++ b/ipa-server/autogen.sh
@@ -0,0 +1,196 @@
+#!/bin/sh
+# Run this to generate all the initial makefiles, etc.
+set -e
+
+PACKAGE=freeipa-server
+
+LIBTOOLIZE=${LIBTOOLIZE-libtoolize}
+LIBTOOLIZE_FLAGS="--copy --force"
+AUTOHEADER=${AUTOHEADER-autoheader}
+AUTOMAKE_FLAGS="--add-missing --gnu"
+AUTOCONF=${AUTOCONF-autoconf}
+
+# automake 1.8 requires autoconf 2.58
+# automake 1.7 requires autoconf 2.54
+automake_min_vers=1.7
+aclocal_min_vers=$automake_min_vers
+autoconf_min_vers=2.54
+libtoolize_min_vers=1.4
+
+# The awk-based string->number conversion we use needs a C locale to work
+# as expected. Setting LC_ALL overrides whether the user set LC_ALL,
+# LC_NUMERIC, or LANG.
+LC_ALL=C
+
+ARGV0=$0
+
+# Allow invocation from a separate build directory; in that case, we change
+# to the source directory to run the auto*, then change back before running configure
+srcdir=`dirname $ARGV0`
+test -z "$srcdir" && srcdir=.
+
+ORIGDIR=`pwd`
+
+cd $srcdir
+
+# Usage:
+# compare_versions MIN_VERSION ACTUAL_VERSION
+# returns true if ACTUAL_VERSION >= MIN_VERSION
+compare_versions() {
+ ch_min_version=$1
+ ch_actual_version=$2
+ ch_status=0
+ IFS="${IFS= }"; ch_save_IFS="$IFS"; IFS="."
+ set $ch_actual_version
+ for ch_min in $ch_min_version; do
+ ch_cur=`echo $1 | sed 's/[^0-9].*$//'`; shift # remove letter suffixes
+ if [ -z "$ch_min" ]; then break; fi
+ if [ -z "$ch_cur" ]; then ch_status=1; break; fi
+ if [ $ch_cur -gt $ch_min ]; then break; fi
+ if [ $ch_cur -lt $ch_min ]; then ch_status=1; break; fi
+ done
+ IFS="$ch_save_IFS"
+ return $ch_status
+}
+
+if ($AUTOCONF --version) < /dev/null > /dev/null 2>&1 ; then
+ if ($AUTOCONF --version | head -n 1 | awk 'NR==1 { if( $(NF) >= '$autoconf_min_vers') \
+ exit 1; exit 0; }');
+ then
+ echo "$ARGV0: ERROR: \`$AUTOCONF' is too old."
+ $AUTOCONF --version
+ echo " (version $autoconf_min_vers or newer is required)"
+ DIE="yes"
+ fi
+else
+ echo $AUTOCONF: command not found
+ echo
+ echo "$ARGV0: ERROR: You must have \`autoconf' installed to compile $PACKAGE."
+ echo " (version $autoconf_min_vers or newer is required)"
+ DIE="yes"
+fi
+
+#
+# Hunt for an appropriate version of automake and aclocal; we can't
+# assume that 'automake' is necessarily the most recent installed version
+#
+# We check automake first to allow it to be a newer version than we know about.
+#
+if test x"$AUTOMAKE" = x || test x"$ACLOCAL" = x ; then
+ am_ver=""
+ for ver in "" "-1.9" "-1.8" "-1.7" ; do
+ am="automake$ver"
+ if ($am --version) < /dev/null > /dev/null 2>&1 ; then
+ if ($am --version | head -n 1 | awk 'NR==1 { if( $(NF) >= '$automake_min_vers') \
+ exit 1; exit 0; }'); then : ; else
+ am_ver=$ver
+ break;
+ fi
+ fi
+ done
+
+ AUTOMAKE=${AUTOMAKE-automake$am_ver}
+ ACLOCAL=${ACLOCAL-aclocal$am_ver}
+fi
+
+#
+# Now repeat the tests with the copies we decided upon and error out if they
+# aren't sufficiently new.
+#
+if ($AUTOMAKE --version) < /dev/null > /dev/null 2>&1 ; then
+ automake_actual_version=`$AUTOMAKE --version | head -n 1 | \
+ sed 's/^.*[ ]\([0-9.]*[a-z]*\).*$/\1/'`
+ if ! compare_versions $automake_min_vers $automake_actual_version; then
+ echo "$ARGV0: ERROR: \`$AUTOMAKE' is too old."
+ $AUTOMAKE --version
+ echo " (version $automake_min_vers or newer is required)"
+ DIE="yes"
+ fi
+ if ($ACLOCAL --version) < /dev/null > /dev/null 2>&1; then
+ aclocal_actual_version=`$ACLOCAL --version | head -n 1 | \
+ sed 's/^.*[ ]\([0-9.]*[a-z]*\).*$/\1/'`
+
+ if ! compare_versions $aclocal_min_vers $aclocal_actual_version; then
+ echo "$ARGV0: ERROR: \`$ACLOCAL' is too old."
+ $ACLOCAL --version
+ echo " (version $aclocal_min_vers or newer is required)"
+ DIE="yes"
+ fi
+ else
+ echo $ACLOCAL: command not found
+ echo
+ echo "$ARGV0: ERROR: Missing \`$ACLOCAL'"
+ echo " The version of $AUTOMAKE installed doesn't appear recent enough."
+ DIE="yes"
+ fi
+else
+ echo $AUTOMAKE: command not found
+ echo
+ echo "$ARGV0: ERROR: You must have \`automake' installed to compile $PACKAGE."
+ echo " (version $automake_min_vers or newer is required)"
+ DIE="yes"
+fi
+
+if ($LIBTOOLIZE --version) < /dev/null > /dev/null 2>&1 ; then
+ if ($LIBTOOLIZE --version | awk 'NR==1 { if( $4 >= '$libtoolize_min_vers') \
+ exit 1; exit 0; }');
+ then
+ echo "$ARGV0: ERROR: \`$LIBTOOLIZE' is too old."
+ echo " (version $libtoolize_min_vers or newer is required)"
+ DIE="yes"
+ fi
+else
+ echo $LIBTOOLIZE: command not found
+ echo
+ echo "$ARGV0: ERROR: You must have \`libtoolize' installed to compile $PACKAGE."
+ echo " (version $libtoolize_min_vers or newer is required)"
+ DIE="yes"
+fi
+
+if test -z "$ACLOCAL_FLAGS"; then
+ acdir=`$ACLOCAL --print-ac-dir`
+ if [ ! -f $acdir/pkg.m4 ]; then
+ echo "$ARGV0: Error: Could not find pkg-config macros."
+ echo " (Looked in $acdir/pkg.m4)"
+ echo " If pkg.m4 is available in /another/directory, please set"
+ echo " ACLOCAL_FLAGS=\"-I /another/directory\""
+ echo " Otherwise, please install pkg-config."
+ echo ""
+ echo "pkg-config is available from:"
+ echo "http://www.freedesktop.org/software/pkgconfig/"
+ DIE=yes
+ fi
+fi
+
+if test "X$DIE" != X; then
+ exit 1
+fi
+
+
+if test -z "$*"; then
+ echo "$ARGV0: Note: \`./configure' will be run with no arguments."
+ echo " If you wish to pass any to it, please specify them on the"
+ echo " \`$0' command line."
+ echo
+fi
+
+do_cmd() {
+ echo "$ARGV0: running \`$@'"
+ $@
+}
+
+do_cmd $LIBTOOLIZE $LIBTOOLIZE_FLAGS
+
+do_cmd $ACLOCAL $ACLOCAL_FLAGS
+
+do_cmd $AUTOHEADER
+
+do_cmd $AUTOMAKE $AUTOMAKE_FLAGS
+
+do_cmd $AUTOCONF
+
+cd $ORIGDIR || exit 1
+
+rm -f config.cache
+
+do_cmd $srcdir/configure --cache-file=config.cache --disable-static --enable-maintainer-mode --enable-gtk-doc ${1+"$@"} && echo "Now type \`make' to compile" || exit 1
diff --git a/ipa-server/configure.ac b/ipa-server/configure.ac
new file mode 100644
index 00000000..2bc96332
--- /dev/null
+++ b/ipa-server/configure.ac
@@ -0,0 +1,323 @@
+AC_PREREQ(2.59)
+m4_include(version.m4)
+AC_INIT([ipa-server],
+ IPA_VERSION,
+ [https://hosted.fedoraproject.org/projects/freeipa/newticket])
+
+AC_CONFIG_SRCDIR([ipaserver/ipaldap.py])
+AC_CONFIG_HEADERS([config.h])
+
+AM_INIT_AUTOMAKE
+
+AM_MAINTAINER_MODE
+AC_PROG_CC
+AC_STDC_HEADERS
+AC_DISABLE_STATIC
+AC_PROG_LIBTOOL
+
+AC_HEADER_STDC
+
+AC_SUBST(VERSION)
+
+dnl ---------------------------------------------------------------------------
+dnl - Check for NSPR
+dnl ---------------------------------------------------------------------------
+AC_CHECK_HEADER(nspr4/nspr.h)
+if test "x$ac_cv_header_nspr4_nspr_h" = "xno" ; then
+ AC_MSG_ERROR([Required NSPR header not available (nspr-devel)])
+fi
+
+dnl ---------------------------------------------------------------------------
+dnl - Check for NSS
+dnl ---------------------------------------------------------------------------
+SAVE_CPPFLAGS=$CPPFLAGS
+CPPFLAGS="-I/usr/include/nspr4"
+AC_CHECK_HEADER(nss3/nss.h)
+CPPFLAGS=$SAVE_CPPFLAGS
+if test "x$ac_cv_header_nss3_nss_h" = "xno" ; then
+ AC_MSG_ERROR([Required NSS header not available (nss-devel)])
+fi
+
+dnl ---------------------------------------------------------------------------
+dnl - Check for DS slapi plugin
+dnl ---------------------------------------------------------------------------
+
+# Need to hack CPPFLAGS to be able to correctly detetct slapi-plugin.h
+SAVE_CPPFLAGS=$CPPFLAGS
+CPPFLAGS="-I/usr/include/nspr4"
+AC_CHECK_HEADER(dirsrv/slapi-plugin.h)
+CPPFLAGS=$SAVE_CPPFLAGS
+
+if test "x$ac_cv_header_dirsrv_slapi_plugin_h" = "xno" ; then
+ AC_MSG_ERROR([Required DS slapi plugin header not available (fedora-ds-base-devel)])
+fi
+
+dnl ---------------------------------------------------------------------------
+dnl - Check for KRB5
+dnl ---------------------------------------------------------------------------
+
+KRB5_LIBS=
+AC_CHECK_HEADER(krb5.h)
+
+krb5_impl=mit
+
+if test "x$ac_cv_header_krb5_h" = "xyes" ; then
+ dnl lazy check for Heimdal Kerberos
+ AC_CHECK_HEADERS(heim_err.h)
+ if test $ac_cv_header_heim_err_h = yes ; then
+ krb5_impl=heimdal
+ else
+ krb5_impl=mit
+ fi
+
+ if test "x$krb5_impl" = "xmit"; then
+ AC_CHECK_LIB(k5crypto, main,
+ [krb5crypto=k5crypto],
+ [krb5crypto=crypto])
+
+ AC_CHECK_LIB(krb5, main,
+ [have_krb5=yes
+ KRB5_LIBS="-lkrb5 -l$krb5crypto -lcom_err"],
+ [have_krb5=no],
+ [-l$krb5crypto -lcom_err])
+
+ elif test "x$krb5_impl" = "xheimdal"; then
+ AC_CHECK_LIB(des, main,
+ [krb5crypto=des],
+ [krb5crypto=crypto])
+
+ AC_CHECK_LIB(krb5, main,
+ [have_krb5=yes
+ KRB5_LIBS="-lkrb5 -l$krb5crypto -lasn1 -lroken -lcom_err"],
+ [have_krb5=no],
+ [-l$krb5crypto -lasn1 -lroken -lcom_err])
+
+ AC_DEFINE(HAVE_HEIMDAL_KERBEROS, 1,
+ [define if you have HEIMDAL Kerberos])
+
+ else
+ have_krb5=no
+ AC_MSG_WARN([Unrecognized Kerberos5 Implementation])
+ fi
+
+ if test "x$have_krb5" = "xyes" ; then
+ ol_link_krb5=yes
+
+ AC_DEFINE(HAVE_KRB5, 1,
+ [define if you have Kerberos V])
+
+ else
+ AC_MSG_ERROR([Required Kerberos 5 support not available])
+ fi
+
+fi
+
+AC_SUBST(KRB5_LIBS)
+
+dnl ---------------------------------------------------------------------------
+dnl - Check for Mozilla LDAP or OpenLDAP SDK
+dnl ---------------------------------------------------------------------------
+
+AC_ARG_WITH(openldap, [ --with-openldap Use OpenLDAP])
+
+dnl The mozldap libraries are always needed because ipa-slapi-plugins/dna/
+dnl will not build against OpenLDAP.
+SAVE_CPPFLAGS=$CPPFLAGS
+CPPFLAGS="-I/usr/include/nspr4 -I/usr/include/nss3"
+AC_CHECK_HEADER(svrcore.h)
+if test "x$ac_cv_header_svrcore_h" = "xno" ; then
+ AC_MSG_ERROR([Required svrcore header not available (svrcore-devel)])
+fi
+CPPFLAGS=$SAVE_CPPFLAGS
+AC_CHECK_HEADER(mozldap/ldap.h)
+if test "x$ac_cv_header_mozldap_ldap_h" = "xno" ; then
+ AC_MSG_ERROR([Required MOZLDAP header not available (mozldap-devel)])
+fi
+PKG_CHECK_MODULES(MOZLDAP, mozldap > 6)
+
+if test x$with_openldap = xyes; then
+ AC_CHECK_LIB(ldap, ldap_search, with_ldap=yes)
+ dnl Check for other libraries we need to link with to get the main routines.
+ test "$with_ldap" != "yes" && { AC_CHECK_LIB(ldap, ldap_open, [with_ldap=yes with_ldap_lber=yes], , -llber) }
+ test "$with_ldap" != "yes" && { AC_CHECK_LIB(ldap, ldap_open, [with_ldap=yes with_ldap_lber=yes with_ldap_krb=yes], , -llber -lkrb) }
+ test "$with_ldap" != "yes" && { AC_CHECK_LIB(ldap, ldap_open, [with_ldap=yes with_ldap_lber=yes with_ldap_krb=yes with_ldap_des=yes], , -llber -lkrb -ldes) }
+ dnl Recently, we need -lber even though the main routines are elsewhere,
+ dnl because otherwise be get link errors w.r.t. ber_pvt_opt_on. So just
+ dnl check for that (it's a variable not a fun but that doesn't seem to
+ dnl matter in these checks) and stick in -lber if so. Can't hurt (even to
+ dnl stick it in always shouldn't hurt, I don't think) ... #### Someone who
+ dnl #### understands LDAP needs to fix this properly.
+ test "$with_ldap_lber" != "yes" && { AC_CHECK_LIB(lber, ber_pvt_opt_on, with_ldap_lber=yes) }
+
+ if test "$with_ldap" = "yes"; then
+ if test "$with_ldap_des" = "yes" ; then
+ LDAP_LIBS="${LDAP_LIBS} -ldes"
+ fi
+ if test "$with_ldap_krb" = "yes" ; then
+ LDAP_LIBS="${LDAP_LIBS} -lkrb"
+ fi
+ if test "$with_ldap_lber" = "yes" ; then
+ LDAP_LIBS="${LDAP_LIBS} -llber"
+ fi
+ LDAP_LIBS="${LDAP_LIBS} -lldap"
+ else
+ AC_MSG_ERROR([OpenLDAP not found])
+ fi
+
+ AC_SUBST(LDAP_LIBS)
+
+ LDAP_CFLAGS="${LDAP_CFLAGS} -DWITH_OPENLDAP"
+ AC_SUBST(LDAP_CFLAGS)
+else
+ LDAP_LIBS="${MOZLDAP_LIBS}"
+ AC_SUBST(LDAP_LIBS)
+
+ LDAP_CFLAGS="${LDAP_CFLAGS} -DWITH_MOZLDAP"
+ AC_SUBST(LDAP_CFLAGS)
+fi
+
+dnl ---------------------------------------------------------------------------
+dnl - Check for OpenSSL Crypto library
+dnl ---------------------------------------------------------------------------
+dnl This is a very simple check, we should probably check also for MD4_Init and
+dnl probably also the version we are using is recent enough
+SSL_LIBS=
+AC_CHECK_LIB(crypto, DES_set_key_unchecked, [SSL_LIBS="-lcrypto"])
+AC_SUBST(SSL_LIBS)
+
+dnl ---------------------------------------------------------------------------
+dnl - Check for Python
+dnl ---------------------------------------------------------------------------
+
+AC_MSG_NOTICE([Checking for Python])
+have_python=no
+AM_PATH_PYTHON(2.3)
+
+if test "x$PYTHON" = "x" ; then
+ AC_MSG_ERROR([Python not found])
+fi
+
+dnl ---------------------------------------------------------------------------
+dnl - Check for TurboGears
+dnl ---------------------------------------------------------------------------
+AC_MSG_NOTICE([Checking for TurboGears])
+AC_CHECK_PROG(tg_found,tg-admin,true,false)
+if test x"${tg_found}" = xfalse ; then
+ AC_MSG_ERROR(tg-admin not found in PATH. Install TurboGears)
+fi
+
+dnl ---------------------------------------------------------------------------
+dnl - Set the data install directory since we don't use pkgdatadir
+dnl ---------------------------------------------------------------------------
+
+IPA_DATA_DIR="$datadir/ipa"
+AC_SUBST(IPA_DATA_DIR)
+
+dnl ---------------------------------------------------------------------------
+dnl Finish
+dnl ---------------------------------------------------------------------------
+
+# Turn on the additional warnings last, so -Werror doesn't affect other tests.
+
+AC_ARG_ENABLE(more-warnings,
+ [AC_HELP_STRING([--enable-more-warnings],
+ [Maximum compiler warnings])],
+ set_more_warnings="$enableval",[
+ if test -d $srcdir/../.hg; then
+ set_more_warnings=yes
+ else
+ set_more_warnings=no
+ fi
+ ])
+AC_MSG_CHECKING(for more warnings)
+if test "$GCC" = "yes" -a "$set_more_warnings" != "no"; then
+ AC_MSG_RESULT(yes)
+ CFLAGS="\
+ -Wall \
+ -Wchar-subscripts -Wmissing-declarations -Wmissing-prototypes \
+ -Wnested-externs -Wpointer-arith \
+ -Wcast-align -Wsign-compare \
+ $CFLAGS"
+
+ for option in -Wno-strict-aliasing -Wno-sign-compare; do
+ SAVE_CFLAGS="$CFLAGS"
+ CFLAGS="$CFLAGS $option"
+ AC_MSG_CHECKING([whether gcc understands $option])
+ AC_TRY_COMPILE([], [],
+ has_option=yes,
+ has_option=no,)
+ if test $has_option = no; then
+ CFLAGS="$SAVE_CFLAGS"
+ fi
+ AC_MSG_RESULT($has_option)
+ unset has_option
+ unset SAVE_CFLAGS
+ done
+ unset option
+else
+ AC_MSG_RESULT(no)
+fi
+
+# Flags
+
+AC_SUBST(CFLAGS)
+AC_SUBST(CPPFLAGS)
+AC_SUBST(LDFLAGS)
+
+# Files
+
+AC_CONFIG_FILES([
+ Makefile
+ ipa-gui/Makefile
+ ipa-gui/ipagui/Makefile
+ ipa-gui/ipagui/config/Makefile
+ ipa-gui/ipagui/forms/Makefile
+ ipa-gui/ipagui/helpers/Makefile
+ ipa-gui/ipagui/static/Makefile
+ ipa-gui/ipagui/static/css/Makefile
+ ipa-gui/ipagui/static/images/Makefile
+ ipa-gui/ipagui/static/images/template/Makefile
+ ipa-gui/ipagui/static/images/branding/Makefile
+ ipa-gui/ipagui/static/images/objects/Makefile
+ ipa-gui/ipagui/static/javascript/Makefile
+ ipa-gui/ipagui/subcontrollers/Makefile
+ ipa-gui/ipagui/templates/Makefile
+ ipa-gui/ipagui/tests/Makefile
+ ipa-gui/ipa_gui.egg-info/Makefile
+ ipa-install/Makefile
+ ipa-install/share/Makefile
+ ipa-install/updates/Makefile
+ ipa-kpasswd/Makefile
+ ipaserver/Makefile
+ ipa-slapi-plugins/Makefile
+ ipa-slapi-plugins/dna/Makefile
+ ipa-slapi-plugins/ipa-memberof/Makefile
+ ipa-slapi-plugins/ipa-pwd-extop/Makefile
+ ipa-slapi-plugins/ipa-winsync/Makefile
+ xmlrpc-server/Makefile
+ xmlrpc-server/test/Makefile
+ man/Makefile
+])
+
+AC_OUTPUT
+
+echo "
+ IPA Server $VERSION
+ ========================
+
+ prefix: ${prefix}
+ exec_prefix: ${exec_prefix}
+ libdir: ${libdir}
+ bindir: ${bindir}
+ sbindir: ${sbindir}
+ sysconfdir: ${sysconfdir}
+ localstatedir: ${localstatedir}
+ datadir: ${datadir}
+ source code location: ${srcdir}
+ compiler: ${CC}
+ cflags: ${CFLAGS}
+ LDAP libs: ${LDAP_LIBS}
+ KRB5 libs: ${KRB5_LIBS}
+ OpenSSL libs: ${SSL_LIBS}
+ Maintainer mode: ${USE_MAINTAINER_MODE}
+"
diff --git a/ipa-server/ipa-compat-manage b/ipa-server/ipa-compat-manage
new file mode 100755
index 00000000..f105f1cd
--- /dev/null
+++ b/ipa-server/ipa-compat-manage
@@ -0,0 +1,171 @@
+#!/usr/bin/env python
+# Authors: Rob Crittenden <rcritten@redhat.com>
+# Authors: Simo Sorce <ssorce@redhat.com>
+#
+# Copyright (C) 2008 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+import sys
+try:
+ from optparse import OptionParser
+ from ipaserver import ipaldap
+ from ipa import entity, ipaerror, ipautil, config
+ from ipaserver import installutils
+ from ipaserver.ldapupdate import LDAPUpdate, BadSyntax, UPDATES_DIR
+ import ldap
+ import logging
+ import re
+ import krbV
+ import platform
+ import shlex
+ import time
+ import random
+except ImportError:
+ print >> sys.stderr, """\
+There was a problem importing one of the required Python modules. The
+error was:
+
+ %s
+""" % sys.exc_value
+ sys.exit(1)
+
+def parse_options():
+ usage = "%prog [options] <enable|disable>\n"
+ usage += "%prog [options]\n"
+ parser = OptionParser(usage=usage, formatter=config.IPAFormatter())
+
+ parser.add_option("-d", "--debug", action="store_true", dest="debug",
+ help="Display debugging information about the update(s)")
+ parser.add_option("-y", dest="password",
+ help="File containing the Directory Manager password")
+
+ config.add_standard_options(parser)
+ options, args = parser.parse_args()
+
+ config.init_config(options)
+
+ return options, args
+
+def get_dirman_password():
+ """Prompt the user for the Directory Manager password and verify its
+ correctness.
+ """
+ password = installutils.read_password("Directory Manager", confirm=False, validate=False)
+
+ return password
+
+def main():
+ retval = 0
+ loglevel = logging.NOTSET
+ files=['/usr/share/ipa/schema_compat.uldif']
+
+ options, args = parse_options()
+ if options.debug:
+ loglevel = logging.DEBUG
+
+ if len(args) != 1:
+ print "You must specify one action, either enable or disable"
+ sys.exit(1)
+ elif args[0] != "enable" and args[0] != "disable":
+ print "Unrecognized action [" + args[0] + "]"
+ sys.exit(1)
+
+ logging.basicConfig(level=loglevel,
+ format='%(levelname)s %(message)s')
+
+ dirman_password = ""
+ if options.password:
+ pw = read_file(options.password)
+ dirman_password = pw[0].strip()
+ else:
+ dirman_password = get_dirman_password()
+
+ try:
+ try:
+ conn = ipaldap.IPAdmin(installutils.get_fqdn())
+ conn.do_simple_bind(bindpw=dirman_password)
+ except ldap.LDAPError, e:
+ print "An error occurred while connecting to the server."
+ print "%s" % e[0]['desc']
+ return 1
+
+ if args[0] == "enable":
+ try:
+ conn.getEntry("cn=Schema Compatibility,cn=plugins,cn=config",
+ ldap.SCOPE_BASE, "(objectclass=*)")
+ print "Plugin already Enabled"
+ retval = 2
+ except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND):
+ print "Enabling plugin"
+ except ldap.LDAPError, e:
+ print "An error occurred while talking to the server."
+ print "%s" % e[0]['desc']
+ retval = 1
+
+ if retval == 0:
+ ld = LDAPUpdate(dm_password=dirman_password, sub_dict={})
+ retval = ld.update(files)
+ if retval == 0:
+ print "This setting will not take effect until you restart Directory Server."
+
+ elif args[0] == "disable":
+ # Make a quick hack foir now, directly delete the entries by name,
+ # In future we should add delete capabilites to LDAPUpdate
+ try:
+ conn.getEntry("cn=Schema Compatibility,cn=plugins,cn=config",
+ ldap.SCOPE_BASE, "(objectclass=*)")
+ conn.deleteEntry("cn=groups,cn=Schema Compatibility,cn=plugins,cn=config")
+ conn.deleteEntry("cn=users,cn=Schema Compatibility,cn=plugins,cn=config")
+ conn.deleteEntry("cn=Schema Compatibility,cn=plugins,cn=config")
+ except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND):
+ print "Plugin is already disabled"
+ retval = 2
+ except ldap.LDAPError, e:
+ print "An error occurred while talking to the server."
+ print "%s" % e[0]['desc']
+ retval = 1
+
+ else:
+ retval = 1
+
+ finally:
+ if conn:
+ conn.unbind()
+
+ return retval
+
+try:
+ if __name__ == "__main__":
+ sys.exit(main())
+except BadSyntax, e:
+ print "There is a syntax error in this update file:"
+ print " %s" % e
+ sys.exit(1)
+except RuntimeError, e:
+ print "%s" % e
+ sys.exit(1)
+except SystemExit, e:
+ sys.exit(e)
+except KeyboardInterrupt, e:
+ sys.exit(1)
+except config.IPAConfigError, e:
+ print "An IPA server to update cannot be found. Has one been configured yet?"
+ print "The error was: %s" % e
+ sys.exit(1)
+except ipaerror, e:
+ print "An error occurred while performing operations: %s" % e
+ sys.exit(1)
diff --git a/ipa-server/ipa-fix-CVE-2008-3274 b/ipa-server/ipa-fix-CVE-2008-3274
new file mode 100644
index 00000000..41d3abc9
--- /dev/null
+++ b/ipa-server/ipa-fix-CVE-2008-3274
@@ -0,0 +1,524 @@
+#!/usr/bin/python
+#
+# Upgrade configuration files to a newer template.
+
+etckrb5conf = "/etc/krb5.conf"
+krb5dir = "/var/kerberos/krb5kdc"
+cachedir = "/var/cache/ipa"
+libdir = "/var/lib/ipa"
+basedir = libdir+"/mkey"
+ourkrb5conf = basedir+"/krb5.conf"
+ldappwdfile = basedir+"/ldappwd"
+
+import sys
+try:
+ from optparse import OptionParser
+
+ import os
+ import random
+ import time
+ import shutil
+ import getpass
+
+ import ipa
+ import ipa.config
+ import ipa.ipautil
+
+ import krbV
+ import ldap
+
+ from ldap import LDAPError
+ from ldap import ldapobject
+
+ from ipaclient import ipachangeconf
+ from ipaserver import ipaldap
+
+ from pyasn1.type import univ, namedtype
+ import pyasn1.codec.ber.encoder
+ import pyasn1.codec.ber.decoder
+ import struct
+ import base64
+
+except ImportError:
+ print >> sys.stderr, """\
+There was a problem importing one of the required Python modules. The
+error was:
+
+ %s
+""" % sys.exc_value
+ sys.exit(1)
+
+def parse_options():
+ parser = OptionParser("%prog [--check] [--fix] [--fix-replica]")
+ parser.add_option("--check", dest="check", action="store_true",
+ help="Just check for the vulnerability and report (default action)")
+ parser.add_option("--fix", dest="fix", action="store_true",
+ help="Run checks and start procedure to fix the problem")
+ parser.add_option("--fix-replica", dest="fix_replica", action="store_true",
+ help="Fix a replica after the tool has been tun with --fix on another master")
+
+ ipa.config.add_standard_options(parser)
+ options, args = parser.parse_args()
+
+ ipa.config.verify_args(parser, args)
+ if not options.fix and not options.fix_replica and not options.check:
+ parser.error("please specify at least one option")
+
+ ipa.config.init_config(options)
+
+ return options, args
+
+def check_vuln(realm, suffix):
+
+ try:
+ conn = ldapobject.SimpleLDAPObject("ldap://127.0.0.1/")
+ conn.simple_bind()
+ msgid = conn.search("cn="+realm+",cn=kerberos,"+suffix,
+ ldap.SCOPE_BASE,
+ "(objectclass=krbRealmContainer)",
+ ("krbmkey", "cn"))
+ res = conn.result(msgid)
+ conn.unbind()
+
+ if len(res) != 2:
+ err = 'Realm Container not found, unable to proceed'
+ print err
+ raise Exception, err
+
+ if 'krbmkey' in res[1][0][1]:
+ print 'System vulnerable'
+ return 1
+ else:
+ print 'System *not* vulnerable'
+ return 0
+ except Exception, e:
+ print "Could not connect to the LDAP server, unable to check server"
+ print "("+type(e)+")("+dir(e)+")"
+ raise e
+
+# We support only des3 encoded stash files for now
+def generate_new_stash_file(file):
+
+ odd_parity_bytes_pool = ['\x01', '\x02', '\x04', '\x07', '\x08', '\x0b',
+ '\r', '\x0e', '\x10', '\x13', '\x15', '\x16', '\x19', '\x1a', '\x1c',
+ '\x1f', ' ', '#', '%', '&', ')', '*', ',', '/', '1', '2', '4', '7', '8',
+ ';', '=', '>', '@', 'C', 'E', 'F', 'I', 'J', 'L', 'O', 'Q', 'R', 'T',
+ 'W', 'X', '[', ']', '^', 'a', 'b', 'd', 'g', 'h', 'k', 'm', 'n', 'p',
+ 's', 'u', 'v', 'y', 'z', '|', '\x7f', '\x80', '\x83', '\x85', '\x86',
+ '\x89', '\x8a', '\x8c', '\x8f', '\x91', '\x92', '\x94', '\x97', '\x98',
+ '\x9b', '\x9d', '\x9e', '\xa1', '\xa2', '\xa4', '\xa7', '\xa8', '\xab',
+ '\xad', '\xae', '\xb0', '\xb3', '\xb5', '\xb6', '\xb9', '\xba', '\xbc',
+ '\xbf', '\xc1', '\xc2', '\xc4', '\xc7', '\xc8', '\xcb', '\xcd', '\xce',
+ '\xd0', '\xd3', '\xd5', '\xd6', '\xd9', '\xda', '\xdc', '\xdf', '\xe0',
+ '\xe3', '\xe5', '\xe6', '\xe9', '\xea', '\xec', '\xef', '\xf1', '\xf2',
+ '\xf4', '\xf7', '\xf8', '\xfb', '\xfd', '\xfe']
+
+ pool_len = len(odd_parity_bytes_pool)
+ keytype = 16 # des3
+ keydata = ""
+
+ r = random.SystemRandom()
+ for k in range(24):
+ keydata += r.choice(odd_parity_bytes_pool)
+
+ format = '=hi%ss' % len(keydata)
+ s = struct.pack(format, keytype, len(keydata), keydata)
+ try:
+ fd = open(file, "w")
+ fd.write(s)
+ except os.error, e:
+ logging.critical("failed to write stash file")
+ raise e
+
+# clean up procedures
+def change_mkey_cleanup(password):
+ try:
+ os.stat(basedir)
+ except:
+ return None
+ try:
+ # always remove ldappwdfile as it contains the Directory Manager password
+ os.remove(ldappwdfile)
+ except:
+ pass
+
+ # tar and encrypt the working dir so that we do not leave sensitive data
+ # around unproteceted
+ curtime = time.strftime("%Y%m%d%H%M%S",time.gmtime())
+ tarfile = libdir+"/ipa-change-mkey-"+curtime+".tar"
+ gpgfile = tarfile+".gpg"
+ args = ['/bin/tar', '-C', libdir, '-cf', tarfile, 'mkey']
+ ipa.ipautil.run(args)
+ ipa.ipautil.encrypt_file(tarfile, gpgfile, password, cachedir)
+ os.remove(tarfile)
+ shutil.rmtree(basedir, ignore_errors=True)
+
+ return "The temporary working directory with backup dump files has been securely archived and gpg-encrypted as "+gpgfile+" using the Directory Manager password."
+
+def change_mkey(password = None, quiet = False):
+
+ krbctx = krbV.default_context()
+
+ realm = krbctx.default_realm
+ suffix = ipa.ipautil.realm_to_suffix(realm)
+
+ backupfile = basedir+"/backup.dump"
+ convertfile = basedir+"/convert.dump"
+ oldstashfile = krb5dir+"/.k5."+realm
+ newstashfile = basedir+"/.new.mkey"
+ bkpstashfile = basedir+"/.k5."+realm
+
+ if os.getuid() != 0:
+ print "ERROR: This command must be run as root"
+ sys.exit(1)
+
+ print "DANGER: This is a dangerous operation, make sure you backup all your IPA data before running the tool"
+ print "This command will restart your Directory and KDC Servers."
+
+ #TODO: ask for confirmation
+ if not ipa.ipautil.user_input("Do you want to proceed and change the Kerberos Master key?", False):
+ print ""
+ print "Aborting..."
+ return 1
+
+ if not password:
+ password = getpass.getpass("Directory Manager password: ")
+
+ # get a connection to the DS
+ try:
+ conn = ipaldap.IPAdmin(ipa.config.config.default_server[0])
+ conn.do_simple_bind(bindpw=password)
+ except Exception, e:
+ print "ERROR: Could not connect to the Directory Server on "+ipa.config.config.default_server[0]+" ("+str(e)+")"
+ return 1
+
+ # Wipe basedir and recreate it
+ shutil.rmtree(basedir, ignore_errors=True)
+ os.mkdir(basedir, 0700)
+
+ generate_new_stash_file(newstashfile)
+
+ # Generate conf files
+ try:
+ shutil.copyfile(etckrb5conf, ourkrb5conf)
+
+ krbconf = ipachangeconf.IPAChangeConf("IPA Installer")
+ krbconf.setOptionAssignment(" = ")
+ krbconf.setSectionNameDelimiters(("[","]"))
+ krbconf.setSubSectionDelimiters(("{","}"))
+ krbconf.setIndent((""," "," "))
+
+ #OPTS
+ opts = [{'name':'ldap_kadmind_dn', 'type':'option', 'action':'set', 'value':'cn=Directory Manager'},
+ {'name':'ldap_service_password_file', 'type':'option', 'action':'set', 'value':ldappwdfile}]
+
+ #REALM
+ realmopts = [{'name':realm, 'type':'subsection', 'action':'set', 'value':opts}]
+
+ #DBMODULES
+ dbopts = [{'name':'dbmodules', 'type':'section', 'action':'set', 'value':realmopts}]
+
+ krbconf.changeConf(ourkrb5conf, dbopts);
+
+ hexpwd = ""
+ for x in password:
+ hexpwd += (hex(ord(x))[2:])
+ pwd_fd = open(ldappwdfile, "w")
+ pwd_fd.write("cn=Directory Manager#{HEX}"+hexpwd+"\n")
+ pwd_fd.close()
+ os.chmod(ldappwdfile, 0600)
+
+ except Exception, e:
+ print "Failed to create custom configuration files ("+str(e)+") aborting..."
+ return 1
+
+ #Set environment vars so that the modified krb5.conf is used
+ os.environ['KRB5_CONFIG'] = ourkrb5conf
+
+ #Backup the kerberos key material for recovery if needed
+ args = ["/usr/kerberos/sbin/kdb5_util", "dump", "-verbose", backupfile]
+ print "Performing safety backup of the key material"
+ try:
+ output = ipa.ipautil.run(args)
+ except ipa.ipautil.CalledProcessError, e:
+ print "Failed to backup key material ("+str(e)+"), aborting ..."
+ return 1
+
+ if not quiet:
+ princlist = output[1].split('\n')
+ print "Principals stored into the backup file "+backupfile+":"
+ for p in princlist:
+ print p
+ print ""
+
+ #Convert the kerberos keys to the new master key
+ args = ["/usr/kerberos/sbin/kdb5_util", "dump", "-verbose", "-new_mkey_file", newstashfile, convertfile]
+ print "Converting key material to new master key"
+ try:
+ output = ipa.ipautil.run(args)
+ except ipa.ipautil.CalledProcessError, e:
+ print "Failed to convert key material, aborting ..."
+ return 1
+
+ savedprinclist = output[1].split('\n')
+
+ if not quiet:
+ princlist = output[1].split('\n')
+ print "Principals dumped for conversion:"
+ for p in princlist:
+ print p
+ print ""
+
+ #Stop the KDC
+ args = ["/etc/init.d/krb5kdc", "stop"]
+ try:
+ output = ipa.ipautil.run(args)
+ if output[0]:
+ print output[0]
+ if output[1]:
+ print output[1]
+ except ipa.ipautil.CalledProcessError, e:
+ print "WARNING: Failed to restart the KDC ("+str(e)+")"
+ print "You will have to manually restart the KDC when the operation is completed"
+
+ #Change the mkey into ldap
+ try:
+ stash = open(newstashfile, "r")
+ keytype = struct.unpack('h', stash.read(2))[0]
+ keylen = struct.unpack('i', stash.read(4))[0]
+ keydata = stash.read(keylen)
+
+ #encode it in the asn.1 attribute
+ MasterKey = univ.Sequence()
+ MasterKey.setComponentByPosition(0, univ.Integer(keytype))
+ MasterKey.setComponentByPosition(1, univ.OctetString(keydata))
+ krbMKey = univ.Sequence()
+ krbMKey.setComponentByPosition(0, univ.Integer(0)) #we have no kvno
+ krbMKey.setComponentByPosition(1, MasterKey)
+ asn1key = pyasn1.codec.ber.encoder.encode(krbMKey)
+
+ dn = "cn="+realm+",cn=kerberos,"+suffix
+ mod = [(ldap.MOD_REPLACE, 'krbMKey', str(asn1key))]
+ conn.modify_s(dn, mod)
+ except Exception, e:
+ print "ERROR: Failed to upload the Master Key from the Stash file: "+newstashfile+" ("+str(e)+")"
+ return 1
+
+ #Backup old stash file and substitute with new
+ try:
+ shutil.move(oldstashfile, bkpstashfile)
+ shutil.copyfile(newstashfile, oldstashfile)
+ except Exception, e:
+ print "ERROR: An error occurred while installing the new stash file("+str(e)+")"
+ print "The KDC may fail to start if the correct stash file is not in place"
+ print "Verify that "+newstashfile+" has been correctly installed into "+oldstashfile
+ print "A backup copy of the old stash file should be saved in "+bkpstashfile
+
+ #Finally upload the converted principals
+ args = ["/usr/kerberos/sbin/kdb5_util", "load", "-verbose", "-update", convertfile]
+ print "Uploading converted key material"
+ try:
+ output = ipa.ipautil.run(args)
+ except ipa.ipautil.CalledProcessError, e:
+ print "Failed to upload key material ("+e+"), aborting ..."
+ return 1
+
+ if not quiet:
+ princlist = output[1].split('\n')
+ print "Principals converted and uploaded:"
+ for p in princlist:
+ print p
+ print ""
+
+ uploadedprinclist = output[1].split('\n')
+
+ #Check for differences and report
+ d = []
+ for p in savedprinclist:
+ if uploadedprinclist.count(p) == 0:
+ d.append(p)
+ if len(d) != 0:
+ print "WARNING: Not all dumped principals have been updated"
+ print "Principals not Updated:"
+ for p in d:
+ print p
+
+ #Remove custom environ
+ del os.environ['KRB5_CONFIG']
+
+ #Restart Directory Server (the pwd plugin need to read the new mkey)
+ args = ["/etc/init.d/dirsrv", "restart"]
+ try:
+ output = ipa.ipautil.run(args)
+ if output[0]:
+ print output[0]
+ if output[1]:
+ print output[1]
+ except ipa.ipautil.CalledProcessError, e:
+ print "WARNING: Failed to restart the Directory Server ("+str(e)+")"
+ print "Please manually restart the DS with 'service dirsrv restart'"
+
+ #Restart the KDC
+ args = ["/etc/init.d/krb5kdc", "start"]
+ try:
+ output = ipa.ipautil.run(args)
+ if output[0]:
+ print output[0]
+ if output[1]:
+ print output[1]
+ except ipa.ipautil.CalledProcessError, e:
+ print "WARNING: Failed to restart the KDC ("+str(e)+")"
+ print "Please manually restart the kdc with 'service krb5kdc start'"
+
+ print "Master Password successfully changed"
+ #print "You MUST now copy the stash file "+oldstashfile+" to all the replicas and restart them!"
+ print ""
+
+ return 0
+
+def fix_replica(password, realm, suffix):
+
+ try:
+ conn = ldapobject.SimpleLDAPObject("ldap://127.0.0.1/")
+ conn.simple_bind("cn=Directory Manager", password)
+ msgid = conn.search("cn="+realm+",cn=kerberos,"+suffix,
+ ldap.SCOPE_BASE,
+ "(objectclass=krbRealmContainer)",
+ ("krbmkey", "cn"))
+ res = conn.result(msgid)
+ conn.unbind()
+ krbmkey = res[1][0][1]['krbmkey'][0]
+ except Exception, e:
+ print "Could not connect to the LDAP server, unable to fix server"
+ print "("+type(e)+")("+dir(e)+")"
+ raise e
+
+ krbMKey = pyasn1.codec.ber.decoder.decode(krbmkey)
+ keytype = int(krbMKey[0][1][0])
+ keydata = str(krbMKey[0][1][1])
+
+ format = '=hi%ss' % len(keydata)
+ s = struct.pack(format, keytype, len(keydata), keydata)
+ try:
+ fd = open("/var/kerberos/krb5kdc/.k5."+realm, "w")
+ fd.write(s)
+ fd.close()
+ except os.error, e:
+ print "failed to write stash file"
+ raise e
+
+ #restart KDC so that it can reload the new Master Key
+ os.system("/etc/init.d/krb5kdc restart")
+
+KRBMKEY_DENY_ACI = """
+(targetattr = "krbMKey")(version 3.0; acl "No external access"; deny (all) userdn != "ldap:///uid=kdc,cn=sysaccounts,cn=etc,$SUFFIX";)
+"""
+
+def fix_main(password, realm, suffix):
+
+ #Run the change master key tool
+ print "Changing Kerberos master key"
+ try:
+ ret = change_mkey(password, True)
+ except SystemExit:
+ ret = 1
+ pass
+ except Exception, e:
+ ret = 1
+ print "%s" % str(e)
+
+ try:
+ msg = change_mkey_cleanup(password)
+ if msg:
+ print msg
+ except Exception, e:
+ print "Failed to clean up the temporary location for the dump files and generate and encrypted archive with error:"
+ print e
+ print "Please securely archive/encrypt "+basedir
+
+ if ret is not 0:
+ sys.exit(ret)
+
+ #Finally upload new master key
+
+ #get the Master Key from the stash file
+ try:
+ stash = open("/var/kerberos/krb5kdc/.k5."+realm, "r")
+ keytype = struct.unpack('h', stash.read(2))[0]
+ keylen = struct.unpack('i', stash.read(4))[0]
+ keydata = stash.read(keylen)
+ except os.error:
+ print "Failed to retrieve Master Key from Stash file: %s"
+ raise e
+ #encode it in the asn.1 attribute
+ MasterKey = univ.Sequence()
+ MasterKey.setComponentByPosition(0, univ.Integer(keytype))
+ MasterKey.setComponentByPosition(1, univ.OctetString(keydata))
+ krbMKey = univ.Sequence()
+ krbMKey.setComponentByPosition(0, univ.Integer(0)) #we have no kvno
+ krbMKey.setComponentByPosition(1, MasterKey)
+ asn1key = pyasn1.codec.ber.encoder.encode(krbMKey)
+
+ dn = "cn=%s,cn=kerberos,%s" % (realm, suffix)
+ sub_dict = dict(REALM=realm, SUFFIX=suffix)
+ #protect the master key by adding an appropriate deny rule along with the key
+ mod = [(ldap.MOD_ADD, 'aci', ipa.ipautil.template_str(KRBMKEY_DENY_ACI, sub_dict)),
+ (ldap.MOD_REPLACE, 'krbMKey', str(asn1key))]
+
+ conn = ldapobject.SimpleLDAPObject("ldap://127.0.0.1/")
+ conn.simple_bind("cn=Directory Manager", password)
+ conn.modify_s(dn, mod)
+ conn.unbind()
+
+ print "\n"
+ print "This server is now correctly configured and the master-key has been changed and secured."
+ print "Please now run this tool with the --fix-replica option on all your other replicas."
+ print "Until you fix the replicas their KDCs will not work."
+
+def main():
+
+ options, args = parse_options()
+
+ if options.fix or options.fix_replica:
+ password = getpass.getpass("Directory Manager password: ")
+
+ krbctx = krbV.default_context()
+ realm = krbctx.default_realm
+ suffix = ipa.ipautil.realm_to_suffix(realm)
+
+ try:
+ ret = check_vuln(realm, suffix)
+ except:
+ sys.exit(1)
+
+ if options.fix_replica:
+ if ret is 1:
+ print "Your system is still vulnerable"
+ print "If you have already run this tool with --fix on a master then make sure your replication is working correctly, before runnig --fix-replica"
+ sys.exit(1)
+ try:
+ fix_replica(password, realm, suffix)
+ except Exception, e:
+ print "Unexpected error ("+str(e)+")"
+ sys.exit(1)
+ sys.exit(0)
+
+ if options.check:
+ sys.exit(0)
+
+ if options.fix:
+ if ret is 1:
+ try:
+ ret = fix_main(password, realm, suffix)
+ except Exception, e:
+ print "Unexpected error ("+str(e)+")"
+ sys.exit(1)
+ sys.exit(ret)
+
+try:
+ if __name__ == "__main__":
+ sys.exit(main())
+except SystemExit, e:
+ sys.exit(e)
+except KeyboardInterrupt, e:
+ sys.exit(1)
diff --git a/ipa-server/ipa-gui/Makefile.am b/ipa-server/ipa-gui/Makefile.am
new file mode 100644
index 00000000..636a5d1f
--- /dev/null
+++ b/ipa-server/ipa-gui/Makefile.am
@@ -0,0 +1,66 @@
+NULL =
+
+SUBDIRS = \
+ ipagui \
+ ipa_gui.egg-info \
+ $(NULL)
+
+sbin_SCRIPTS = \
+ ipa_webgui \
+ $(NULL)
+
+appdir = $(IPA_DATA_DIR)
+app_DATA = \
+ ipa_webgui.cfg \
+ $(NULL)
+
+LINGUAS = ja
+
+mo = $(foreach lang,$(LINGUAS),locales/$(lang)/LC_MESSAGES/messages.mo)
+po = $(foreach lang,$(LINGUAS),locales/$(lang)/LC_MESSAGES/messages.po)
+$(mo): $(po)
+ tg-admin i18n compile
+
+noinst_DATA = $(mo)
+
+EXTRA_DIST = \
+ README.txt \
+ $(sbin_SCRIPTS) \
+ $(app_DATA) \
+ ipa_webgui.init \
+ dev.cfg \
+ sample-prod.cfg \
+ setup.py \
+ start-ipagui.py \
+ test.cfg \
+ $(po) \
+ locales/messages.pot \
+ $(NULL)
+
+MAINTAINERCLEANFILES = \
+ *~ \
+ Makefile.in
+
+CLEANFILES = \
+ $(mo)
+
+initdir=$(sysconfdir)/rc.d/init.d
+
+install-data-hook: ipa_webgui.init
+ if test '!' -d $(DESTDIR)$(initdir); then \
+ $(mkinstalldirs) $(DESTDIR)$(initdir); \
+ chmod 755 $(DESTDIR)$(initdir); \
+ fi
+
+ $(INSTALL_SCRIPT) $(srcdir)/ipa_webgui.init $(DESTDIR)$(initdir)/ipa_webgui
+
+ for mo in $(mo); do \
+ $(mkinstalldirs) $(DESTDIR)$(appdir)/`dirname $$mo`; \
+ $(INSTALL_DATA) $$mo $(DESTDIR)$(appdir)/`dirname $$mo`; \
+ done
+
+uninstall-hook:
+ rm -f $(DESTDIR)$(initdir)/ipa_webgui
+ for mo in $(mo); do \
+ rm -f $(DESTDIR)$(appdir)/$$mo; \
+ done
diff --git a/ipa-server/ipa-gui/README.i18n b/ipa-server/ipa-gui/README.i18n
new file mode 100644
index 00000000..e98dc902
--- /dev/null
+++ b/ipa-server/ipa-gui/README.i18n
@@ -0,0 +1,10 @@
+You can collect the template pot file by running the following command:
+
+~/ipa-server/ipa-gui$ tg-admin i18n collect
+
+which creates locales/messages.pot. However, unfortunately, the above
+command doesn't work correctly for .kid files unless you apply the
+patch i18n.patch.
+
+If you add a new language translation remember to add the name to
+LINGUAS in Makefile.am.
diff --git a/ipa-server/ipa-gui/README.multivalue b/ipa-server/ipa-gui/README.multivalue
new file mode 100644
index 00000000..ba315181
--- /dev/null
+++ b/ipa-server/ipa-gui/README.multivalue
@@ -0,0 +1,27 @@
+The way multi-valued fields work is this:
+ - A new widget is added to the form. I name it as the attribute + s.
+ For example, I use cns for the cn attribute.
+ - If you need a new validator use a ForEach() so that each value is
+ checked.
+ - This attribute is populated from the incoming attribute from the
+ user or group record. The widget can support multiple fields at once
+ but I'm using it for just one field. In fact, I don't know if it
+ will work with more the way I'm using it.
+ - In the GUI an operator can add/remove values to each multi-valued field.
+ - Naming is very important in the widget. TurboGears automatically
+ re-assembles the data into a list of dict entries if you name things
+ properly. For example, the cns (multiple CN entries) looks like:
+ cns-0.cn=Rob+Crittenden&cns-1.cn=Robert+Crittenden&cns-2.cn=rcrit
+ - This gets converted to:
+ [{'cn': u'Rob Crittenden'}, {'cn': u'Robert Crittenden'}, {'cn': u'rcrit'}]
+ - I take this list of dicts and pull out each value and append it to a new
+ list that represents the original multi-valued field
+ - Then the list/dict version is removed (in this case, kw['cns']).
+
+When adding a new field you have to update:
+
+1. The form to add the new ExpandingForm() field and perhaps a validator
+2. The edit template to add the boilerplate to display the field
+3. The show template to be able to display all the fields separately
+4. The new template if you want to be able to enter these on new entries
+5. The subcontroller so you can do the input and output conversions
diff --git a/ipa-server/ipa-gui/README.txt b/ipa-server/ipa-gui/README.txt
new file mode 100644
index 00000000..876b880b
--- /dev/null
+++ b/ipa-server/ipa-gui/README.txt
@@ -0,0 +1,4 @@
+ipa-gui
+
+This is a TurboGears (http://www.turbogears.org) project. It can be
+started by running the start-ipagui.py script. \ No newline at end of file
diff --git a/ipa-server/ipa-gui/dev.cfg b/ipa-server/ipa-gui/dev.cfg
new file mode 100644
index 00000000..cb8e0f75
--- /dev/null
+++ b/ipa-server/ipa-gui/dev.cfg
@@ -0,0 +1,73 @@
+[global]
+# This is where all of your settings go for your development environment
+# Settings that are the same for both development and production
+# (such as template engine, encodings, etc.) all go in
+# ipagui/config/app.cfg
+
+# DATABASE
+
+# pick the form for your database
+# sqlobject.dburi="postgres://username@hostname/databasename"
+# sqlobject.dburi="mysql://username:password@hostname:port/databasename"
+# sqlobject.dburi="sqlite:///file_name_and_path"
+
+# If you have sqlite, here's a simple default to get you started
+# in development
+# sqlobject.dburi="sqlite://%(current_dir_uri)s/devdata.sqlite"
+
+# if you are using a database or table type without transactions
+# (MySQL default, for example), you should turn off transactions
+# by prepending notrans_ on the uri
+# sqlobject.dburi="notrans_mysql://username:password@hostname:port/databasename"
+
+# for Windows users, sqlite URIs look like:
+# sqlobject.dburi="sqlite:///drive_letter:/path/to/file"
+
+# TurboGears sessions. Storing in /tmp for a production system would be
+# insane but should be fine for developers.
+session_filter.storage_type='File'
+session_filter.storage_path='/tmp'
+
+# SERVER
+
+# Some server parameters that you may want to tweak
+# server.socket_port=8080
+
+# Enable the debug output at the end on pages.
+# log_debug_info_filter.on = False
+
+server.environment="development"
+autoreload.package="ipagui"
+
+# Auto-Reload after code modification
+# autoreload.on = True
+
+# Set to True if you'd like to abort execution if a controller gets an
+# unexpected parameter. False by default
+tg.strict_parameters = True
+
+# Set to True if you want to use internationalization support.
+i18n.run_template_filter = True
+
+# LOGGING
+# Logging configuration generally follows the style of the standard
+# Python logging module configuration. Note that when specifying
+# log format messages, you need to use *() for formatting variables.
+# Deployment independent log configuration is in ipagui/config/log.cfg
+[logging]
+
+[[loggers]]
+[[[ipagui]]]
+level='DEBUG'
+qualname='ipagui'
+handlers=['debug_out']
+
+[[[allinfo]]]
+level='INFO'
+handlers=['debug_out']
+
+[[[access]]]
+level='INFO'
+qualname='turbogears.access'
+handlers=['access_out']
+propagate=0
diff --git a/ipa-server/ipa-gui/i18n.patch b/ipa-server/ipa-gui/i18n.patch
new file mode 100644
index 00000000..cc46a555
--- /dev/null
+++ b/ipa-server/ipa-gui/i18n.patch
@@ -0,0 +1,16 @@
+--- turbogears/command/i18n.py.orig 2008-02-07 16:40:14.000000000 -0500
++++ turbogears/command/i18n.py 2008-02-07 16:40:43.000000000 -0500
+@@ -251,7 +251,12 @@
+ if self.options.loose_kid_support or el.get('lang', None):
+ tag = re.sub('({[^}]+})?(\w+)', '\\2', el.tag)
+ ents = []
+- if el.text: ents = [el.text.strip()]
++ if el.text and not ( el.text.strip() in keys):
++ if el.tag == "script":
++ ents = [el.text.strip()]
++ else:
++ messages.append((tag, fname, el.text.strip()))
++ keys.append(el.text.strip())
+ if el.attrib: ents.extend(el.attrib.values())
+ for k in ents:
+ key = None
diff --git a/ipa-server/ipa-gui/ipa_gui.egg-info/Makefile.am b/ipa-server/ipa-gui/ipa_gui.egg-info/Makefile.am
new file mode 100644
index 00000000..dfd814f2
--- /dev/null
+++ b/ipa-server/ipa-gui/ipa_gui.egg-info/Makefile.am
@@ -0,0 +1,22 @@
+NULL =
+
+appdir = $(IPA_DATA_DIR)/ipa_gui.egg-info
+app_DATA = \
+ dependency_links.txt \
+ entry_points.txt \
+ not-zip-safe \
+ paster_plugins.txt \
+ PKG-INFO \
+ requires.txt \
+ SOURCES.txt \
+ sqlobject.txt \
+ top_level.txt \
+ $(NULL)
+
+EXTRA_DIST = \
+ $(app_DATA) \
+ $(NULL)
+
+MAINTAINERCLEANFILES = \
+ *~ \
+ Makefile.in
diff --git a/ipa-server/ipa-gui/ipa_gui.egg-info/PKG-INFO b/ipa-server/ipa-gui/ipa_gui.egg-info/PKG-INFO
new file mode 100644
index 00000000..544ba9f9
--- /dev/null
+++ b/ipa-server/ipa-gui/ipa_gui.egg-info/PKG-INFO
@@ -0,0 +1,15 @@
+Metadata-Version: 1.0
+Name: ipa-gui
+Version: 1.0
+Summary: UNKNOWN
+Home-page: UNKNOWN
+Author: UNKNOWN
+Author-email: UNKNOWN
+License: UNKNOWN
+Description: UNKNOWN
+Platform: UNKNOWN
+Classifier: Development Status :: 3 - Alpha
+Classifier: Operating System :: OS Independent
+Classifier: Programming Language :: Python
+Classifier: Topic :: Software Development :: Libraries :: Python Modules
+Classifier: Framework :: TurboGears
diff --git a/ipa-server/ipa-gui/ipa_gui.egg-info/SOURCES.txt b/ipa-server/ipa-gui/ipa_gui.egg-info/SOURCES.txt
new file mode 100644
index 00000000..be2f0206
--- /dev/null
+++ b/ipa-server/ipa-gui/ipa_gui.egg-info/SOURCES.txt
@@ -0,0 +1,30 @@
+README.txt
+ipa-webgui
+setup.py
+start-ipagui.py
+ipa_gui.egg-info/PKG-INFO
+ipa_gui.egg-info/SOURCES.txt
+ipa_gui.egg-info/dependency_links.txt
+ipa_gui.egg-info/entry_points.txt
+ipa_gui.egg-info/not-zip-safe
+ipa_gui.egg-info/paster_plugins.txt
+ipa_gui.egg-info/requires.txt
+ipa_gui.egg-info/sqlobject.txt
+ipa_gui.egg-info/top_level.txt
+ipagui/__init__.py
+ipagui/controllers.py
+ipagui/json.py
+ipagui/model.py
+ipagui/proxyprovider.py
+ipagui/proxyvisit.py
+ipagui/release.py
+ipagui/config/__init__.py
+ipagui/forms/__init__.py
+ipagui/forms/group.py
+ipagui/forms/user.py
+ipagui/helpers/__init__.py
+ipagui/helpers/userhelper.py
+ipagui/templates/__init__.py
+ipagui/tests/__init__.py
+ipagui/tests/test_controllers.py
+ipagui/tests/test_model.py
diff --git a/ipa-server/ipa-gui/ipa_gui.egg-info/dependency_links.txt b/ipa-server/ipa-gui/ipa_gui.egg-info/dependency_links.txt
new file mode 100644
index 00000000..8b137891
--- /dev/null
+++ b/ipa-server/ipa-gui/ipa_gui.egg-info/dependency_links.txt
@@ -0,0 +1 @@
+
diff --git a/ipa-server/ipa-gui/ipa_gui.egg-info/entry_points.txt b/ipa-server/ipa-gui/ipa_gui.egg-info/entry_points.txt
new file mode 100644
index 00000000..baecc2ce
--- /dev/null
+++ b/ipa-server/ipa-gui/ipa_gui.egg-info/entry_points.txt
@@ -0,0 +1,6 @@
+
+ [turbogears.identity.provider]
+ proxyprovider = ipagui.proxyprovider:ProxyIdentityProvider
+ [turbogears.visit.manager]
+ proxyvisit = ipagui.proxyvisit:ProxyVisitManager
+ \ No newline at end of file
diff --git a/ipa-server/ipa-gui/ipa_gui.egg-info/not-zip-safe b/ipa-server/ipa-gui/ipa_gui.egg-info/not-zip-safe
new file mode 100644
index 00000000..8b137891
--- /dev/null
+++ b/ipa-server/ipa-gui/ipa_gui.egg-info/not-zip-safe
@@ -0,0 +1 @@
+
diff --git a/ipa-server/ipa-gui/ipa_gui.egg-info/paster_plugins.txt b/ipa-server/ipa-gui/ipa_gui.egg-info/paster_plugins.txt
new file mode 100644
index 00000000..14fec70a
--- /dev/null
+++ b/ipa-server/ipa-gui/ipa_gui.egg-info/paster_plugins.txt
@@ -0,0 +1,2 @@
+TurboGears
+PasteScript
diff --git a/ipa-server/ipa-gui/ipa_gui.egg-info/requires.txt b/ipa-server/ipa-gui/ipa_gui.egg-info/requires.txt
new file mode 100644
index 00000000..aecc3096
--- /dev/null
+++ b/ipa-server/ipa-gui/ipa_gui.egg-info/requires.txt
@@ -0,0 +1 @@
+TurboGears >= 1.0.2.2 \ No newline at end of file
diff --git a/ipa-server/ipa-gui/ipa_gui.egg-info/sqlobject.txt b/ipa-server/ipa-gui/ipa_gui.egg-info/sqlobject.txt
new file mode 100644
index 00000000..cc84b8d0
--- /dev/null
+++ b/ipa-server/ipa-gui/ipa_gui.egg-info/sqlobject.txt
@@ -0,0 +1,2 @@
+db_module=ipagui.model
+history_dir=$base/ipagui/sqlobject-history
diff --git a/ipa-server/ipa-gui/ipa_gui.egg-info/top_level.txt b/ipa-server/ipa-gui/ipa_gui.egg-info/top_level.txt
new file mode 100644
index 00000000..2ba21519
--- /dev/null
+++ b/ipa-server/ipa-gui/ipa_gui.egg-info/top_level.txt
@@ -0,0 +1 @@
+ipagui
diff --git a/ipa-server/ipa-gui/ipa_webgui b/ipa-server/ipa-gui/ipa_webgui
new file mode 100644
index 00000000..677fc9a5
--- /dev/null
+++ b/ipa-server/ipa-gui/ipa_webgui
@@ -0,0 +1,207 @@
+#! /usr/bin/python -E
+#
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+import os, sys, pwd, grp
+from optparse import OptionParser
+import traceback
+import logging
+import signal
+
+def usage():
+ print "ipa_webgui [-f|--foreground] [-d|--debug]"
+ sys.exit(1)
+
+def parse_options():
+ parser = OptionParser()
+ parser.add_option("-f", "--foreground", dest="foreground",
+ action="store_true", default=False,
+ help="Remain in the foreground")
+ parser.add_option("-d", "--debug", dest="debug", action="store_true",
+ default=False,
+ help="Increase the amount of logging information")
+ parser.add_option("--usage", action="store_true",
+ help="Program usage")
+
+ options, args = parser.parse_args(sys.argv)
+
+ return options, args
+
+def daemonize():
+ # fork once so the parent can exit
+ try:
+ pid = os.fork()
+ except OSError, e:
+ raise Exception, "%s [%d]" % (e.strerror, e.errno)
+
+ if pid != 0:
+ os._exit(0)
+
+ # become session leader
+ os.setsid()
+
+ # fork again to reparent to init
+ try:
+ pid = os.fork()
+ except OSError, e:
+ raise Exception, "%s [%d]" % (e.strerror, e.errno)
+
+ if pid != 0:
+ os._exit(0)
+
+ os.chdir("/")
+ os.umask(0)
+
+ import resource
+ maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
+ if (maxfd == resource.RLIM_INFINITY):
+ maxfd = 1024
+
+ # close all file descriptors
+ for fd in range(0, maxfd):
+ try:
+ os.close(fd)
+ except OSError:
+ pass
+
+ # stdin
+ os.open("/dev/null", os.O_RDWR)
+ # stdout
+ os.open("/dev/null", os.O_RDWR)
+ # stderr
+ os.open("/dev/null", os.O_RDWR)
+
+def main():
+ options, args = parse_options()
+
+ foreground = options.foreground
+
+ if options.debug:
+ loglevel = logging.DEBUG
+ else:
+ loglevel = logging.WARN
+
+ # To make development easier, we detect if we are in the development
+ # environment to load a different configuration and avoid becoming
+ # a daemon
+ devel = False
+ if os.path.exists(os.path.join(os.path.dirname(__file__), "Makefile.am")):
+ devel = True
+ foreground = True
+
+ if not foreground:
+ try:
+ daemonize()
+ except Exception, e:
+ sys.stderr.write("error becoming daemon: " + str(e))
+ sys.exit(1)
+
+ if not foreground:
+ try:
+ daemonize()
+ except Exception, e:
+ sys.stderr.write("error becoming daemon: " + str(e))
+ sys.exit(1)
+
+ # Drop privileges and write our pid file only if we're running as root
+ if os.getuid() == 0:
+ # Write out our pid file
+ pidfile = open("/var/run/ipa_webgui.pid", "w")
+ pidfile.write(str(os.getpid()))
+ pidfile.close()
+
+ # Drop privs
+ apache_uid = pwd.getpwnam("apache")[2]
+ apache_gid = grp.getgrnam("apache")[2]
+
+ try:
+ os.setgid(apache_gid)
+ except OSError, e:
+ log.error("Could not set effective group id: %s" % e)
+
+ try:
+ os.setuid(apache_uid)
+ except OSError, e:
+ log.error("Could not set effective user id: %s" % e)
+
+ if foreground:
+ logging.basicConfig(level=loglevel,
+ format='%(asctime)s %(name)s %(levelname)s %(message)s',
+ stream=sys.stderr)
+ else:
+ # This log file name needs to be kept in sync with the one in
+ # ipa_webgui.cfg
+ logging.basicConfig(level=loglevel,
+ format='%(asctime)s %(name)s %(levelname)s %(message)s',
+ filename='/var/log/ipa_error.log')
+
+ sys.path.append("/usr/share/ipa")
+
+ # this must be after sys.path is changed to work correctly
+ import pkg_resources
+ pkg_resources.require("TurboGears")
+ pkg_resources.require("ipa_gui")
+
+ from turbogears import update_config, start_server
+ from turbogears.config import update
+ import cherrypy
+ cherrypy.lowercase_api = True
+
+ try:
+ if hasattr(signal, "SIGTERM"):
+ def SIGTERM(signum=None, frame=None):
+ cherrypy.server.stop()
+ signal.signal(signal.SIGTERM, SIGTERM)
+ except ValueError, _signal_exc:
+ if _signal_exc.args[0] != "signal only works in main thread":
+ raise
+
+
+ # Shut down the logging set up here so that CherryPy logging can take
+ # over. TurboGears configuration errors will not be caught.
+ if not foreground:
+ logging.shutdown()
+
+ # Load the config - look for a local file first for development
+ # and then the system config file
+ if devel:
+ update_config(configfile="dev.cfg",
+ modulename="ipagui.config")
+ update( { "i18n.locale_dir": "locales"} )
+ else:
+ update_config(configfile="/usr/share/ipa/ipa_webgui.cfg",
+ modulename="ipagui.config.app")
+ update( { "i18n.locale_dir": "/usr/share/ipa/locales"} )
+
+ from ipagui.controllers import Root
+
+ start_server(Root())
+
+try:
+ main()
+ sys.exit(0)
+except SystemExit, e:
+ sys.exit(e)
+except Exception, e:
+ message = "failed to start web gui: %s" % str(e)
+ print message
+ for str in traceback.format_tb(sys.exc_info()[2]):
+ message = message + "\n" + str
+ logging.error(message)
+
+ sys.exit(1)
diff --git a/ipa-server/ipa-gui/ipa_webgui.cfg b/ipa-server/ipa-gui/ipa_webgui.cfg
new file mode 100644
index 00000000..5d0f6f8a
--- /dev/null
+++ b/ipa-server/ipa-gui/ipa_webgui.cfg
@@ -0,0 +1,109 @@
+[global]
+# This is where all of your settings go for your production environment.
+# Settings that are the same for both development and production
+# (such as template engine, encodings, etc.) all go in
+# ipagui/config/app.cfg
+
+# DATABASE
+
+# no database for ipa_webgui since everything is stored in LDAP
+
+# SERVER
+
+server.environment="production"
+autoreload.package="ipagui"
+autoreload.on = False
+
+# Sets the number of threads the server uses
+server.thread_pool = 10
+
+# if this is part of a larger site, you can set the path
+# to the TurboGears instance here
+server.webpath="/ipa/ui"
+
+# Set to True if you are deploying your App behind a proxy
+# e.g. Apache using mod_proxy
+base_url_filter.on = True
+
+# Set to True if your proxy adds the x_forwarded_host header
+# base_url_filter.use_x_forwarded_host = True
+
+# If your proxy does not add the x_forwarded_host header, set
+# the following to the *public* host url.
+# (Note: This will be overridden by the use_x_forwarded_host option
+# if it is set to True and the proxy adds the header correctly.
+# base_url_filter.base_url = "http://www.example.com"
+
+# Set to True if you'd like to abort execution if a controller gets an
+# unexpected parameter. False by default
+# tg.strict_parameters = False
+
+# Set to True if you want to use internationalization support.
+i18n.run_template_filter = True
+
+# TurboGears sessions.
+session_filter.storage_type='File'
+session_filter.storage_path='/var/cache/ipa/sessions'
+
+# Listen only on the local interface so all requests go through
+# Apache/mod_auth_kerb/mod_proxy.
+server.socket_port = 8080
+server.socket_host="127.0.0.1"
+
+# LOGGING
+# Logging configuration generally follows the style of the standard
+# Python logging module configuration. Note that when specifying
+# log format messages, you need to use *() for formatting variables.
+# Deployment independent log configuration is in ipagui/config/log.cfg
+[logging]
+
+[[formatters]]
+[[[message_only]]]
+format='*(message)s'
+
+[[[full_content]]]
+format='*(asctime)s *(name)s *(levelname)s *(message)s'
+
+[[handlers]]
+[[[debug_out]]]
+# Rotate weekly on Sunday. Keep 4 backups of the log
+class='TimedRotatingFileHandler'
+level='DEBUG'
+args="('/var/log/ipa_error.log', 'w6', 1, 4)"
+formatter='full_content'
+
+[[[access_out]]]
+# For example only if one wants to duplicate the access log in TurboGears
+# Rotate weekly on Sunday. Keep 4 backups of the log
+#class='TimedRotatingFileHandler'
+#level='INFO'
+#args="('/var/log/ipa_error.log', 'w6', 1, 4)"
+#formatter='message_only'
+# By default log access to stdout which will go to /dev/null in production
+class='StreamHandler'
+level='INFO'
+args='(sys.stdout,)'
+formatter='message_only'
+
+[[[error_out]]]
+class='StreamHandler'
+level='ERROR'
+args='(sys.stdout,)'
+
+[[loggers]]
+[[[ipagui]]]
+level='DEBUG'
+qualname='ipagui'
+handlers=['debug_out']
+propagate=0
+
+[[[allinfo]]]
+level='INFO'
+handlers=['debug_out']
+propagate=0
+
+[[[access]]]
+level='INFO'
+qualname='turbogears.access'
+handlers=['access_out']
+propagate=0
diff --git a/ipa-server/ipa-gui/ipa_webgui.init b/ipa-server/ipa-gui/ipa_webgui.init
new file mode 100644
index 00000000..42478588
--- /dev/null
+++ b/ipa-server/ipa-gui/ipa_webgui.init
@@ -0,0 +1,79 @@
+#!/bin/sh
+#
+# ipa_webgui This starts and stops ipa_webgui
+#
+# chkconfig: - 36 64
+# description: ipa_webgui IPA Web User Interface
+# processname: /usr/sbin/ipa_webgui
+# configdir: /etc/sysconfig/ipa_webgui
+#
+
+# Source function library.
+if [ -f /etc/rc.d/init.d/functions ] ; then
+. /etc/rc.d/init.d/functions
+fi
+# Source networking configuration.
+if [ -f /etc/sysconfig/network ] ; then
+. /etc/sysconfig/network
+fi
+
+# Check that networking is up.
+if [ "${NETWORKING}" = "no" ]
+then
+ echo "Networking is down"
+ exit 0
+fi
+
+NAME="ipa_webgui"
+PROG="/usr/sbin/ipa_webgui"
+RUNAS="apache"
+
+start() {
+ echo -n $"Starting $NAME: "
+ daemon $PROG
+ RETVAL=$?
+ echo
+ [ $RETVAL -eq 0 ] && touch /var/lock/subsys/ipa_webgui || \
+ RETVAL=1
+ return $RETVAL
+}
+
+stop() {
+ echo -n $"Shutting down $NAME: "
+ killproc $NAME
+ RETVAL=$?
+ echo
+ [ $RETVAL -eq 0 ] && rm -f /var/lock/subsys/ipa_webgui
+ return $RETVAL
+}
+
+restart() {
+ stop
+ start
+}
+
+case "$1" in
+ start)
+ start
+ ;;
+ stop)
+ stop
+ ;;
+ status)
+ status $PROG
+ ;;
+ restart)
+ restart
+ ;;
+ condrestart)
+ [ -f /var/lock/subsys/ipa_webgui ] && restart || :
+ ;;
+ reload)
+ exit 3
+ ;;
+ *)
+ echo $"Usage: $0 {start|stop|status|restart|condrestart}"
+ exit 2
+esac
+
+exit $?
diff --git a/ipa-server/ipa-gui/ipagui/Makefile.am b/ipa-server/ipa-gui/ipagui/Makefile.am
new file mode 100644
index 00000000..83636323
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/Makefile.am
@@ -0,0 +1,30 @@
+NULL =
+
+SUBDIRS = \
+ config \
+ forms \
+ helpers \
+ static \
+ subcontrollers \
+ templates \
+ tests \
+ $(NULL)
+
+appdir = $(IPA_DATA_DIR)/ipagui
+app_PYTHON = \
+ __init__.py \
+ controllers.py \
+ json.py \
+ model.py \
+ proxyprovider.py \
+ proxyvisit.py \
+ release.py \
+ $(NULL)
+
+EXTRA_DIST = \
+ $(NULL)
+
+MAINTAINERCLEANFILES = \
+ *~ \
+ *.pyc \
+ Makefile.in
diff --git a/ipa-server/ipa-gui/ipagui/__init__.py b/ipa-server/ipa-gui/ipagui/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/__init__.py
diff --git a/ipa-server/ipa-gui/ipagui/config/Makefile.am b/ipa-server/ipa-gui/ipagui/config/Makefile.am
new file mode 100644
index 00000000..db96758f
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/config/Makefile.am
@@ -0,0 +1,20 @@
+NULL =
+
+appdir = $(IPA_DATA_DIR)/ipagui/config
+app_PYTHON = \
+ __init__.py \
+ $(NULL)
+
+app_DATA = \
+ app.cfg \
+ log.cfg \
+ $(NULL)
+
+EXTRA_DIST = \
+ $(app_DATA) \
+ $(NULL)
+
+MAINTAINERCLEANFILES = \
+ *~ \
+ *.pyc \
+ Makefile.in
diff --git a/ipa-server/ipa-gui/ipagui/config/__init__.py b/ipa-server/ipa-gui/ipagui/config/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/config/__init__.py
diff --git a/ipa-server/ipa-gui/ipagui/config/app.cfg b/ipa-server/ipa-gui/ipagui/config/app.cfg
new file mode 100644
index 00000000..01bb5ad2
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/config/app.cfg
@@ -0,0 +1,104 @@
+[global]
+# The settings in this file should not vary depending on the deployment
+# environment. dev.cfg and prod.cfg are the locations for
+# the different deployment settings. Settings in this file will
+# be overridden by settings in those other files.
+
+# The commented out values below are the defaults
+
+# VIEW
+
+# which view (template engine) to use if one is not specified in the
+# template name
+# tg.defaultview = "kid"
+
+# The following kid settings determine the settings used by the kid serializer.
+
+# One of (html|html-strict|xhtml|xhtml-strict|xml|json)
+# kid.outputformat="html"
+
+# kid.encoding="utf-8"
+
+# The sitetemplate is used for overall styling of a site that
+# includes multiple TurboGears applications
+# tg.sitetemplate="<packagename.templates.templatename>"
+
+# Allow every exposed function to be called as json,
+# tg.allow_json = False
+
+# List of Widgets to include on every page.
+# for exemple ['turbogears.mochikit']
+# tg.include_widgets = []
+
+# Set to True if the scheduler should be started
+# tg.scheduler = False
+
+# Set session or cookie
+session_filter.on = True
+
+# VISIT TRACKING
+# Each visit to your application will be assigned a unique visit ID tracked via
+# a cookie sent to the visitor's browser.
+# --------------
+
+# Enable Visit tracking
+visit.on=True
+
+# Number of minutes a visit may be idle before it expires.
+# visit.timeout=20
+
+# The name of the cookie to transmit to the visitor's browser.
+# visit.cookie.name="tg-visit"
+
+# Domain name to specify when setting the cookie (must begin with . according to
+# RFC 2109). The default (None) should work for most cases and will default to
+# the machine to which the request was made. NOTE: localhost is NEVER a valid
+# value and will NOT WORK.
+# visit.cookie.domain=None
+
+# Specific path for the cookie
+# visit.cookie.path="/"
+
+# The name of the VisitManager plugin to use for visitor tracking.
+visit.manager="proxyvisit"
+
+# IDENTITY
+# General configuration of the TurboGears Identity management module
+# --------
+
+# Switch to turn on or off the Identity management module
+identity.on=True
+
+# [REQUIRED] URL to which CherryPy will internally redirect when an access
+# control check fails. If Identity management is turned on, a value for this
+# option must be specified.
+identity.failure_url="/loginfailed"
+
+identity.provider='proxyprovider'
+
+# The names of the fields on the login form containing the visitor's user ID
+# and password. In addition, the submit button is specified simply so its
+# existence may be stripped out prior to passing the form data to the target
+# controller.
+# identity.form.user_name="user_name"
+# identity.form.password="password"
+# identity.form.submit="login"
+
+# What sources should the identity provider consider when determining the
+# identity associated with a request? Comma separated list of identity sources.
+# Valid sources: form, visit, http_auth
+# identity.source="form,http_auth,visit"
+identity.source="visit"
+
+# compress the data sends to the web browser
+# [/]
+# gzip_filter.on = True
+# gzip_filter.mime_types = ["application/x-javascript", "text/javascript", "text/html", "text/css", "text/plain"]
+
+[/static]
+static_filter.on = True
+static_filter.dir = "%(top_level_dir)s/static"
+
+[/favicon.ico]
+static_filter.on = True
+static_filter.file = "%(top_level_dir)s/static/images/favicon.ico"
diff --git a/ipa-server/ipa-gui/ipagui/config/log.cfg b/ipa-server/ipa-gui/ipagui/config/log.cfg
new file mode 100644
index 00000000..483069cd
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/config/log.cfg
@@ -0,0 +1,32 @@
+# LOGGING
+# Logging is often deployment specific, but some handlers and
+# formatters can be defined here.
+
+[logging]
+[[formatters]]
+[[[message_only]]]
+format='*(message)s'
+
+[[[full_content]]]
+format='*(asctime)s *(name)s *(levelname)s *(message)s'
+
+[[[datestamped]]]
+format='*(asctime)s *(message)s'
+
+[[handlers]]
+[[[debug_out]]]
+class='StreamHandler'
+level='DEBUG'
+args='(sys.stdout,)'
+formatter='full_content'
+
+[[[access_out]]]
+class='StreamHandler'
+level='INFO'
+args='(sys.stdout,)'
+formatter='datestamped'
+
+[[[error_out]]]
+class='StreamHandler'
+level='ERROR'
+args='(sys.stdout,)'
diff --git a/ipa-server/ipa-gui/ipagui/controllers.py b/ipa-server/ipa-gui/ipagui/controllers.py
new file mode 100644
index 00000000..024a804d
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/controllers.py
@@ -0,0 +1,135 @@
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+import logging
+import StringIO
+import traceback
+
+import cherrypy
+import turbogears
+from turbogears import controllers, expose, flash
+from turbogears import config
+from turbogears import validators, validate
+from turbogears import widgets, paginate
+from turbogears import error_handler
+from turbogears import identity
+
+import ipa.config
+import ipa.ipaclient
+
+from subcontrollers.user import UserController
+from subcontrollers.group import GroupController
+from subcontrollers.delegation import DelegationController
+from subcontrollers.policy import PolicyController
+from subcontrollers.ipapolicy import IPAPolicyController
+from subcontrollers.principal import PrincipalController
+
+ipa.config.init_config()
+
+log = logging.getLogger(__name__)
+
+class Root(controllers.RootController):
+
+ user = UserController()
+ group = GroupController()
+ delegate = DelegationController()
+ policy = PolicyController()
+ ipapolicy = IPAPolicyController()
+ principal = PrincipalController()
+
+ @expose(template="ipagui.templates.welcome")
+ @identity.require(identity.not_anonymous())
+ def index(self):
+ return dict()
+
+ @expose()
+ @identity.require(identity.not_anonymous())
+ def topsearch(self, **kw):
+ if kw.get('searchtype') == "Users":
+ return Root.user.list(uid=kw.get('searchvalue'))
+ else:
+ return Root.group.list(criteria=kw.get('searchvalue'))
+
+ @expose("ipagui.templates.loginfailed")
+ def loginfailed(self, **kw):
+ return dict()
+
+
+ _error_codes = {
+ None: u'General Error',
+ 400: u'400 - Bad Request',
+ 401: u'401 - Unauthorized',
+ 403: u'403 - Forbidden',
+ 404: u'404 - Not Found',
+ 500: u'500 - Internal Server Error',
+ 501: u'501 - Not Implemented',
+ 502: u'502 - Bad Gateway',
+ }
+
+ def handle_error(self, status, message):
+ """This method is derived from the sample error catcher on
+ http://docs.turbogears.org/1.0/ErrorReporting."""
+ try:
+ error_msg = self._error_codes.get(status, self._error_codes[None])
+ url = "%s %s" % (cherrypy.request.method, cherrypy.request.path)
+ if (status == 500):
+ log.exception("%s error (%s) for request '%s'", status,
+ error_msg, url)
+ else:
+ log.error("%s error (%s) for request '%s'", status,
+ error_msg, url)
+
+ if config.get('server.environment') == 'production':
+ details = ''
+ else:
+ buf = StringIO.StringIO()
+ traceback.print_exc(file=buf)
+ details = buf.getvalue()
+ buf.close()
+
+ data = dict(
+ status = status,
+ message = message,
+ error_msg = error_msg,
+ url = url,
+ details = details,
+ )
+
+ if status == 404:
+ page_template = 'ipagui.templates.not_found'
+ else:
+ page_template = 'ipagui.templates.unhandled_exception'
+
+ body = controllers._process_output(
+ data,
+ page_template,
+ 'html',
+ 'text/html',
+ None
+ )
+ cherrypy.response.headers['Content-Length'] = len(body)
+ cherrypy.response.body = body
+
+ # don't catch SystemExit
+ except StandardError, exc:
+ log.exception('Error handler failed: %s', exc)
+
+ # To hook in error handler for production only:
+ # if config.get('server.environment') == 'production':
+ # _cp_on_http_error = handle_error
+
+ _cp_on_http_error = handle_error
diff --git a/ipa-server/ipa-gui/ipagui/forms/Makefile.am b/ipa-server/ipa-gui/ipagui/forms/Makefile.am
new file mode 100644
index 00000000..a7f3c762
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/forms/Makefile.am
@@ -0,0 +1,19 @@
+NULL =
+
+appdir = $(IPA_DATA_DIR)/ipagui/forms
+app_PYTHON = \
+ __init__.py \
+ group.py \
+ ipapolicy.py \
+ user.py \
+ delegate.py \
+ principal.py \
+ $(NULL)
+
+EXTRA_DIST = \
+ $(NULL)
+
+MAINTAINERCLEANFILES = \
+ *~ \
+ *.pyc \
+ Makefile.in
diff --git a/ipa-server/ipa-gui/ipagui/forms/__init__.py b/ipa-server/ipa-gui/ipagui/forms/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/forms/__init__.py
diff --git a/ipa-server/ipa-gui/ipagui/forms/delegate.py b/ipa-server/ipa-gui/ipagui/forms/delegate.py
new file mode 100644
index 00000000..e7ba03f9
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/forms/delegate.py
@@ -0,0 +1,110 @@
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+import turbogears
+from turbogears import validators, widgets
+from ipagui.helpers import ipahelper
+
+from ipagui.forms.user import UserFields
+
+# TODO - get from config or somewhere
+aci_attrs = [
+ UserFields.givenname,
+ UserFields.sn,
+ UserFields.cn,
+ UserFields.title,
+ UserFields.displayname,
+ UserFields.initials,
+ UserFields.uid,
+ UserFields.krbprincipalkey,
+ UserFields.uidnumber,
+ UserFields.gidnumber,
+ UserFields.homedirectory,
+ UserFields.loginshell,
+ UserFields.gecos,
+ UserFields.mail,
+ UserFields.telephonenumber,
+ UserFields.facsimiletelephonenumber,
+ UserFields.mobile,
+ UserFields.pager,
+ UserFields.homephone,
+ UserFields.street,
+ UserFields.l,
+ UserFields.st,
+ UserFields.postalcode,
+ UserFields.ou,
+ UserFields.businesscategory,
+ UserFields.description,
+ UserFields.employeetype,
+ UserFields.manager,
+ UserFields.roomnumber,
+ UserFields.secretary,
+ UserFields.carlicense,
+ UserFields.labeleduri,
+]
+
+aci_checkbox_attrs = [(field.name, field.label) for field in aci_attrs]
+
+aci_name_to_label = dict(aci_checkbox_attrs)
+
+class DelegateFields(object):
+ name = widgets.TextField(name="name", label="Delegation Name")
+
+ source_group_dn = widgets.HiddenField(name="source_group_dn")
+ dest_group_dn = widgets.HiddenField(name="dest_group_dn")
+
+ source_group_cn = widgets.HiddenField(name="source_group_cn",
+ label="People in Group")
+ dest_group_cn = widgets.HiddenField(name="dest_group_cn",
+ label="For People in Group")
+
+ orig_acistr = widgets.HiddenField(name="orig_acistr")
+
+ attrs = widgets.CheckBoxList(name="attrs", label="Can Modify",
+ options=aci_checkbox_attrs, validator=validators.NotEmpty)
+
+class DelegateValidator(validators.Schema):
+ name = validators.String(not_empty=True)
+ source_group_dn = validators.String(not_empty=True,
+ messages = { 'empty': _("Please choose a group"), })
+ dest_group_dn = validators.String(not_empty=True,
+ messages = { 'empty': _("Please choose a group"), })
+ # There is no attrs validator here because then it shows as one
+ # huge block of color in the form. The validation is done in
+ # the subcontroller.
+
+class DelegateForm(widgets.Form):
+ params = ['delegate_fields', 'attr_list']
+
+ hidden_fields = [
+ DelegateFields.source_group_dn,
+ DelegateFields.dest_group_dn,
+ DelegateFields.source_group_cn,
+ DelegateFields.dest_group_cn,
+ DelegateFields.orig_acistr,
+ ]
+
+ validator = DelegateValidator()
+
+ def __init__(self, *args, **kw):
+ super(DelegateForm,self).__init__(*args, **kw)
+ (self.template_c, self.template) = ipahelper.load_template(
+ "ipagui.templates.delegateform")
+ self.delegate_fields = DelegateFields
+
+ def update_params(self, params):
+ super(DelegateForm,self).update_params(params)
diff --git a/ipa-server/ipa-gui/ipagui/forms/group.py b/ipa-server/ipa-gui/ipagui/forms/group.py
new file mode 100644
index 00000000..564e577a
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/forms/group.py
@@ -0,0 +1,89 @@
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+import turbogears
+from turbogears import validators, widgets
+from tg_expanding_form_widget.tg_expanding_form_widget import ExpandingForm
+from ipagui.helpers import ipahelper,validators
+
+class GroupFields(object):
+ cn = widgets.TextField(name="cn", label="Name")
+ gidnumber = widgets.TextField(name="gidnumber", label="GID")
+ description = widgets.TextField(name="description", label="Description")
+
+ editprotected_hidden = widgets.HiddenField(name="editprotected")
+
+ nsAccountLock = widgets.SingleSelectField(name="nsAccountLock",
+ label="Group Status",
+ options = [("", "active"), ("true", "inactive")])
+
+ group_orig = widgets.HiddenField(name="group_orig")
+ member_data = widgets.HiddenField(name="member_data")
+ dn_to_info_json = widgets.HiddenField(name="dn_to_info_json")
+
+class GroupNewValidator(validators.Schema):
+ filter_extra_fields = True
+ allow_extra_fields = True
+ cn = validators.GoodName(not_empty=True)
+ description = validators.String(not_empty=False)
+
+
+class GroupNewForm(widgets.Form):
+ params = ['group_fields']
+
+ hidden_fields = [
+ GroupFields.dn_to_info_json
+ ]
+
+ validator = GroupNewValidator()
+
+ def __init__(self, *args, **kw):
+ super(GroupNewForm,self).__init__(*args, **kw)
+ (self.template_c, self.template) = ipahelper.load_template("ipagui.templates.groupnewform")
+ self.group_fields = GroupFields
+
+ def update_params(self, params):
+ super(GroupNewForm,self).update_params(params)
+
+
+class GroupEditValidator(validators.Schema):
+ filter_extra_fields = True
+ allow_extra_fields = True
+ cn = validators.GoodName(not_empty=False)
+ gidnumber = validators.Int(not_empty=False)
+ description = validators.String(not_empty=False)
+
+ pre_validators = [
+ validators.RequireIfPresent(required='cn', present='editprotected'),
+ validators.RequireIfPresent(required='gidnumber', present='editprotected'),
+ ]
+
+class GroupEditForm(widgets.Form):
+ params = ['members', 'group_fields']
+
+ hidden_fields = [
+ GroupFields.editprotected_hidden,
+ GroupFields.group_orig, GroupFields.member_data,
+ GroupFields.dn_to_info_json
+ ]
+
+ validator = GroupEditValidator()
+
+ def __init__(self, *args, **kw):
+ super(GroupEditForm,self).__init__(*args, **kw)
+ (self.template_c, self.template) = ipahelper.load_template("ipagui.templates.groupeditform")
+ self.group_fields = GroupFields
diff --git a/ipa-server/ipa-gui/ipagui/forms/ipapolicy.py b/ipa-server/ipa-gui/ipagui/forms/ipapolicy.py
new file mode 100644
index 00000000..7ad9fe08
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/forms/ipapolicy.py
@@ -0,0 +1,87 @@
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+import turbogears
+from turbogears import validators, widgets
+from tg_expanding_form_widget.tg_expanding_form_widget import ExpandingForm
+from ipagui.helpers import ipahelper
+
+class IPAPolicyFields(object):
+ # From cn=ipaConfig
+ ipausersearchfields = widgets.TextField(name="ipausersearchfields", label="User Search Fields", attrs=dict(size=50))
+ ipagroupsearchfields = widgets.TextField(name="ipagroupsearchfields", label="Group Search Fields")
+ ipasearchtimelimit = widgets.TextField(name="ipasearchtimelimit", label="Search Time Limit (sec.)", attrs=dict(size=6,maxlength=6))
+ ipasearchrecordslimit = widgets.TextField(name="ipasearchrecordslimit", label="Search Records Limit", attrs=dict(size=6,maxlength=6))
+ ipahomesrootdir = widgets.TextField(name="ipahomesrootdir", label="Root for Home Directories")
+ ipadefaultloginshell = widgets.TextField(name="ipadefaultloginshell", label="Default Shell")
+ ipadefaultprimarygroup = widgets.TextField(name="ipadefaultprimarygroup", label="Default User Group")
+ ipamaxusernamelength = widgets.TextField(name="ipamaxusernamelength", label="Max. Username Length", attrs=dict(size=3,maxlength=3))
+ ipapwdexpadvnotify = widgets.TextField(name="ipapwdexpadvnotify", label="Password Expiration Notification (days)", attrs=dict(size=3,maxlength=3))
+ ipauserobjectclasses = widgets.TextField(name="ipauserobjectclasses", label="Default User Object Classes", attrs=dict(size=50))
+ userobjectclasses = ExpandingForm(name="userobjectclasses", label="Default User Object Classes", fields=[ipauserobjectclasses])
+ ipagroupobjectclasses = widgets.TextField(name="ipagroupobjectclasses", label="Default Group Object Classes", attrs=dict(size=50))
+ groupobjectclasses = ExpandingForm(name="groupobjectclasses", label="Default Group Object Classes", fields=[ipagroupobjectclasses])
+ ipadefaultemaildomain = widgets.TextField(name="ipadefaultemaildomain", label="Default E-mail Domain", attrs=dict(size=20))
+
+ ipapolicy_orig = widgets.HiddenField(name="ipapolicy_orig")
+
+ # From cn=accounts
+ krbmaxpwdlife = widgets.TextField(name="krbmaxpwdlife", label="Max. Password Lifetime (days)", attrs=dict(size=3,maxlength=3))
+ krbminpwdlife = widgets.TextField(name="krbminpwdlife", label="Min. Password Lifetime (hours)", attrs=dict(size=3,maxlength=3))
+ krbpwdmindiffchars = widgets.TextField(name="krbpwdmindiffchars", label="Min. Number of Character Classes", attrs=dict(size=3,maxlength=3))
+ krbpwdminlength = widgets.TextField(name="krbpwdminlength", label="Min. Length of Password", attrs=dict(size=3,maxlength=3))
+ krbpwdhistorylength = widgets.TextField(name="krbpwdhistorylength", label="Password History Size", attrs=dict(size=3,maxlength=3))
+
+ password_orig = widgets.HiddenField(name="password_orig")
+
+class IPAPolicyValidator(validators.Schema):
+ ipausersearchfields = validators.String(not_empty=True)
+ ipagroupsearchfields = validators.String(not_empty=True)
+ ipasearchtimelimit = validators.Number(not_empty=True)
+ ipasearchrecordslimit = validators.Number(not_empty=True)
+ ipamaxusernamelength = validators.Number(not_empty=True)
+ ipapwdexpadvnotify = validators.Number(not_empty=True)
+ ipahomesrootdir = validators.String(not_empty=True)
+ ipadefaultloginshell = validators.String(not_empty=True)
+ ipadefaultprimarygroup = validators.String(not_empty=True)
+ ipauserobjectclasses = validators.ForEach(validators.String(not_empty=True))
+ ipagroupobjectclasses = validators.ForEach(validators.String(not_empty=True))
+ ipadefaultemaildomain = validators.String(not_empty=True)
+
+ krbmaxpwdlife = validators.Number(not_empty=True)
+ krbminpwdlife = validators.Number(not_empty=True)
+ krbpwdmindiffchars = validators.Number(not_empty=True)
+ krbpwdminlength = validators.Number(not_empty=True)
+ krbpwdhistorylength = validators.Number(not_empty=True)
+
+class IPAPolicyForm(widgets.Form):
+ params = ['ipapolicy_fields']
+
+ hidden_fields = [
+ IPAPolicyFields.ipapolicy_orig, IPAPolicyFields.password_orig
+ ]
+
+ validator = IPAPolicyValidator()
+
+ def __init__(self, *args, **kw):
+ super(IPAPolicyForm,self).__init__(*args, **kw)
+ (self.template_c, self.template) = ipahelper.load_template(
+ "ipagui.templates.ipapolicyeditform")
+ self.ipapolicy_fields = IPAPolicyFields
+
+ def update_params(self, params):
+ super(IPAPolicyForm,self).update_params(params)
diff --git a/ipa-server/ipa-gui/ipagui/forms/principal.py b/ipa-server/ipa-gui/ipagui/forms/principal.py
new file mode 100644
index 00000000..8ff5eed0
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/forms/principal.py
@@ -0,0 +1,55 @@
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+import turbogears
+from turbogears import validators, widgets
+from tg_expanding_form_widget.tg_expanding_form_widget import ExpandingForm
+from ipagui.helpers import ipahelper
+
+class PrincipalFields(object):
+ hostname = widgets.TextField(name="hostname", label="Host Name")
+ service = widgets.SingleSelectField(name="service",
+ label="Service Type",
+ options = [
+ ("cifs", "cifs"),
+ ("dns", "dns"),
+ ("host", "host"),
+ ("HTTP", "HTTP"),
+ ("ldap", "ldap"),
+ ("nfs", "nfs"),
+ ("other", "other")
+ ],
+ attrs=dict(onchange="toggleOther(this.id)"))
+ other = widgets.TextField(name="other", label="Other Service", attrs=dict(size=10))
+
+class PrincipalNewValidator(validators.Schema):
+ hostname = validators.String(not_empty=True)
+ service = validators.String(not_empty=True)
+ other = validators.String(not_empty=False)
+
+class PrincipalNewForm(widgets.Form):
+ params = ['principal_fields']
+
+ validator = PrincipalNewValidator()
+
+ def __init__(self, *args, **kw):
+ super(PrincipalNewForm,self).__init__(*args, **kw)
+ (self.template_c, self.template) = ipahelper.load_template("ipagui.templates.principalnewform")
+ self.principal_fields = PrincipalFields
+
+ def update_params(self, params):
+ super(PrincipalNewForm,self).update_params(params)
diff --git a/ipa-server/ipa-gui/ipagui/forms/user.py b/ipa-server/ipa-gui/ipagui/forms/user.py
new file mode 100644
index 00000000..62fc0dfd
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/forms/user.py
@@ -0,0 +1,207 @@
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+import turbogears
+from turbogears import validators, widgets
+from tg_expanding_form_widget.tg_expanding_form_widget import ExpandingForm
+from ipagui.helpers.validators import *
+from ipagui.helpers import ipahelper
+
+class UserFields(object):
+ givenname = widgets.TextField(name="givenname", label="First Name")
+ sn = widgets.TextField(name="sn", label="Last Name")
+ cn = widgets.TextField(name="cn", label="Full Name")
+ cns = ExpandingForm(name="cns", label="Full Name", fields=[cn])
+ title = widgets.TextField(name="title", label="Job Title")
+ displayname = widgets.TextField(name="displayname", label="Display Name")
+ initials = widgets.TextField(name="initials", label="Initials")
+
+ uid = widgets.TextField(name="uid", label="Login", attrs=dict(onchange="warnRDN(this.id)"))
+ krbprincipalkey = widgets.PasswordField(name="krbprincipalkey", label="Password")
+ krbprincipalkey_confirm = widgets.PasswordField(name="krbprincipalkey_confirm",
+ label="Confirm Password")
+ uidnumber = widgets.TextField(name="uidnumber", label="UID")
+ gidnumber = widgets.TextField(name="gidnumber", label="GID")
+ homedirectory = widgets.TextField(name="homedirectory", label="Home Directory")
+ loginshell = widgets.TextField(name="loginshell", label="Login Shell")
+ gecos = widgets.TextField(name="gecos", label="GECOS")
+
+ mail = widgets.TextField(name="mail", label="E-mail Address")
+ telephonenumber = widgets.TextField(name="telephonenumber", label="Work Number")
+ telephonenumbers = ExpandingForm(name="telephonenumbers", label="Work Numbers", fields=[telephonenumber])
+ facsimiletelephonenumber = widgets.TextField(name="facsimiletelephonenumber",
+ label="Fax Number")
+ facsimiletelephonenumbers = ExpandingForm(name="facsimiletelephonenumbers", label="Fax Numbers", fields=[facsimiletelephonenumber])
+ mobile = widgets.TextField(name="mobile", label="Cell Number")
+ mobiles = ExpandingForm(name="mobiles", label="Cell Numbers", fields=[mobile])
+ pager = widgets.TextField(name="pager", label="Pager Number")
+ pagers = ExpandingForm(name="pagers", label="Pager Numbers", fields=[pager])
+ homephone = widgets.TextField(name="homephone", label="Home Number")
+ homephones = ExpandingForm(name="homephones", label="Home Numbers", fields=[homephone])
+
+ street = widgets.TextField(name="street", label="Street Address")
+ l = widgets.TextField(name="l", label="City")
+ st = widgets.TextField(name="st", label="State")
+ postalcode = widgets.TextField(name="postalcode", label="ZIP")
+
+ ou = widgets.TextField(name="ou", label="Org Unit")
+ businesscategory = widgets.TextField(name="businesscategory", label="Tags")
+ description = widgets.TextField(name="description", label="Description")
+ employeetype = widgets.TextField(name="employeetype", label="Employee Type")
+ manager = widgets.HiddenField(name="manager", label="Manager")
+ manager_cn = widgets.HiddenField(name="manager_cn", label="Manager")
+ roomnumber = widgets.TextField(name="roomnumber", label="Room Number")
+ secretary = widgets.HiddenField(name="secretary", label="Secretary")
+ secretary_cn = widgets.HiddenField(name="secretary_cn", label="Manager")
+
+ carlicense = widgets.TextField(name="carlicense", label="Car License")
+ labeleduri = widgets.TextField(name="labeleduri", label="Home Page")
+
+ nsAccountLock = widgets.SingleSelectField(name="nsAccountLock",
+ label="Account Status",
+ options = [("", "active"), ("true", "inactive")])
+
+ uid_hidden = widgets.HiddenField(name="uid_hidden")
+ krbPasswordExpiration_hidden = widgets.HiddenField(name="krbPasswordExpiration")
+ editprotected_hidden = widgets.HiddenField(name="editprotected")
+
+ user_orig = widgets.HiddenField(name="user_orig")
+ user_groups_data = widgets.HiddenField(name="user_groups_data")
+ dn_to_info_json = widgets.HiddenField(name="dn_to_info_json")
+
+ custom_fields = []
+
+class UserNewValidator(validators.Schema):
+ uid = GoodName(not_empty=True)
+ krbprincipalkey = validators.String(not_empty=False)
+ krbprincipalkey_confirm = validators.String(not_empty=False)
+ givenname = validators.String(not_empty=True)
+ sn = validators.String(not_empty=True)
+ cn = UniqueList(not_empty=True)
+ mail = validators.Email(not_empty=False)
+ telephonenumber = UniqueList(not_empty=False)
+ facsimiletelephonenumber = UniqueList(not_empty=False)
+ mobile = UniqueList(not_empty=False)
+ pager = UniqueList(not_empty=False)
+ homephone = UniqueList(not_empty=False)
+
+ chained_validators = [
+ validators.FieldsMatch('krbprincipalkey', 'krbprincipalkey_confirm')
+ ]
+
+
+class UserNewForm(widgets.Form):
+ params = ['user_fields', 'custom_fields']
+
+ hidden_fields = [
+ UserFields.dn_to_info_json,
+ UserFields.manager,
+ UserFields.manager_cn,
+ UserFields.secretary,
+ UserFields.secretary_cn,
+ ]
+
+ custom_fields = []
+
+ validator = UserNewValidator()
+
+ def __init__(self, *args, **kw):
+ super(UserNewForm,self).__init__(*args, **kw)
+ (self.template_c, self.template) = ipahelper.load_template("ipagui.templates.usernewform")
+
+ self.user_fields = UserFields
+
+ def update_params(self, params):
+ super(UserNewForm,self).update_params(params)
+
+class UserEditValidator(validators.Schema):
+ uid = GoodName(not_empty=False)
+ krbprincipalkey = validators.String(not_empty=False)
+ krbprincipalkey_confirm = validators.String(not_empty=False)
+ givenname = validators.String(not_empty=True)
+ sn = validators.String(not_empty=True)
+ cn = UniqueList(not_empty=True)
+ mail = validators.Email(not_empty=False)
+ uidnumber = validators.Int(not_empty=False)
+ gidnumber = validators.Int(not_empty=False)
+ telephonenumber = UniqueList(not_empty=False)
+ facsimiletelephonenumber = UniqueList(not_empty=False)
+ mobile = UniqueList(not_empty=False)
+ pager = UniqueList(not_empty=False)
+ homephone = UniqueList(not_empty=False)
+
+ pre_validators = [
+ validators.RequireIfPresent(required='uid', present='editprotected'),
+ validators.RequireIfPresent(required='uidnumber', present='editprotected'),
+ validators.RequireIfPresent(required='gidnumber', present='editprotected'),
+ ]
+
+ chained_validators = [
+ validators.FieldsMatch('krbprincipalkey', 'krbprincipalkey_confirm')
+ ]
+
+class UserEditForm(widgets.Form):
+ params = ['user_fields', 'custom_fields']
+
+ hidden_fields = [
+ UserFields.uid_hidden, UserFields.user_orig,
+ UserFields.krbPasswordExpiration_hidden,
+ UserFields.editprotected_hidden,
+ UserFields.user_groups_data,
+ UserFields.dn_to_info_json,
+ UserFields.manager,
+ UserFields.manager_cn,
+ UserFields.secretary,
+ UserFields.secretary_cn,
+ ]
+
+ custom_fields = []
+
+ validator = UserEditValidator()
+
+ def __init__(self, *args, **kw):
+ super(UserEditForm,self).__init__(*args, **kw)
+ (self.template_c, self.template) = ipahelper.load_template("ipagui.templates.usereditform")
+
+ self.user_fields = UserFields
+
+
+# TODO - add dynamic field retrieval:
+# myfields=[]
+# schema = ipa.rpcclient.get_add_schema ()
+#
+# # FIXME: What if schema is None or an error is thrown?
+#
+# for s in schema:
+# required=False
+#
+# if (s['type'] == "text"):
+# field = widgets.TextField(name=s['name'],label=s['label'])
+# elif (s['type'] == "password"):
+# field = widgets.PasswordField(name=s['name'],label=s['label'])
+#
+# if (s['required'] == "true"):
+# required=True
+#
+# if (s['validator'] == "text"):
+# field.validator=validators.PlainText(not_empty=required)
+# elif (s['validator'] == "email"):
+# field.validator=validators.Email(not_empty=required)
+# elif (s['validator'] == "string"):
+# field.validator=validators.String(not_empty=required)
+#
+# myfields.append(field)
diff --git a/ipa-server/ipa-gui/ipagui/helpers/Makefile.am b/ipa-server/ipa-gui/ipagui/helpers/Makefile.am
new file mode 100644
index 00000000..46185b09
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/helpers/Makefile.am
@@ -0,0 +1,17 @@
+NULL =
+
+appdir = $(IPA_DATA_DIR)/ipagui/helpers
+app_PYTHON = \
+ __init__.py \
+ ipahelper.py \
+ userhelper.py \
+ validators.py \
+ $(NULL)
+
+EXTRA_DIST = \
+ $(NULL)
+
+MAINTAINERCLEANFILES = \
+ *~ \
+ *.pyc \
+ Makefile.in
diff --git a/ipa-server/ipa-gui/ipagui/helpers/__init__.py b/ipa-server/ipa-gui/ipagui/helpers/__init__.py
new file mode 100644
index 00000000..143f486c
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/helpers/__init__.py
@@ -0,0 +1 @@
+# __init__.py
diff --git a/ipa-server/ipa-gui/ipagui/helpers/ipahelper.py b/ipa-server/ipa-gui/ipagui/helpers/ipahelper.py
new file mode 100644
index 00000000..9b340483
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/helpers/ipahelper.py
@@ -0,0 +1,88 @@
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+import re
+import logging
+import turbogears
+import kid
+from turbokid import kidsupport
+from pkg_resources import resource_filename
+
+def javascript_string_escape(input):
+ """Escapes the ' " and \ characters in a string so
+ it can be embedded inside a dynamically generated string."""
+
+ return re.sub(r'[\'\"\\]',
+ lambda match: "\\%s" % match.group(),
+ input)
+
+def setup_mv_fields(field, fieldname):
+ """Given a field (must be a list) and field name, convert that
+ field into a list of dictionaries of the form:
+ [ { fieldname : v1}, { fieldname : v2 }, .. ]
+
+ This is how we pre-fill values for multi-valued fields.
+ """
+ mvlist = []
+ if field:
+ for v in field:
+ if v:
+ mvlist.append({ fieldname : v } )
+ if len(mvlist) == 0:
+ # We need to return an empty value so something can be
+ # displayed on the edit page. Otherwise only an Add link
+ # will show, not an empty field.
+ mvlist.append({ fieldname : '' } )
+ return mvlist
+
+def fix_incoming_fields(fields, fieldname, multifieldname):
+ """This is called by the update() function. It takes the incoming
+ list of dictionaries and converts it into back into the original
+ field, then removes the multiple field.
+ """
+ fields[fieldname] = []
+ try:
+ for i in range(len(fields[multifieldname])):
+ if fields[multifieldname][i][fieldname] is not None and len(fields[multifieldname][i][fieldname]) > 0:
+ fields[fieldname].append(fields[multifieldname][i][fieldname])
+ del(fields[multifieldname])
+ except Exception, e:
+ logging.warn("fix_incoming_fields error: " + str(e))
+
+ return fields
+
+def load_template(classname, encoding=None):
+ """
+ Loads the given template. This only handles .kid files.
+ Returns a tuple (compiled_tmpl, None) to emulate
+ turbogears.meta.load_kid_template() which ends up not properly handling
+ encoding.
+ """
+ if not encoding:
+ encoding = turbogears.config.get('kid.encoding', kidsupport.KidSupport.assume_encoding)
+ divider = classname.rfind(".")
+ package, basename = classname[:divider], classname[divider+1:]
+ file_path = resource_filename(package, basename + ".kid")
+
+ tclass = kid.load_template(
+ file_path,
+ name = classname,
+ ).Template
+ tclass.serializer = kid.HTMLSerializer(encoding=encoding)
+ tclass.assume_encoding=encoding
+
+ return (tclass, None)
diff --git a/ipa-server/ipa-gui/ipagui/helpers/userhelper.py b/ipa-server/ipa-gui/ipagui/helpers/userhelper.py
new file mode 100644
index 00000000..d80c4d3a
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/helpers/userhelper.py
@@ -0,0 +1,46 @@
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+import sys
+import datetime
+
+from ipa import ipautil
+
+def password_expires_in(datestr):
+ """Returns the number of days that password expires in. Returns a negative number
+ if the password is already expired."""
+ if (datestr == None) or (datestr == ""):
+ return sys.maxint
+
+ expdate = ipautil.parse_generalized_time(datestr)
+ if not expdate:
+ return sys.maxint
+
+ delta = expdate - datetime.datetime.now(ipautil.GeneralizedTimeZone())
+ return delta.days
+
+def password_is_expired(days):
+ return days < 0
+
+def password_expires_soon(days):
+ return (not password_is_expired(days)) and (days < 7)
+
+def account_status_display(status):
+ if status == "true":
+ return "inactive"
+ else:
+ return "active"
diff --git a/ipa-server/ipa-gui/ipagui/helpers/validators.py b/ipa-server/ipa-gui/ipagui/helpers/validators.py
new file mode 100644
index 00000000..8ed73b87
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/helpers/validators.py
@@ -0,0 +1,92 @@
+# Copyright (C) 2007-2008 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+from formencode.validators import *
+from formencode.compound import *
+from formencode.api import Invalid, NoDefault
+from formencode.schema import Schema
+from formencode import ForEach
+
+def _(s): return s # dummy
+
+class UniqueList(FancyValidator):
+ """
+ Given a list, ensure that all of the values in it are unique.
+
+ >>> x = UniqueList()
+ >>> x.validate_python(['1','1'],'')
+ Traceback (most recent call last):
+ ...
+ formencode.api.Invalid: Duplicate values are not allowed
+ >>> x.validate_python(['1','2'],'')
+ >>>
+ """
+
+ not_empty = None
+
+ messages = {
+ 'notunique': _('Duplicate values are not allowed'),
+ 'empty': _('Empty values not allowed'),
+ }
+
+ def __initargs__(self, new_attrs):
+ if self.not_empty is None:
+ self.not_empty = True
+
+ def validate_python(self, value, state):
+ if not isinstance(value, list):
+ return # just punt for now
+
+ if self.not_empty:
+ for v in value:
+ if v is None or len(v) == 0:
+ raise Invalid(self.message('empty', state),
+ value, state)
+
+ orig = len(value)
+ check = len(set(value))
+
+ if orig > check:
+ raise Invalid(self.message('notunique', state),
+ value, state)
+
+class GoodName(Regex):
+ """
+ Test that the field contains only letters, numbers, underscore,
+ dash, hyphen and $.
+
+ Examples::
+
+ >>> GoodName.to_python('_this9_')
+ '_this9_'
+ >>> GoodName.from_python(' this ')
+ ' this '
+ >>> GoodName(accept_python=False).from_python(' this ')
+ Traceback (most recent call last):
+ ...
+ Invalid: Enter only letters, numbers, _ (underscore), - (dash) or $')
+ >>> GoodName(strip=True).to_python(' this ')
+ 'this'
+ >>> GoodName(strip=True).from_python(' this ')
+ 'this'
+ """
+
+ regex = r"^[a-zA-Z0-9_.][a-zA-Z0-9_.-]{0,30}[a-zA-Z0-9_.$-]?$"
+
+ messages = {
+ 'invalid': _('Enter only letters, numbers, _ (underscore), - (dash) or $'),
+ }
diff --git a/ipa-server/ipa-gui/ipagui/json.py b/ipa-server/ipa-gui/ipagui/json.py
new file mode 100644
index 00000000..6d912178
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/json.py
@@ -0,0 +1,27 @@
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+# A JSON-based API(view) for your app.
+# Most rules would look like:
+# @jsonify.when("isinstance(obj, YourClass)")
+# def jsonify_yourclass(obj):
+# return [obj.val1, obj.val2]
+# @jsonify can convert your objects to following types:
+# lists, dicts, numbers and strings
+
+from turbojson.jsonify import jsonify
+
diff --git a/ipa-server/ipa-gui/ipagui/model.py b/ipa-server/ipa-gui/ipagui/model.py
new file mode 100644
index 00000000..49820c39
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/model.py
@@ -0,0 +1,26 @@
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+from turbogears.database import PackageHub
+from sqlobject import *
+
+hub = PackageHub('ipagui')
+__connection__ = hub
+
+# class YourDataClass(SQLObject):
+# pass
+
diff --git a/ipa-server/ipa-gui/ipagui/proxyprovider.py b/ipa-server/ipa-gui/ipagui/proxyprovider.py
new file mode 100644
index 00000000..90257d39
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/proxyprovider.py
@@ -0,0 +1,176 @@
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+from turbogears.identity.soprovider import *
+from turbogears.identity.visitor import *
+import logging
+import os
+import ipa.ipaclient
+from ipaserver import funcs
+import ipa.config
+import ipa.group
+import ipa.user
+import ldap
+import krbV
+
+log = logging.getLogger("turbogears.identity")
+
+class IPA_User(object):
+ '''
+ Shell of a User definition. We don't really need much here.
+ '''
+
+ def __init__(self, user_name):
+ self.user_name = user_name
+ (principal, realm) = user_name.split('@')
+ self.permissions = None
+ transport = funcs.IPAServer()
+ client = ipa.ipaclient.IPAClient(transport)
+ client.set_krbccache(os.environ["KRB5CCNAME"])
+ try:
+ # Use memberof so we can see recursive group memberships as well.
+ user = client.get_user_by_principal(user_name, ['dn', 'uid', 'memberof'])
+ self.display_name = user.getValue('uid')
+ self.groups = []
+ memberof = user.getValues('memberof')
+ if memberof is None:
+ # the user isn't in any groups
+ return
+ if isinstance(memberof, str):
+ memberof = [memberof]
+ for mo in memberof:
+ rdn_list = ldap.explode_dn(mo, 0)
+ first_rdn = rdn_list[0]
+ (type,value) = first_rdn.split('=')
+ if type == "cn":
+ self.groups.append(value)
+ except:
+ raise
+
+ return
+
+class ProxyIdentity(object):
+ def __init__(self, visit_key, user=None):
+ self._user= user
+ self.visit_key= visit_key
+
+ def _get_user(self):
+ try:
+ return self._user
+ except AttributeError:
+ # User hasn't already been set
+ return None
+ user= property(_get_user)
+
+ def _get_user_name(self):
+ if not self._user:
+ return None
+ return self._user.user_name
+ user_name= property(_get_user_name)
+
+ def _get_display_name(self):
+ if not self._user:
+ return None
+ return self._user.display_name
+ display_name= property(_get_display_name)
+
+ def _get_anonymous(self):
+ return not self._user
+ anonymous= property(_get_anonymous)
+
+ def _get_permissions(self):
+ try:
+ return self._permissions
+ except AttributeError:
+ # Permissions haven't been computed yet
+ return None
+ permissions= property(_get_permissions)
+
+ def _get_groups(self):
+ try:
+ return self._user.groups
+ except AttributeError:
+ # Groups haven't been computed yet
+ return []
+ groups= property(_get_groups)
+
+ def logout(self):
+ '''
+ Remove the link between this identity and the visit.
+ '''
+ # Clear the current identity
+ anon= ProxyObjectIdentity(None,None)
+ #XXX if user is None anonymous will be true, no need to set attr.
+ #anon.anonymous= True
+ identity.set_current_identity( anon )
+
+class ProxyIdentityProvider(SqlObjectIdentityProvider):
+ '''
+ IdentityProvider that uses REMOTE_USER from Apache
+ '''
+ def __init__(self):
+ super(ProxyIdentityProvider, self).__init__()
+ get = turbogears.config.get
+ # We can get any config variables here
+ log.info( "Proxy Identity starting" )
+
+ def create_provider_model(self):
+ pass
+
+ def validate_identity(self, user_name, password, visit_key):
+ try:
+ user = IPA_User(user_name)
+ log.debug( "validate_identity %s" % user_name)
+ return ProxyIdentity(visit_key, user)
+ except Exception, e:
+ # Something went wrong in fetching the user. Set to
+ # anonymous which will deny access.
+ return ProxyIdentity( None )
+
+ def validate_password(self, user, user_name, password):
+ '''Validation has already occurred in the proxy'''
+ return True
+
+ def load_identity(self, visit_key):
+ try:
+ os.environ["KRB5CCNAME"] = cherrypy.request.headers['X-FORWARDED-KEYTAB']
+ ccache = krbV.CCache(cherrypy.request.headers['X-FORWARDED-KEYTAB'])
+ user_name = ccache.principal().name
+# user_name = "test@FREEIPA.ORG"
+# os.environ["KRB5CCNAME"] = "FILE:/tmp/krb5cc_500"
+ except KeyError:
+ return None
+ except AttributeError:
+ return None
+ except krbV.Krb5Error:
+ return None
+
+ set_login_attempted( True )
+ return self.validate_identity( user_name, None, visit_key )
+
+ def anonymous_identity( self ):
+ '''
+ This shouldn't ever happen in IPA but including it to include the
+ entire identity API.
+ '''
+ return ProxyIdentity( None )
+
+ def authenticated_identity(self, user):
+ '''
+ Constructs Identity object for user that has no associated visit_key.
+ '''
+ return ProxyIdentity(None, user)
diff --git a/ipa-server/ipa-gui/ipagui/proxyvisit.py b/ipa-server/ipa-gui/ipagui/proxyvisit.py
new file mode 100644
index 00000000..91b20d27
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/proxyvisit.py
@@ -0,0 +1,42 @@
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+from turbogears.visit.api import BaseVisitManager, Visit
+from turbogears import config
+
+import logging
+
+log = logging.getLogger("turbogears.visit.proxyvisit")
+
+class ProxyVisitManager(BaseVisitManager):
+ """Virtually empty class just so can avoid saving this stuff in a
+ database."""
+ def __init__(self, timeout):
+ super(ProxyVisitManager,self).__init__(timeout)
+ return
+
+ def create_model(self):
+ return
+
+ def new_visit_with_key(self, visit_key):
+ return Visit(visit_key, True)
+
+ def visit_for_key(self, visit_key):
+ return Visit(visit_key, False)
+
+ def update_queued_visits(self, queue):
+ return None
diff --git a/ipa-server/ipa-gui/ipagui/release.py b/ipa-server/ipa-gui/ipagui/release.py
new file mode 100644
index 00000000..f5bc211c
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/release.py
@@ -0,0 +1,16 @@
+# Release information about ipa-gui
+
+version = "1.0"
+
+# NOTE: We aren't really using this because we aren't shipping the UI as
+# a separate .egg but it might look something like this:
+
+# description = "The Identity, Policy and Audit system"
+# long_description = "IPA is an integrated solution to provide centrally managed Identity (machine, user, virtual machines, groups, authentication credentials), Policy (configuration settings, access control information) and Audit (events, logs, analysis thereof)."
+# author = "Your Name Here"
+# email = "YourEmail@YourDomain"
+# copyright = "2007 Red Hat, Inc."
+
+# url = "http://www.freeipa.org/"
+# download_url = "http://www.freeipa.org/page/Downloads"
+# license = "GPLv2"
diff --git a/ipa-server/ipa-gui/ipagui/static/Makefile.am b/ipa-server/ipa-gui/ipagui/static/Makefile.am
new file mode 100644
index 00000000..d4ca4557
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/static/Makefile.am
@@ -0,0 +1,12 @@
+NULL =
+
+SUBDIRS = \
+ css \
+ images \
+ javascript \
+ $(NULL)
+
+MAINTAINERCLEANFILES = \
+ *~ \
+ *.pyc \
+ Makefile.in
diff --git a/ipa-server/ipa-gui/ipagui/static/css/Makefile.am b/ipa-server/ipa-gui/ipagui/static/css/Makefile.am
new file mode 100644
index 00000000..eb5502ab
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/static/css/Makefile.am
@@ -0,0 +1,17 @@
+NULL =
+
+appdir = $(IPA_DATA_DIR)/ipagui/static/css
+app_DATA = \
+ style_freeipa.css \
+ style_platform.css \
+ style_platform-objects.css \
+ $(NULL)
+
+EXTRA_DIST = \
+ $(app_DATA) \
+ $(NULL)
+
+MAINTAINERCLEANFILES = \
+ *~ \
+ *.pyc \
+ Makefile.in
diff --git a/ipa-server/ipa-gui/ipagui/static/css/style_freeipa.css b/ipa-server/ipa-gui/ipagui/static/css/style_freeipa.css
new file mode 100644
index 00000000..f56971bd
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/static/css/style_freeipa.css
@@ -0,0 +1,62 @@
+/* freeipa-specific styles */
+
+#login {
+ float: right;
+ padding-top: 15px;
+ padding-right: 10px;
+}
+
+#details {
+ border-top: 1px solid #bbdc5f;
+}
+
+#details h1 {
+ background-repeat: no-repeat;
+ margin-bottom: 18px;
+}
+
+
+#alertbox {
+ background-color: #6995d5;
+}
+
+#footer {
+ padding-top: 0px;
+ border-top: none;
+ text-align: center;
+ margin-left: auto;
+ margin-right: auto;
+ width: 30%;
+ padding: 20px 20px;
+}
+
+/*** TableKit CSS - see http://www.millstream.com.au/view/code/tablekit/ **/
+
+.sortcol {
+ cursor: pointer;
+ padding-left: 10px !important;
+ background-repeat: no-repeat !important;
+ background-position: left center !important;
+ text-decoration: underline;
+}
+.sortasc {
+ background-image: url(/ipa/ui/static/images/up.gif) !important;
+}
+.sortdesc {
+ background-image: url(/ipa/ui/static/images/down.gif) !important;
+}
+
+.warning_message {
+ font-size: 120%;
+ font-weight: bolder;
+}
+
+.fielderror {
+ color: red !important;
+ font-weight: bold;
+}
+
+.requiredfield {
+ background-color: #eebbbb !important;
+}
+
diff --git a/ipa-server/ipa-gui/ipagui/static/css/style_platform-objects.css b/ipa-server/ipa-gui/ipagui/static/css/style_platform-objects.css
new file mode 100644
index 00000000..51d57089
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/static/css/style_platform-objects.css
@@ -0,0 +1,19 @@
+/* object h1 styles */
+
+#details h1.overview { background-image: url('/ipa/ui/static/images/objects/object-overview.png'); }
+
+#details h1.accesscontrol { background-image: url('/ipa/ui/static/images/objects/object-accesscontrol.png'); }
+
+#details h1.user { background-image: url('/ipa/ui/static/images/objects/object-user.png'); }
+#details h1.usergroup { background-image: url('/ipa/ui/static/images/objects/object-usergroup.png'); }
+
+#details h1.content-overview { background-image: url('/ipa/ui/static/images/objects/object-content.png'); }
+#details h1.channel { background-image: url('/ipa/ui/static/images/objects/object-channel.png'); }
+#details h1.channel-new { background-image: url('/ipa/ui/static/images/objects/object-channel.png'); }
+#details h1.channels { background-image: url('/ipa/ui/static/images/objects/object-channels.png'); }
+#details h1.media { background-image: url('/ipa/ui/static/images/objects/object-media.png'); }
+
+#details h1.system { background-image: url('/ipa/ui/static/images/objects/object-system.png'); }
+#details h1.virtualsystem { background-image: url('/ipa/ui/static/images/objects/object-virtualsystem.png'); }
+
+#details h1.policy { background-image: url('/ipa/ui/static/images/objects/object-policy.png'); }
diff --git a/ipa-server/ipa-gui/ipagui/static/css/style_platform.css b/ipa-server/ipa-gui/ipagui/static/css/style_platform.css
new file mode 100644
index 00000000..03ac52d3
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/static/css/style_platform.css
@@ -0,0 +1,517 @@
+/** BASIC PAGE STYLES */
+
+*
+{
+ margin: 0;
+ padding: 0;
+ font-size: small;
+}
+
+html, body {
+ height: 100%;
+}
+
+body {
+ background-image: url('/ipa/ui/static/images/template/background.png');
+ background-repeat: repeat-x;
+ background-color: #f9f9f9;
+ margin: 0px;
+ padding: 0px;
+ padding-top: 16px;
+ min-width: 750px;
+}
+
+body, h1, h2, h3, h4, h5, p, ul, li, div, span, td {
+ font-family: "Luxi Sans", "Gill Sans", "Verdana", "Helvetica", sans-serif;
+ font-size: small;
+ color: #444;
+}
+
+td, th {
+ text-align: left;
+}
+
+#head {
+ margin: 0px;
+ padding: 0px 1.5ex;
+}
+
+#head h1 a {
+ display: block;
+ text-indent: -9999px;
+ height: 60px;
+ width: 350px;
+ overflow: hidden;
+ float: left;
+ margin-top: -10px;
+
+ background: url('/ipa/ui/static/images/branding/logo.png') no-repeat;
+}
+
+#content {
+ width: 100%;
+ min-height: 100%;
+
+ background-color: #f9f9f9;
+ background-image: url('/ipa/ui/static/images/template/background-content.png');
+ background-repeat: repeat-x;
+}
+
+#main_content table {
+ clear: left;
+}
+
+#main_content {
+ height: auto;
+ margin-bottom: 4ex;
+}
+
+#footer {
+ font-size: x-small;
+ color: #ccc;
+ clear: both;
+ text-align: center;
+ padding-top: 4ex;
+ border-top: 1px solid #efefef;
+ width: 100%;
+}
+
+/* freeipa only? */
+div#search {
+ padding-top: 16px;
+ padding-bottom: 24px;
+}
+
+#searchbar {
+ float: right;
+ margin-top: 18px;
+}
+
+/** MAIN NAVBAR SECTION **/
+
+#navbar {
+ width: 100%;
+ height: 70px;
+ margin: 0px;
+ clear: both;
+
+ background-image: url('/ipa/ui/static/images/template/background-navbar.png');
+ background-repeat: repeat-x;
+
+}
+
+#navbar ul {
+ margin: 0px;
+ padding: 0px;
+ padding-left: 10px;
+ list-style: none;
+}
+
+#navbar li {
+ float: left;
+ margin: 0px;
+ padding: 0px;
+
+ font-size: small;
+}
+
+#navbar a {
+ display: block;
+ margin: 22px 15px;
+}
+
+#navbar .active {
+ background-image: url('/ipa/ui/static/images/template/background-navbar-active.png');
+ height: 70px;
+ width: 116px;
+
+ text-align: center;
+}
+
+#navbar-secondary li {
+ font-size: medium;
+}
+
+#navbar-secondary .active a:link,
+#navbar-secondary .active a:visited,
+#navbar-secondary .active a:active,
+#navbar-secondary .active a,
+#navbar-secondary li
+{
+ color: #555 !important;
+ text-decoration: none;
+ font-weight: bold;
+}
+
+/** SIDEBAR SECTION **/
+
+#sidebar {
+ width: 250px;
+ text-align: left;
+
+ padding: 18px 12px;
+ margin-right: 24px;
+ float: right;
+ height: 100%;
+
+ border: 1px solid #aaa;
+ background-color: #ccc;
+ background-image: url('/ipa/ui/static/images/template/background-sidebar.png');
+ background-repeat: repeat-y;
+}
+
+#sidebar h1, h2, h3 {
+ padding: 0px;
+ margin: 0px;
+}
+
+#sidebar h2 { font-size: medium; }
+#sidebar h3 { font-size: small; }
+
+#sidebar ul {
+ padding: 0px;
+ margin: 0px;
+ list-style: none;
+ padding-bottom: 10px;
+}
+
+#sidebar ul, #sidebar li {
+ margin-bottom: 6px;
+ font-size: small;
+}
+
+#sidebar hr {
+ border-top: 1px solid #aaa;
+ border-bottom: 1px solid #ddd;
+ color: #ddd;
+ margin-top: 20px !important;
+ margin-bottom: 20px !important;
+}
+
+.context-tools {
+ float: right;
+ margin-top: -1.2em;
+ font-size: small;
+}
+
+.context-tools a:link, .context-tools a:active, .context-tools a:visited {
+ text-decoration: none;
+}
+
+/** DETAILS SECTION **/
+
+#details {
+ height: 100%;
+ margin: 0px 24px;
+ margin-right: 298px;
+
+ padding: 18px 18px;
+ padding-bottom: 12%;
+ border-top: 1px solid #aaa;
+ background-color: white;
+ text-align: left;
+ color: #444;
+}
+
+#details p {
+ margin-top: 1ex;
+ margin-bottom: 1ex;
+}
+
+#details h3 {
+ font-size: medium;
+ text-transform: uppercase;
+ margin-bottom: 1ex;
+ margin-top: 1.5ex;
+}
+
+#details h4 {
+ font-size: medium;
+ color: #8aa445;
+}
+
+
+#details p,
+#details td,
+#details li {
+ font-size: small;
+ color: #555;
+}
+
+#details h1 {
+ color: #7d7d5b;
+ font-size: x-large;
+ margin-bottom: 18px;
+ height: 40px;
+ padding-left: 48px;
+ padding-top: 6px;
+ vertical-align: middle;
+ background-repeat: no-repeat;
+}
+
+
+#details h2, #details table caption {
+ color: #999;
+ font-size: large;
+ font-weight: normal;
+
+ border-bottom: 1px solid #999;
+ margin-bottom: 10px;
+
+ text-align: left;
+ width: 100%;
+}
+
+#details h2 img {
+ margin-right: 1.4ex;
+}
+
+table.details {
+ margin-bottom: 18px;
+ width: 100%;
+}
+
+#details h3, table.formtable th {
+ font-size: small;
+ color: black;
+}
+
+#details table.details th {
+ font-size: small;
+ width: 150px;
+ padding: 4px 0px;
+ padding-right: 8px;
+ border-bottom: 1px dotted #ddd;
+}
+
+#details table.details th.even { background-color: white; }
+#details table.details th.odd { background-color: #eee; }
+
+#details table.details td {
+ padding-left: 8px;
+ padding-bottom: 3px;
+ border-bottom: 1px dotted #ddd;
+}
+
+#details hr {
+ margin-top: 48px;
+ margin-bottom: 12px;
+ height: 1px;
+ border-color: #bbb;
+ border-width: 0pt 0pt 1px;
+ padding: 0.5em;
+ border-style: none none dashed;
+}
+
+.details-block {
+ border-top: 1px solid #eeeeee;
+}
+
+#details ul.context-nav {
+ float: left;
+ width: 100%;
+ padding: 0;
+ margin: 0;
+ list-style-type: none;
+ border-bottom: 6px solid #eee;
+ margin-bottom: 2ex;
+}
+
+#details ul.context-nav li a {
+ float: left;
+ text-decoration: none;
+ background-color: #d6d6d6;
+ padding: 1ex 2ex;
+ text-align: center;
+ margin-right: 3px;
+
+ -moz-border-radius-topleft: 12px;
+ -moz-border-radius-topright: 12px;
+}
+
+#details ul.context-nav li#active a {
+ color: #444;
+ background-color: #eee;
+ font-weight: bold;
+}
+
+/** FORMS SECTION **/
+
+input.text {
+ border: 1px solid #8e8e8e;
+ background-color: #e5f1f4;
+ color: #444444;
+}
+
+input.submitbutton {
+ float: right;
+}
+
+form.tableform table th {
+ padding-right: 2ex;
+ text-align: right;
+}
+
+h2.formsection {
+ color: #999;
+ font-size: large;
+ font-weight: normal;
+
+ border-bottom: 1px solid #999;
+ margin-bottom: 10px;
+ margin-top: 12px;
+
+ text-align: left;
+ width: 100%;
+}
+
+table.formtable {
+ width: 100%;
+}
+
+/**** freeipa only below? ****/
+ul.checkboxlist li {
+ list-style: none;
+ margin: 8px 0px;
+}
+
+ul.checkboxlist li input {
+ background-color: yellow;
+ height: 1.1em;
+ width: 1.2em;
+ border: 1px solid red;
+}
+
+table.formtable th, table.formtable td {
+ vertical-align: top;
+ padding-bottom: 10px;
+}
+
+table.formtable th {
+ width: 28%;
+}
+
+input.submitbutton, input.searchbutton, #source_searcharea input.searchbutton {
+ border: 1px outset #aaa;
+ padding: 2px 1px;
+ margin-bottom: 2px;
+}
+
+table.formtable td input[type="text"], input#criteria {
+ border: 1px inset #dcdcdc;
+ font-size: medium;
+ padding: 2px 1px;
+}
+
+table.formtable td select {
+ border: 1px inset #dcdcdc;
+ font-size: small;
+ padding: 2px 1px;
+}
+
+#inactive {
+ background-color: silver;
+}
+
+/** ALERTS / MESSAGING SECTION **/
+
+#alertbox {
+ width: 100%;
+ padding: 10px 0px;
+ margin-top: 12px;
+ margin-bottom: 18px;
+ vertical-align: middle;
+
+ -moz-border-radius: 6px;
+ background-color: #7d7d5b;
+ color: white;
+}
+
+#alertbox h2 {
+ width: auto;
+ padding: 0px 16px;
+ float: left;
+ font-size: medium;
+ text-transform: uppercase;
+ color: white;
+ font-weight: bold;
+ border: none;
+}
+
+#alertbox p {
+ padding: 0px 16px;
+ text-align: center;
+ color: white;
+ width: auto;
+}
+
+/**** freeipa only alerts/messaging below ****/
+
+p.empty-message {
+ font-size: large;
+ font-style: italic;
+ color: #888 !important;
+}
+
+div.instructions {
+ padding: 2px 6px;
+ margin-top: 16px;
+ border-top: 1px solid #c0d5f1;
+ border-bottom: 1px solid #c0d5f1;
+ background-color: #eef4fd;
+}
+
+
+/** OVERVIEW PAGE STYLES **/
+
+.summary {
+ width: 40%;
+ float: left;
+ clear: none;
+ padding-top: 2ex;
+}
+
+.tasks, .search {
+ padding: 3ex;
+ padding-top: 2ex;
+ width: 40%;
+ float: right;
+ background-image: url('/ipa/ui/static/images/template/background-search.png');
+ background-repeat: repeat-y;
+ background-color: white;
+}
+
+.summary ul, .tasks ul, ul { margin-top: 1ex; padding-top: 1ex; list-style: square; margin-left: 2ex; }
+.summary ul + ul, .tasks ul + ul { border-top: 1px solid #eee; }
+.search ul { list-style: none; margin-left: 2ex; }
+
+.additional-link {
+ font-size: x-small;
+}
+
+#perspectives h3 a {
+ font-size: x-small;
+ text-transform: none;
+ margin-left: 1ex;
+ font-weight: normal;
+}
+
+/** TURBOGEARS GRID-TABLE-SPECIFIC STYLES **/
+
+.grid td, .grid th {padding:3px;border:none;}
+.grid .action_cell {text-align:right;}
+.grid THEAD tr th {text-align:left;background-color:#f0f0f0;color:#333;}
+.grid .heading img {float:right;margin-left:2px;margin-right:3px;}
+.grid .heading a {text-decoration:none;color:#333;}
+.grid td a {text-decoration:none;color:#333;}
+.grid tr.odd td {background-color:#edf3fe;}
+.grid tr.even td {background-color:#fff;}
+.grid .pointer {cursor:pointer;}
+.grid .column_chooser_link {position:relative;background-color:#e3e3e3;}
+.grid .column_chooser_link ul {position:absolute;display:none;top:0px;right:-20px;}
+.grid .column_chooser_list a {width:200px;display:block;padding:3px;background-color:#e3e3e3;}
+.grid .column_chooser_list a:hover {background-color:#cdcdcd;}
+.grid .column_chooser_list {padding:0;margin:0;list-style:none;background-color:#e3e3e3;}
+
+
+
diff --git a/ipa-server/ipa-gui/ipagui/static/images/Makefile.am b/ipa-server/ipa-gui/ipagui/static/images/Makefile.am
new file mode 100644
index 00000000..bb0f3aab
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/static/images/Makefile.am
@@ -0,0 +1,29 @@
+NULL =
+
+SUBDIRS = \
+ branding \
+ objects \
+ template \
+ $(NULL)
+
+appdir = $(IPA_DATA_DIR)/ipagui/static/images
+app_DATA = \
+ down.gif \
+ favicon.ico \
+ header_inner.png \
+ info.png \
+ logo.png \
+ ok.png \
+ tg_under_the_hood.png \
+ under_the_hood_blue.png \
+ up.gif
+ $(NULL)
+
+EXTRA_DIST = \
+ $(app_DATA) \
+ $(NULL)
+
+MAINTAINERCLEANFILES = \
+ *~ \
+ *.pyc \
+ Makefile.in
diff --git a/ipa-server/ipa-gui/ipagui/static/images/branding/Makefile.am b/ipa-server/ipa-gui/ipagui/static/images/branding/Makefile.am
new file mode 100644
index 00000000..cb0a8142
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/static/images/branding/Makefile.am
@@ -0,0 +1,15 @@
+NULL =
+
+appdir = $(IPA_DATA_DIR)/ipagui/static/images/branding
+app_DATA = \
+ logo.png \
+ $(NULL)
+
+EXTRA_DIST = \
+ $(app_DATA) \
+ $(NULL)
+
+MAINTAINERCLEANFILES = \
+ *~ \
+ *.pyc \
+ Makefile.in
diff --git a/ipa-server/ipa-gui/ipagui/static/images/branding/logo.png b/ipa-server/ipa-gui/ipagui/static/images/branding/logo.png
new file mode 100644
index 00000000..ebabed79
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/static/images/branding/logo.png
Binary files differ
diff --git a/ipa-server/ipa-gui/ipagui/static/images/down.gif b/ipa-server/ipa-gui/ipagui/static/images/down.gif
new file mode 100644
index 00000000..c527b4e6
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/static/images/down.gif
Binary files differ
diff --git a/ipa-server/ipa-gui/ipagui/static/images/favicon.ico b/ipa-server/ipa-gui/ipagui/static/images/favicon.ico
new file mode 100644
index 00000000..bafbff92
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/static/images/favicon.ico
Binary files differ
diff --git a/ipa-server/ipa-gui/ipagui/static/images/header_inner.png b/ipa-server/ipa-gui/ipagui/static/images/header_inner.png
new file mode 100644
index 00000000..2b2d87d5
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/static/images/header_inner.png
Binary files differ
diff --git a/ipa-server/ipa-gui/ipagui/static/images/info.png b/ipa-server/ipa-gui/ipagui/static/images/info.png
new file mode 100644
index 00000000..329c523f
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/static/images/info.png
Binary files differ
diff --git a/ipa-server/ipa-gui/ipagui/static/images/logo.png b/ipa-server/ipa-gui/ipagui/static/images/logo.png
new file mode 100644
index 00000000..ebabed79
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/static/images/logo.png
Binary files differ
diff --git a/ipa-server/ipa-gui/ipagui/static/images/objects/Makefile.am b/ipa-server/ipa-gui/ipagui/static/images/objects/Makefile.am
new file mode 100644
index 00000000..02e89883
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/static/images/objects/Makefile.am
@@ -0,0 +1,25 @@
+NULL =
+
+appdir = $(IPA_DATA_DIR)/ipagui/static/images/objects
+app_DATA = \
+ object-accesscontrol.png \
+ object-channel.png \
+ object-channels.png \
+ object-content.png \
+ object-media.png \
+ object-overview.png \
+ object-system.png \
+ object-usergroup.png \
+ object-user.png \
+ object-virtualsystem.png \
+ object-policy.png \
+ $(NULL)
+
+EXTRA_DIST = \
+ $(app_DATA) \
+ $(NULL)
+
+MAINTAINERCLEANFILES = \
+ *~ \
+ *.pyc \
+ Makefile.in
diff --git a/ipa-server/ipa-gui/ipagui/static/images/objects/object-accesscontrol.png b/ipa-server/ipa-gui/ipagui/static/images/objects/object-accesscontrol.png
new file mode 100644
index 00000000..bddec41b
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/static/images/objects/object-accesscontrol.png
Binary files differ
diff --git a/ipa-server/ipa-gui/ipagui/static/images/objects/object-channel.png b/ipa-server/ipa-gui/ipagui/static/images/objects/object-channel.png
new file mode 100644
index 00000000..7fd37c4e
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/static/images/objects/object-channel.png
Binary files differ
diff --git a/ipa-server/ipa-gui/ipagui/static/images/objects/object-channels.png b/ipa-server/ipa-gui/ipagui/static/images/objects/object-channels.png
new file mode 100644
index 00000000..7fd37c4e
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/static/images/objects/object-channels.png
Binary files differ
diff --git a/ipa-server/ipa-gui/ipagui/static/images/objects/object-content.png b/ipa-server/ipa-gui/ipagui/static/images/objects/object-content.png
new file mode 100644
index 00000000..608a19e5
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/static/images/objects/object-content.png
Binary files differ
diff --git a/ipa-server/ipa-gui/ipagui/static/images/objects/object-media.png b/ipa-server/ipa-gui/ipagui/static/images/objects/object-media.png
new file mode 100644
index 00000000..0220fc05
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/static/images/objects/object-media.png
Binary files differ
diff --git a/ipa-server/ipa-gui/ipagui/static/images/objects/object-overview.png b/ipa-server/ipa-gui/ipagui/static/images/objects/object-overview.png
new file mode 100644
index 00000000..a320b9c8
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/static/images/objects/object-overview.png
Binary files differ
diff --git a/ipa-server/ipa-gui/ipagui/static/images/objects/object-policy.png b/ipa-server/ipa-gui/ipagui/static/images/objects/object-policy.png
new file mode 100644
index 00000000..eb55f453
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/static/images/objects/object-policy.png
Binary files differ
diff --git a/ipa-server/ipa-gui/ipagui/static/images/objects/object-system.png b/ipa-server/ipa-gui/ipagui/static/images/objects/object-system.png
new file mode 100644
index 00000000..82b09a5d
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/static/images/objects/object-system.png
Binary files differ
diff --git a/ipa-server/ipa-gui/ipagui/static/images/objects/object-user.png b/ipa-server/ipa-gui/ipagui/static/images/objects/object-user.png
new file mode 100644
index 00000000..bf294efd
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/static/images/objects/object-user.png
Binary files differ
diff --git a/ipa-server/ipa-gui/ipagui/static/images/objects/object-usergroup.png b/ipa-server/ipa-gui/ipagui/static/images/objects/object-usergroup.png
new file mode 100644
index 00000000..7338ad2e
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/static/images/objects/object-usergroup.png
Binary files differ
diff --git a/ipa-server/ipa-gui/ipagui/static/images/objects/object-virtualsystem.png b/ipa-server/ipa-gui/ipagui/static/images/objects/object-virtualsystem.png
new file mode 100644
index 00000000..8f2befca
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/static/images/objects/object-virtualsystem.png
Binary files differ
diff --git a/ipa-server/ipa-gui/ipagui/static/images/ok.png b/ipa-server/ipa-gui/ipagui/static/images/ok.png
new file mode 100644
index 00000000..fee6751c
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/static/images/ok.png
Binary files differ
diff --git a/ipa-server/ipa-gui/ipagui/static/images/template/Makefile.am b/ipa-server/ipa-gui/ipagui/static/images/template/Makefile.am
new file mode 100644
index 00000000..a446ce63
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/static/images/template/Makefile.am
@@ -0,0 +1,21 @@
+NULL =
+
+appdir = $(IPA_DATA_DIR)/ipagui/static/images/template
+app_DATA = \
+ background-content.png \
+ background-navbar-active_fullsize.png \
+ background-navbar-active.png \
+ background-navbar_fullsize.png \
+ background-navbar.png \
+ background.png \
+ background-sidebar.png \
+ $(NULL)
+
+EXTRA_DIST = \
+ $(app_DATA) \
+ $(NULL)
+
+MAINTAINERCLEANFILES = \
+ *~ \
+ *.pyc \
+ Makefile.in
diff --git a/ipa-server/ipa-gui/ipagui/static/images/template/background-content.png b/ipa-server/ipa-gui/ipagui/static/images/template/background-content.png
new file mode 100644
index 00000000..082f10ae
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/static/images/template/background-content.png
Binary files differ
diff --git a/ipa-server/ipa-gui/ipagui/static/images/template/background-navbar-active.png b/ipa-server/ipa-gui/ipagui/static/images/template/background-navbar-active.png
new file mode 100644
index 00000000..1b088501
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/static/images/template/background-navbar-active.png
Binary files differ
diff --git a/ipa-server/ipa-gui/ipagui/static/images/template/background-navbar-active_fullsize.png b/ipa-server/ipa-gui/ipagui/static/images/template/background-navbar-active_fullsize.png
new file mode 100644
index 00000000..756a1e61
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/static/images/template/background-navbar-active_fullsize.png
Binary files differ
diff --git a/ipa-server/ipa-gui/ipagui/static/images/template/background-navbar.png b/ipa-server/ipa-gui/ipagui/static/images/template/background-navbar.png
new file mode 100644
index 00000000..2c6a2de4
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/static/images/template/background-navbar.png
Binary files differ
diff --git a/ipa-server/ipa-gui/ipagui/static/images/template/background-navbar_fullsize.png b/ipa-server/ipa-gui/ipagui/static/images/template/background-navbar_fullsize.png
new file mode 100644
index 00000000..72a71063
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/static/images/template/background-navbar_fullsize.png
Binary files differ
diff --git a/ipa-server/ipa-gui/ipagui/static/images/template/background-sidebar.png b/ipa-server/ipa-gui/ipagui/static/images/template/background-sidebar.png
new file mode 100644
index 00000000..4eaadbbc
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/static/images/template/background-sidebar.png
Binary files differ
diff --git a/ipa-server/ipa-gui/ipagui/static/images/template/background.png b/ipa-server/ipa-gui/ipagui/static/images/template/background.png
new file mode 100644
index 00000000..96ead97f
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/static/images/template/background.png
Binary files differ
diff --git a/ipa-server/ipa-gui/ipagui/static/images/tg_under_the_hood.png b/ipa-server/ipa-gui/ipagui/static/images/tg_under_the_hood.png
new file mode 100644
index 00000000..bc9c79cc
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/static/images/tg_under_the_hood.png
Binary files differ
diff --git a/ipa-server/ipa-gui/ipagui/static/images/under_the_hood_blue.png b/ipa-server/ipa-gui/ipagui/static/images/under_the_hood_blue.png
new file mode 100644
index 00000000..90e84b72
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/static/images/under_the_hood_blue.png
Binary files differ
diff --git a/ipa-server/ipa-gui/ipagui/static/images/up.gif b/ipa-server/ipa-gui/ipagui/static/images/up.gif
new file mode 100644
index 00000000..c5ef548a
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/static/images/up.gif
Binary files differ
diff --git a/ipa-server/ipa-gui/ipagui/static/javascript/Makefile.am b/ipa-server/ipa-gui/ipagui/static/javascript/Makefile.am
new file mode 100644
index 00000000..a2ca2289
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/static/javascript/Makefile.am
@@ -0,0 +1,21 @@
+NULL =
+
+appdir = $(IPA_DATA_DIR)/ipagui/static/javascript
+app_DATA = \
+ dynamicedit.js \
+ dynamicselect.js \
+ effects.js \
+ ipautil.js \
+ prototype.js \
+ scriptaculous.js \
+ tablekit.js \
+ $(NULL)
+
+EXTRA_DIST = \
+ $(app_DATA) \
+ $(NULL)
+
+MAINTAINERCLEANFILES = \
+ *~ \
+ *.pyc \
+ Makefile.in
diff --git a/ipa-server/ipa-gui/ipagui/static/javascript/dynamicedit.js b/ipa-server/ipa-gui/ipagui/static/javascript/dynamicedit.js
new file mode 100644
index 00000000..b670c457
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/static/javascript/dynamicedit.js
@@ -0,0 +1,217 @@
+/**
+ * Copyright (C) 2007 Red Hat
+ * see file 'COPYING' for use and warranty information
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; version 2 only
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ */
+
+/**
+ * dynamicedit.js
+ *
+ * Shared code, data, and functions for the dynamic add/remove lists on the
+ * edit group/user pages.
+ *
+ * These functions have specific expectations of the page they are used on:
+ *
+ * - If you want to preserve the dn_to_info_hash on round trip:
+ * - The form must have a 'form_dn_to_info_json' hidden field.
+ * - The form must have onsubmit="preSubmit()" set in its tag.
+ * - Restoring the contents of add/remove lists on round trip unfortunately
+ * can't be shared because it is a mixture of python and javascript. See
+ * the bottom part editgroup.kid for example code on this.
+ *
+ * - The page must have a div: 'newmembers'
+ * that new members are dynamically added to.
+ *
+ * - The page must have a div: 'delmembers'
+ * that removed members are dynamically added to.
+ *
+ * - Hidden fields called 'dnadd' and 'dndel' will be dynamically created,
+ * holding the values of the 'dn' passed to addmember() and removemember()
+ *
+ * Other Notes:
+ *
+ * - Many of the fields refer to 'dn'. There is no intrinsic reason this has
+ * to be a dn (it can hold any "unique id" for the objects to add/remove)
+ *
+ * - Similarly, the word 'member' is used because the code was originally
+ * written for editgroup. A 'member' is just a 'thing' to add/remove.
+ * On the useredit pages, for example, a 'member' is actually a group.
+ */
+
+// Stored as the values in the dn_to_info_hash
+MemberDisplayInfo = Class.create();
+MemberDisplayInfo.prototype = {
+ initialize: function(name, descr, type) {
+ this.name = name;
+ this.descr = descr;
+ this.type = type;
+ }
+};
+
+
+// this is used for round-trip recontruction of the names.
+// the hidden fields only contain dns.
+var dn_to_info_hash = new Hash();
+
+// used to filter search results.
+// records dns already in the group
+var member_hash = new Hash();
+
+// used to prevent double adding
+// records dns to be added
+var added_hash = new Hash();
+
+// Tracks the div ids that each member belongs to.
+// Since dn's will contain illegal characters for div ids, this is used
+// to map them to the correct div
+var dn_to_member_div_id = new Hash();
+
+
+
+/*
+ * Renders the information about the member into the passed in
+ * element. This is used by addmember and removemember to
+ * consistently create the dom for the member information
+ * (name, descr) and add icons/font changes correct for each type.
+ */
+function renderMemberInfo(newdiv, info) {
+ if (info.type == "user") {
+ bold = document.createElement('b');
+ bold.appendChild(document.createTextNode(
+ info.name + " " + info.descr + " "));
+ newdiv.appendChild(bold);
+ } else if (info.type == "iuser") {
+ newdiv.appendChild(document.createTextNode(
+ info.name + " " + info.descr + " "));
+ } else if (info.type == "group") {
+ ital = document.createElement('i');
+ bold = document.createElement('b');
+ ital.appendChild(bold);
+ bold.appendChild(document.createTextNode(
+ info.name + " " +
+ info.descr + " "));
+ newdiv.appendChild(ital);
+ } else if (info.type == "igroup") {
+ ital = document.createElement('i');
+ ital.appendChild(document.createTextNode(
+ info.name + " " +
+ info.descr + " "));
+ newdiv.appendChild(ital);
+ }
+}
+
+/*
+ * Callback used for afterFinish in scriptaculous effect
+ */
+function removeElement(effect) {
+ Element.remove(effect.element);
+}
+
+function addmember(dn, info) {
+ dn_to_info_hash[dn] = info;
+
+ if ((added_hash[dn] == 1) || (member_hash[dn] == 1)) {
+ return null;
+ }
+ added_hash[dn] = 1;
+
+ var newdiv = document.createElement('div');
+ renderMemberInfo(newdiv, info);
+
+ var undolink = document.createElement('a');
+ undolink.setAttribute('href', '');
+ undolink.setAttribute('onclick',
+ 'new Effect.Fade(Element.up(this), {afterFinish: removeElement, duration: 0.75});' +
+ 'added_hash.remove("' + jsStringEscape(dn) + '");' +
+ 'return false;');
+ undolink.appendChild(document.createTextNode("undo"));
+ newdiv.appendChild(undolink);
+
+ var dnInfo = document.createElement('input');
+ dnInfo.setAttribute('type', 'hidden');
+ dnInfo.setAttribute('name', 'dnadd');
+ dnInfo.setAttribute('value', dn);
+ newdiv.appendChild(dnInfo);
+
+ newdiv.style.display = 'none';
+ $('newmembers').appendChild(newdiv);
+
+ return newdiv;
+}
+
+function addmemberHandler(element, dn, info) {
+ var newdiv = addmember(dn, info);
+ if (newdiv != null) {
+ new Effect.Fade(Element.up(element), {duration: 0.75});
+ new Effect.Appear(newdiv, {duration: 0.75});
+ /* Element.up(element).remove(); */
+ }
+}
+
+function removemember(dn, info) {
+ dn_to_info_hash[dn] = info;
+
+ var newdiv = document.createElement('div');
+ renderMemberInfo(newdiv, info);
+
+ orig_div_id = dn_to_member_div_id[dn];
+ var undolink = document.createElement('a');
+ undolink.setAttribute('href', '');
+ undolink.setAttribute('onclick',
+ 'new Effect.Fade(Element.up(this), {afterFinish: removeElement, duration: 0.75});' +
+ "new Effect.Appear($('" + orig_div_id + "'), {duration: 0.75});" +
+ 'return false;');
+ undolink.appendChild(document.createTextNode("undo"));
+ newdiv.appendChild(undolink);
+
+ var dnInfo = document.createElement('input');
+ dnInfo.setAttribute('type', 'hidden');
+ dnInfo.setAttribute('name', 'dndel');
+ dnInfo.setAttribute('value', dn);
+ newdiv.appendChild(dnInfo);
+
+ newdiv.style.display = 'none';
+ $('delmembers').appendChild(newdiv);
+
+ return newdiv;
+}
+
+function removememberHandler(element, dn, info) {
+ var newdiv = removemember(dn, info);
+ new Effect.Fade(Element.up(element), {duration: 0.75});
+ new Effect.Appear(newdiv, {duration: 0.75});
+ /* Element.up(element).remove(); */
+}
+
+function preSubmit() {
+ var json = dn_to_info_hash.toJSON();
+ $('form_dn_to_info_json').value = json;
+ return true;
+}
+
+function enterDoSearch(e) {
+ var keyPressed;
+ if (window.event) {
+ keyPressed = window.event.keyCode;
+ } else {
+ keyPressed = e.which;
+ }
+
+ if (keyPressed == 13) {
+ return doSearch();
+ } else {
+ return true;
+ }
+}
diff --git a/ipa-server/ipa-gui/ipagui/static/javascript/dynamicselect.js b/ipa-server/ipa-gui/ipagui/static/javascript/dynamicselect.js
new file mode 100644
index 00000000..17fdf8fe
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/static/javascript/dynamicselect.js
@@ -0,0 +1,70 @@
+/* Copyright (C) 2007 Red Hat
+ * see file 'COPYING' for use and warranty information
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; version 2 only
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ */
+
+/**
+ * dynamicselect.js
+ *
+ * Shared code, data, and functions for the dynamic select lists on the
+ * edit user pages.
+ *
+ */
+
+function enterDoSelectSearch(e, which_select) {
+ var keyPressed;
+ if (window.event) {
+ keyPressed = window.event.keyCode;
+ } else {
+ keyPressed = e.which;
+ }
+
+ if (keyPressed == 13) {
+ return doSelectSearch(which_select);
+ } else {
+ return true;
+ }
+}
+
+function startSelect(which_select) {
+ new Effect.Appear($(which_select + '_searcharea'), {duration: 0.25});
+ new Effect.Fade($(which_select + '_links'), {duration: 0.25});
+ return false;
+}
+
+function doSelect(which_select, select_dn, select_cn) {
+ select_dn_field = $('form_' + which_select);
+ select_cn_field = $('form_' + which_select + '_cn');
+ select_cn_span = $(which_select + '_select_cn');
+
+ select_dn_field.value = select_dn;
+ select_cn_field.value = select_cn;
+ select_cn_span.update(select_cn);
+
+ new Effect.Fade($(which_select + '_searcharea'), {duration: 0.25});
+ new Effect.Appear($(which_select + '_links'), {duration: 0.25});
+}
+
+function clearSelect(which_select) {
+ select_dn_field = $('form_' + which_select);
+ select_cn_field = $('form_' + which_select + '_cn');
+ select_cn_span = $(which_select + '_select_cn');
+
+ select_dn_field.value = '';
+ select_cn_field.value = '';
+ select_cn_span.update('');
+
+ return false;
+}
diff --git a/ipa-server/ipa-gui/ipagui/static/javascript/effects.js b/ipa-server/ipa-gui/ipagui/static/javascript/effects.js
new file mode 100644
index 00000000..70d07526
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/static/javascript/effects.js
@@ -0,0 +1,1094 @@
+// script.aculo.us effects.js v1.7.1_beta3, Fri May 25 17:19:41 +0200 2007
+
+// Copyright (c) 2005-2007 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
+// Contributors:
+// Justin Palmer (http://encytemedia.com/)
+// Mark Pilgrim (http://diveintomark.org/)
+// Martin Bialasinki
+//
+// script.aculo.us is freely distributable under the terms of an MIT-style license.
+// For details, see the script.aculo.us web site: http://script.aculo.us/
+
+// converts rgb() and #xxx to #xxxxxx format,
+// returns self (or first argument) if not convertable
+String.prototype.parseColor = function() {
+ var color = '#';
+ if(this.slice(0,4) == 'rgb(') {
+ var cols = this.slice(4,this.length-1).split(',');
+ var i=0; do { color += parseInt(cols[i]).toColorPart() } while (++i<3);
+ } else {
+ if(this.slice(0,1) == '#') {
+ if(this.length==4) for(var i=1;i<4;i++) color += (this.charAt(i) + this.charAt(i)).toLowerCase();
+ if(this.length==7) color = this.toLowerCase();
+ }
+ }
+ return(color.length==7 ? color : (arguments[0] || this));
+}
+
+/*--------------------------------------------------------------------------*/
+
+Element.collectTextNodes = function(element) {
+ return $A($(element).childNodes).collect( function(node) {
+ return (node.nodeType==3 ? node.nodeValue :
+ (node.hasChildNodes() ? Element.collectTextNodes(node) : ''));
+ }).flatten().join('');
+}
+
+Element.collectTextNodesIgnoreClass = function(element, className) {
+ return $A($(element).childNodes).collect( function(node) {
+ return (node.nodeType==3 ? node.nodeValue :
+ ((node.hasChildNodes() && !Element.hasClassName(node,className)) ?
+ Element.collectTextNodesIgnoreClass(node, className) : ''));
+ }).flatten().join('');
+}
+
+Element.setContentZoom = function(element, percent) {
+ element = $(element);
+ element.setStyle({fontSize: (percent/100) + 'em'});
+ if(Prototype.Browser.WebKit) window.scrollBy(0,0);
+ return element;
+}
+
+Element.getInlineOpacity = function(element){
+ return $(element).style.opacity || '';
+}
+
+Element.forceRerendering = function(element) {
+ try {
+ element = $(element);
+ var n = document.createTextNode(' ');
+ element.appendChild(n);
+ element.removeChild(n);
+ } catch(e) { }
+};
+
+/*--------------------------------------------------------------------------*/
+
+Array.prototype.call = function() {
+ var args = arguments;
+ this.each(function(f){ f.apply(this, args) });
+}
+
+/*--------------------------------------------------------------------------*/
+
+var Effect = {
+ _elementDoesNotExistError: {
+ name: 'ElementDoesNotExistError',
+ message: 'The specified DOM element does not exist, but is required for this effect to operate'
+ },
+ tagifyText: function(element) {
+ if(typeof Builder == 'undefined')
+ throw("Effect.tagifyText requires including script.aculo.us' builder.js library");
+
+ var tagifyStyle = 'position:relative';
+ if(Prototype.Browser.IE) tagifyStyle += ';zoom:1';
+
+ element = $(element);
+ $A(element.childNodes).each( function(child) {
+ if(child.nodeType==3) {
+ child.nodeValue.toArray().each( function(character) {
+ element.insertBefore(
+ Builder.node('span',{style: tagifyStyle},
+ character == ' ' ? String.fromCharCode(160) : character),
+ child);
+ });
+ Element.remove(child);
+ }
+ });
+ },
+ multiple: function(element, effect) {
+ var elements;
+ if(((typeof element == 'object') ||
+ (typeof element == 'function')) &&
+ (element.length))
+ elements = element;
+ else
+ elements = $(element).childNodes;
+
+ var options = Object.extend({
+ speed: 0.1,
+ delay: 0.0
+ }, arguments[2] || {});
+ var masterDelay = options.delay;
+
+ $A(elements).each( function(element, index) {
+ new effect(element, Object.extend(options, { delay: index * options.speed + masterDelay }));
+ });
+ },
+ PAIRS: {
+ 'slide': ['SlideDown','SlideUp'],
+ 'blind': ['BlindDown','BlindUp'],
+ 'appear': ['Appear','Fade']
+ },
+ toggle: function(element, effect) {
+ element = $(element);
+ effect = (effect || 'appear').toLowerCase();
+ var options = Object.extend({
+ queue: { position:'end', scope:(element.id || 'global'), limit: 1 }
+ }, arguments[2] || {});
+ Effect[element.visible() ?
+ Effect.PAIRS[effect][1] : Effect.PAIRS[effect][0]](element, options);
+ }
+};
+
+var Effect2 = Effect; // deprecated
+
+/* ------------- transitions ------------- */
+
+Effect.Transitions = {
+ linear: Prototype.K,
+ sinoidal: function(pos) {
+ return (-Math.cos(pos*Math.PI)/2) + 0.5;
+ },
+ reverse: function(pos) {
+ return 1-pos;
+ },
+ flicker: function(pos) {
+ var pos = ((-Math.cos(pos*Math.PI)/4) + 0.75) + Math.random()/4;
+ return (pos > 1 ? 1 : pos);
+ },
+ wobble: function(pos) {
+ return (-Math.cos(pos*Math.PI*(9*pos))/2) + 0.5;
+ },
+ pulse: function(pos, pulses) {
+ pulses = pulses || 5;
+ return (
+ Math.round((pos % (1/pulses)) * pulses) == 0 ?
+ ((pos * pulses * 2) - Math.floor(pos * pulses * 2)) :
+ 1 - ((pos * pulses * 2) - Math.floor(pos * pulses * 2))
+ );
+ },
+ none: function(pos) {
+ return 0;
+ },
+ full: function(pos) {
+ return 1;
+ }
+};
+
+/* ------------- core effects ------------- */
+
+Effect.ScopedQueue = Class.create();
+Object.extend(Object.extend(Effect.ScopedQueue.prototype, Enumerable), {
+ initialize: function() {
+ this.effects = [];
+ this.interval = null;
+ },
+ _each: function(iterator) {
+ this.effects._each(iterator);
+ },
+ add: function(effect) {
+ var timestamp = new Date().getTime();
+
+ var position = (typeof effect.options.queue == 'string') ?
+ effect.options.queue : effect.options.queue.position;
+
+ switch(position) {
+ case 'front':
+ // move unstarted effects after this effect
+ this.effects.findAll(function(e){ return e.state=='idle' }).each( function(e) {
+ e.startOn += effect.finishOn;
+ e.finishOn += effect.finishOn;
+ });
+ break;
+ case 'with-last':
+ timestamp = this.effects.pluck('startOn').max() || timestamp;
+ break;
+ case 'end':
+ // start effect after last queued effect has finished
+ timestamp = this.effects.pluck('finishOn').max() || timestamp;
+ break;
+ }
+
+ effect.startOn += timestamp;
+ effect.finishOn += timestamp;
+
+ if(!effect.options.queue.limit || (this.effects.length < effect.options.queue.limit))
+ this.effects.push(effect);
+
+ if(!this.interval)
+ this.interval = setInterval(this.loop.bind(this), 15);
+ },
+ remove: function(effect) {
+ this.effects = this.effects.reject(function(e) { return e==effect });
+ if(this.effects.length == 0) {
+ clearInterval(this.interval);
+ this.interval = null;
+ }
+ },
+ loop: function() {
+ var timePos = new Date().getTime();
+ for(var i=0, len=this.effects.length;i<len;i++)
+ this.effects[i] && this.effects[i].loop(timePos);
+ }
+});
+
+Effect.Queues = {
+ instances: $H(),
+ get: function(queueName) {
+ if(typeof queueName != 'string') return queueName;
+
+ if(!this.instances[queueName])
+ this.instances[queueName] = new Effect.ScopedQueue();
+
+ return this.instances[queueName];
+ }
+}
+Effect.Queue = Effect.Queues.get('global');
+
+Effect.DefaultOptions = {
+ transition: Effect.Transitions.sinoidal,
+ duration: 1.0, // seconds
+ fps: 100, // 100= assume 66fps max.
+ sync: false, // true for combining
+ from: 0.0,
+ to: 1.0,
+ delay: 0.0,
+ queue: 'parallel'
+}
+
+Effect.Base = function() {};
+Effect.Base.prototype = {
+ position: null,
+ start: function(options) {
+ function codeForEvent(options,eventName){
+ return (
+ (options[eventName+'Internal'] ? 'this.options.'+eventName+'Internal(this);' : '') +
+ (options[eventName] ? 'this.options.'+eventName+'(this);' : '')
+ );
+ }
+ if(options.transition === false) options.transition = Effect.Transitions.linear;
+ this.options = Object.extend(Object.extend({},Effect.DefaultOptions), options || {});
+ this.currentFrame = 0;
+ this.state = 'idle';
+ this.startOn = this.options.delay*1000;
+ this.finishOn = this.startOn+(this.options.duration*1000);
+ this.fromToDelta = this.options.to-this.options.from;
+ this.totalTime = this.finishOn-this.startOn;
+ this.totalFrames = this.options.fps*this.options.duration;
+
+ eval('this.render = function(pos){ '+
+ 'if(this.state=="idle"){this.state="running";'+
+ codeForEvent(options,'beforeSetup')+
+ (this.setup ? 'this.setup();':'')+
+ codeForEvent(options,'afterSetup')+
+ '};if(this.state=="running"){'+
+ 'pos=this.options.transition(pos)*'+this.fromToDelta+'+'+this.options.from+';'+
+ 'this.position=pos;'+
+ codeForEvent(options,'beforeUpdate')+
+ (this.update ? 'this.update(pos);':'')+
+ codeForEvent(options,'afterUpdate')+
+ '}}');
+
+ this.event('beforeStart');
+ if(!this.options.sync)
+ Effect.Queues.get(typeof this.options.queue == 'string' ?
+ 'global' : this.options.queue.scope).add(this);
+ },
+ loop: function(timePos) {
+ if(timePos >= this.startOn) {
+ if(timePos >= this.finishOn) {
+ this.render(1.0);
+ this.cancel();
+ this.event('beforeFinish');
+ if(this.finish) this.finish();
+ this.event('afterFinish');
+ return;
+ }
+ var pos = (timePos - this.startOn) / this.totalTime,
+ frame = Math.round(pos * this.totalFrames);
+ if(frame > this.currentFrame) {
+ this.render(pos);
+ this.currentFrame = frame;
+ }
+ }
+ },
+ cancel: function() {
+ if(!this.options.sync)
+ Effect.Queues.get(typeof this.options.queue == 'string' ?
+ 'global' : this.options.queue.scope).remove(this);
+ this.state = 'finished';
+ },
+ event: function(eventName) {
+ if(this.options[eventName + 'Internal']) this.options[eventName + 'Internal'](this);
+ if(this.options[eventName]) this.options[eventName](this);
+ },
+ inspect: function() {
+ var data = $H();
+ for(property in this)
+ if(typeof this[property] != 'function') data[property] = this[property];
+ return '#<Effect:' + data.inspect() + ',options:' + $H(this.options).inspect() + '>';
+ }
+}
+
+Effect.Parallel = Class.create();
+Object.extend(Object.extend(Effect.Parallel.prototype, Effect.Base.prototype), {
+ initialize: function(effects) {
+ this.effects = effects || [];
+ this.start(arguments[1]);
+ },
+ update: function(position) {
+ this.effects.invoke('render', position);
+ },
+ finish: function(position) {
+ this.effects.each( function(effect) {
+ effect.render(1.0);
+ effect.cancel();
+ effect.event('beforeFinish');
+ if(effect.finish) effect.finish(position);
+ effect.event('afterFinish');
+ });
+ }
+});
+
+Effect.Event = Class.create();
+Object.extend(Object.extend(Effect.Event.prototype, Effect.Base.prototype), {
+ initialize: function() {
+ var options = Object.extend({
+ duration: 0
+ }, arguments[0] || {});
+ this.start(options);
+ },
+ update: Prototype.emptyFunction
+});
+
+Effect.Opacity = Class.create();
+Object.extend(Object.extend(Effect.Opacity.prototype, Effect.Base.prototype), {
+ initialize: function(element) {
+ this.element = $(element);
+ if(!this.element) throw(Effect._elementDoesNotExistError);
+ // make this work on IE on elements without 'layout'
+ if(Prototype.Browser.IE && (!this.element.currentStyle.hasLayout))
+ this.element.setStyle({zoom: 1});
+ var options = Object.extend({
+ from: this.element.getOpacity() || 0.0,
+ to: 1.0
+ }, arguments[1] || {});
+ this.start(options);
+ },
+ update: function(position) {
+ this.element.setOpacity(position);
+ }
+});
+
+Effect.Move = Class.create();
+Object.extend(Object.extend(Effect.Move.prototype, Effect.Base.prototype), {
+ initialize: function(element) {
+ this.element = $(element);
+ if(!this.element) throw(Effect._elementDoesNotExistError);
+ var options = Object.extend({
+ x: 0,
+ y: 0,
+ mode: 'relative'
+ }, arguments[1] || {});
+ this.start(options);
+ },
+ setup: function() {
+ // Bug in Opera: Opera returns the "real" position of a static element or
+ // relative element that does not have top/left explicitly set.
+ // ==> Always set top and left for position relative elements in your stylesheets
+ // (to 0 if you do not need them)
+ this.element.makePositioned();
+ this.originalLeft = parseFloat(this.element.getStyle('left') || '0');
+ this.originalTop = parseFloat(this.element.getStyle('top') || '0');
+ if(this.options.mode == 'absolute') {
+ // absolute movement, so we need to calc deltaX and deltaY
+ this.options.x = this.options.x - this.originalLeft;
+ this.options.y = this.options.y - this.originalTop;
+ }
+ },
+ update: function(position) {
+ this.element.setStyle({
+ left: Math.round(this.options.x * position + this.originalLeft) + 'px',
+ top: Math.round(this.options.y * position + this.originalTop) + 'px'
+ });
+ }
+});
+
+// for backwards compatibility
+Effect.MoveBy = function(element, toTop, toLeft) {
+ return new Effect.Move(element,
+ Object.extend({ x: toLeft, y: toTop }, arguments[3] || {}));
+};
+
+Effect.Scale = Class.create();
+Object.extend(Object.extend(Effect.Scale.prototype, Effect.Base.prototype), {
+ initialize: function(element, percent) {
+ this.element = $(element);
+ if(!this.element) throw(Effect._elementDoesNotExistError);
+ var options = Object.extend({
+ scaleX: true,
+ scaleY: true,
+ scaleContent: true,
+ scaleFromCenter: false,
+ scaleMode: 'box', // 'box' or 'contents' or {} with provided values
+ scaleFrom: 100.0,
+ scaleTo: percent
+ }, arguments[2] || {});
+ this.start(options);
+ },
+ setup: function() {
+ this.restoreAfterFinish = this.options.restoreAfterFinish || false;
+ this.elementPositioning = this.element.getStyle('position');
+
+ this.originalStyle = {};
+ ['top','left','width','height','fontSize'].each( function(k) {
+ this.originalStyle[k] = this.element.style[k];
+ }.bind(this));
+
+ this.originalTop = this.element.offsetTop;
+ this.originalLeft = this.element.offsetLeft;
+
+ var fontSize = this.element.getStyle('font-size') || '100%';
+ ['em','px','%','pt'].each( function(fontSizeType) {
+ if(fontSize.indexOf(fontSizeType)>0) {
+ this.fontSize = parseFloat(fontSize);
+ this.fontSizeType = fontSizeType;
+ }
+ }.bind(this));
+
+ this.factor = (this.options.scaleTo - this.options.scaleFrom)/100;
+
+ this.dims = null;
+ if(this.options.scaleMode=='box')
+ this.dims = [this.element.offsetHeight, this.element.offsetWidth];
+ if(/^content/.test(this.options.scaleMode))
+ this.dims = [this.element.scrollHeight, this.element.scrollWidth];
+ if(!this.dims)
+ this.dims = [this.options.scaleMode.originalHeight,
+ this.options.scaleMode.originalWidth];
+ },
+ update: function(position) {
+ var currentScale = (this.options.scaleFrom/100.0) + (this.factor * position);
+ if(this.options.scaleContent && this.fontSize)
+ this.element.setStyle({fontSize: this.fontSize * currentScale + this.fontSizeType });
+ this.setDimensions(this.dims[0] * currentScale, this.dims[1] * currentScale);
+ },
+ finish: function(position) {
+ if(this.restoreAfterFinish) this.element.setStyle(this.originalStyle);
+ },
+ setDimensions: function(height, width) {
+ var d = {};
+ if(this.options.scaleX) d.width = Math.round(width) + 'px';
+ if(this.options.scaleY) d.height = Math.round(height) + 'px';
+ if(this.options.scaleFromCenter) {
+ var topd = (height - this.dims[0])/2;
+ var leftd = (width - this.dims[1])/2;
+ if(this.elementPositioning == 'absolute') {
+ if(this.options.scaleY) d.top = this.originalTop-topd + 'px';
+ if(this.options.scaleX) d.left = this.originalLeft-leftd + 'px';
+ } else {
+ if(this.options.scaleY) d.top = -topd + 'px';
+ if(this.options.scaleX) d.left = -leftd + 'px';
+ }
+ }
+ this.element.setStyle(d);
+ }
+});
+
+Effect.Highlight = Class.create();
+Object.extend(Object.extend(Effect.Highlight.prototype, Effect.Base.prototype), {
+ initialize: function(element) {
+ this.element = $(element);
+ if(!this.element) throw(Effect._elementDoesNotExistError);
+ var options = Object.extend({ startcolor: '#ffff99' }, arguments[1] || {});
+ this.start(options);
+ },
+ setup: function() {
+ // Prevent executing on elements not in the layout flow
+ if(this.element.getStyle('display')=='none') { this.cancel(); return; }
+ // Disable background image during the effect
+ this.oldStyle = {};
+ if (!this.options.keepBackgroundImage) {
+ this.oldStyle.backgroundImage = this.element.getStyle('background-image');
+ this.element.setStyle({backgroundImage: 'none'});
+ }
+ if(!this.options.endcolor)
+ this.options.endcolor = this.element.getStyle('background-color').parseColor('#ffffff');
+ if(!this.options.restorecolor)
+ this.options.restorecolor = this.element.getStyle('background-color');
+ // init color calculations
+ this._base = $R(0,2).map(function(i){ return parseInt(this.options.startcolor.slice(i*2+1,i*2+3),16) }.bind(this));
+ this._delta = $R(0,2).map(function(i){ return parseInt(this.options.endcolor.slice(i*2+1,i*2+3),16)-this._base[i] }.bind(this));
+ },
+ update: function(position) {
+ this.element.setStyle({backgroundColor: $R(0,2).inject('#',function(m,v,i){
+ return m+(Math.round(this._base[i]+(this._delta[i]*position)).toColorPart()); }.bind(this)) });
+ },
+ finish: function() {
+ this.element.setStyle(Object.extend(this.oldStyle, {
+ backgroundColor: this.options.restorecolor
+ }));
+ }
+});
+
+Effect.ScrollTo = Class.create();
+Object.extend(Object.extend(Effect.ScrollTo.prototype, Effect.Base.prototype), {
+ initialize: function(element) {
+ this.element = $(element);
+ this.start(arguments[1] || {});
+ },
+ setup: function() {
+ Position.prepare();
+ var offsets = Position.cumulativeOffset(this.element);
+ if(this.options.offset) offsets[1] += this.options.offset;
+ var max = window.innerHeight ?
+ window.height - window.innerHeight :
+ document.body.scrollHeight -
+ (document.documentElement.clientHeight ?
+ document.documentElement.clientHeight : document.body.clientHeight);
+ this.scrollStart = Position.deltaY;
+ this.delta = (offsets[1] > max ? max : offsets[1]) - this.scrollStart;
+ },
+ update: function(position) {
+ Position.prepare();
+ window.scrollTo(Position.deltaX,
+ this.scrollStart + (position*this.delta));
+ }
+});
+
+/* ------------- combination effects ------------- */
+
+Effect.Fade = function(element) {
+ element = $(element);
+ var oldOpacity = element.getInlineOpacity();
+ var options = Object.extend({
+ from: element.getOpacity() || 1.0,
+ to: 0.0,
+ afterFinishInternal: function(effect) {
+ if(effect.options.to!=0) return;
+ effect.element.hide().setStyle({opacity: oldOpacity});
+ }}, arguments[1] || {});
+ return new Effect.Opacity(element,options);
+}
+
+Effect.Appear = function(element) {
+ element = $(element);
+ var options = Object.extend({
+ from: (element.getStyle('display') == 'none' ? 0.0 : element.getOpacity() || 0.0),
+ to: 1.0,
+ // force Safari to render floated elements properly
+ afterFinishInternal: function(effect) {
+ effect.element.forceRerendering();
+ },
+ beforeSetup: function(effect) {
+ effect.element.setOpacity(effect.options.from).show();
+ }}, arguments[1] || {});
+ return new Effect.Opacity(element,options);
+}
+
+Effect.Puff = function(element) {
+ element = $(element);
+ var oldStyle = {
+ opacity: element.getInlineOpacity(),
+ position: element.getStyle('position'),
+ top: element.style.top,
+ left: element.style.left,
+ width: element.style.width,
+ height: element.style.height
+ };
+ return new Effect.Parallel(
+ [ new Effect.Scale(element, 200,
+ { sync: true, scaleFromCenter: true, scaleContent: true, restoreAfterFinish: true }),
+ new Effect.Opacity(element, { sync: true, to: 0.0 } ) ],
+ Object.extend({ duration: 1.0,
+ beforeSetupInternal: function(effect) {
+ Position.absolutize(effect.effects[0].element)
+ },
+ afterFinishInternal: function(effect) {
+ effect.effects[0].element.hide().setStyle(oldStyle); }
+ }, arguments[1] || {})
+ );
+}
+
+Effect.BlindUp = function(element) {
+ element = $(element);
+ element.makeClipping();
+ return new Effect.Scale(element, 0,
+ Object.extend({ scaleContent: false,
+ scaleX: false,
+ restoreAfterFinish: true,
+ afterFinishInternal: function(effect) {
+ effect.element.hide().undoClipping();
+ }
+ }, arguments[1] || {})
+ );
+}
+
+Effect.BlindDown = function(element) {
+ element = $(element);
+ var elementDimensions = element.getDimensions();
+ return new Effect.Scale(element, 100, Object.extend({
+ scaleContent: false,
+ scaleX: false,
+ scaleFrom: 0,
+ scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width},
+ restoreAfterFinish: true,
+ afterSetup: function(effect) {
+ effect.element.makeClipping().setStyle({height: '0px'}).show();
+ },
+ afterFinishInternal: function(effect) {
+ effect.element.undoClipping();
+ }
+ }, arguments[1] || {}));
+}
+
+Effect.SwitchOff = function(element) {
+ element = $(element);
+ var oldOpacity = element.getInlineOpacity();
+ return new Effect.Appear(element, Object.extend({
+ duration: 0.4,
+ from: 0,
+ transition: Effect.Transitions.flicker,
+ afterFinishInternal: function(effect) {
+ new Effect.Scale(effect.element, 1, {
+ duration: 0.3, scaleFromCenter: true,
+ scaleX: false, scaleContent: false, restoreAfterFinish: true,
+ beforeSetup: function(effect) {
+ effect.element.makePositioned().makeClipping();
+ },
+ afterFinishInternal: function(effect) {
+ effect.element.hide().undoClipping().undoPositioned().setStyle({opacity: oldOpacity});
+ }
+ })
+ }
+ }, arguments[1] || {}));
+}
+
+Effect.DropOut = function(element) {
+ element = $(element);
+ var oldStyle = {
+ top: element.getStyle('top'),
+ left: element.getStyle('left'),
+ opacity: element.getInlineOpacity() };
+ return new Effect.Parallel(
+ [ new Effect.Move(element, {x: 0, y: 100, sync: true }),
+ new Effect.Opacity(element, { sync: true, to: 0.0 }) ],
+ Object.extend(
+ { duration: 0.5,
+ beforeSetup: function(effect) {
+ effect.effects[0].element.makePositioned();
+ },
+ afterFinishInternal: function(effect) {
+ effect.effects[0].element.hide().undoPositioned().setStyle(oldStyle);
+ }
+ }, arguments[1] || {}));
+}
+
+Effect.Shake = function(element) {
+ element = $(element);
+ var oldStyle = {
+ top: element.getStyle('top'),
+ left: element.getStyle('left') };
+ return new Effect.Move(element,
+ { x: 20, y: 0, duration: 0.05, afterFinishInternal: function(effect) {
+ new Effect.Move(effect.element,
+ { x: -40, y: 0, duration: 0.1, afterFinishInternal: function(effect) {
+ new Effect.Move(effect.element,
+ { x: 40, y: 0, duration: 0.1, afterFinishInternal: function(effect) {
+ new Effect.Move(effect.element,
+ { x: -40, y: 0, duration: 0.1, afterFinishInternal: function(effect) {
+ new Effect.Move(effect.element,
+ { x: 40, y: 0, duration: 0.1, afterFinishInternal: function(effect) {
+ new Effect.Move(effect.element,
+ { x: -20, y: 0, duration: 0.05, afterFinishInternal: function(effect) {
+ effect.element.undoPositioned().setStyle(oldStyle);
+ }}) }}) }}) }}) }}) }});
+}
+
+Effect.SlideDown = function(element) {
+ element = $(element).cleanWhitespace();
+ // SlideDown need to have the content of the element wrapped in a container element with fixed height!
+ var oldInnerBottom = element.down().getStyle('bottom');
+ var elementDimensions = element.getDimensions();
+ return new Effect.Scale(element, 100, Object.extend({
+ scaleContent: false,
+ scaleX: false,
+ scaleFrom: window.opera ? 0 : 1,
+ scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width},
+ restoreAfterFinish: true,
+ afterSetup: function(effect) {
+ effect.element.makePositioned();
+ effect.element.down().makePositioned();
+ if(window.opera) effect.element.setStyle({top: ''});
+ effect.element.makeClipping().setStyle({height: '0px'}).show();
+ },
+ afterUpdateInternal: function(effect) {
+ effect.element.down().setStyle({bottom:
+ (effect.dims[0] - effect.element.clientHeight) + 'px' });
+ },
+ afterFinishInternal: function(effect) {
+ effect.element.undoClipping().undoPositioned();
+ effect.element.down().undoPositioned().setStyle({bottom: oldInnerBottom}); }
+ }, arguments[1] || {})
+ );
+}
+
+Effect.SlideUp = function(element) {
+ element = $(element).cleanWhitespace();
+ var oldInnerBottom = element.down().getStyle('bottom');
+ return new Effect.Scale(element, window.opera ? 0 : 1,
+ Object.extend({ scaleContent: false,
+ scaleX: false,
+ scaleMode: 'box',
+ scaleFrom: 100,
+ restoreAfterFinish: true,
+ beforeStartInternal: function(effect) {
+ effect.element.makePositioned();
+ effect.element.down().makePositioned();
+ if(window.opera) effect.element.setStyle({top: ''});
+ effect.element.makeClipping().show();
+ },
+ afterUpdateInternal: function(effect) {
+ effect.element.down().setStyle({bottom:
+ (effect.dims[0] - effect.element.clientHeight) + 'px' });
+ },
+ afterFinishInternal: function(effect) {
+ effect.element.hide().undoClipping().undoPositioned().setStyle({bottom: oldInnerBottom});
+ effect.element.down().undoPositioned();
+ }
+ }, arguments[1] || {})
+ );
+}
+
+// Bug in opera makes the TD containing this element expand for a instance after finish
+Effect.Squish = function(element) {
+ return new Effect.Scale(element, window.opera ? 1 : 0, {
+ restoreAfterFinish: true,
+ beforeSetup: function(effect) {
+ effect.element.makeClipping();
+ },
+ afterFinishInternal: function(effect) {
+ effect.element.hide().undoClipping();
+ }
+ });
+}
+
+Effect.Grow = function(element) {
+ element = $(element);
+ var options = Object.extend({
+ direction: 'center',
+ moveTransition: Effect.Transitions.sinoidal,
+ scaleTransition: Effect.Transitions.sinoidal,
+ opacityTransition: Effect.Transitions.full
+ }, arguments[1] || {});
+ var oldStyle = {
+ top: element.style.top,
+ left: element.style.left,
+ height: element.style.height,
+ width: element.style.width,
+ opacity: element.getInlineOpacity() };
+
+ var dims = element.getDimensions();
+ var initialMoveX, initialMoveY;
+ var moveX, moveY;
+
+ switch (options.direction) {
+ case 'top-left':
+ initialMoveX = initialMoveY = moveX = moveY = 0;
+ break;
+ case 'top-right':
+ initialMoveX = dims.width;
+ initialMoveY = moveY = 0;
+ moveX = -dims.width;
+ break;
+ case 'bottom-left':
+ initialMoveX = moveX = 0;
+ initialMoveY = dims.height;
+ moveY = -dims.height;
+ break;
+ case 'bottom-right':
+ initialMoveX = dims.width;
+ initialMoveY = dims.height;
+ moveX = -dims.width;
+ moveY = -dims.height;
+ break;
+ case 'center':
+ initialMoveX = dims.width / 2;
+ initialMoveY = dims.height / 2;
+ moveX = -dims.width / 2;
+ moveY = -dims.height / 2;
+ break;
+ }
+
+ return new Effect.Move(element, {
+ x: initialMoveX,
+ y: initialMoveY,
+ duration: 0.01,
+ beforeSetup: function(effect) {
+ effect.element.hide().makeClipping().makePositioned();
+ },
+ afterFinishInternal: function(effect) {
+ new Effect.Parallel(
+ [ new Effect.Opacity(effect.element, { sync: true, to: 1.0, from: 0.0, transition: options.opacityTransition }),
+ new Effect.Move(effect.element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition }),
+ new Effect.Scale(effect.element, 100, {
+ scaleMode: { originalHeight: dims.height, originalWidth: dims.width },
+ sync: true, scaleFrom: window.opera ? 1 : 0, transition: options.scaleTransition, restoreAfterFinish: true})
+ ], Object.extend({
+ beforeSetup: function(effect) {
+ effect.effects[0].element.setStyle({height: '0px'}).show();
+ },
+ afterFinishInternal: function(effect) {
+ effect.effects[0].element.undoClipping().undoPositioned().setStyle(oldStyle);
+ }
+ }, options)
+ )
+ }
+ });
+}
+
+Effect.Shrink = function(element) {
+ element = $(element);
+ var options = Object.extend({
+ direction: 'center',
+ moveTransition: Effect.Transitions.sinoidal,
+ scaleTransition: Effect.Transitions.sinoidal,
+ opacityTransition: Effect.Transitions.none
+ }, arguments[1] || {});
+ var oldStyle = {
+ top: element.style.top,
+ left: element.style.left,
+ height: element.style.height,
+ width: element.style.width,
+ opacity: element.getInlineOpacity() };
+
+ var dims = element.getDimensions();
+ var moveX, moveY;
+
+ switch (options.direction) {
+ case 'top-left':
+ moveX = moveY = 0;
+ break;
+ case 'top-right':
+ moveX = dims.width;
+ moveY = 0;
+ break;
+ case 'bottom-left':
+ moveX = 0;
+ moveY = dims.height;
+ break;
+ case 'bottom-right':
+ moveX = dims.width;
+ moveY = dims.height;
+ break;
+ case 'center':
+ moveX = dims.width / 2;
+ moveY = dims.height / 2;
+ break;
+ }
+
+ return new Effect.Parallel(
+ [ new Effect.Opacity(element, { sync: true, to: 0.0, from: 1.0, transition: options.opacityTransition }),
+ new Effect.Scale(element, window.opera ? 1 : 0, { sync: true, transition: options.scaleTransition, restoreAfterFinish: true}),
+ new Effect.Move(element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition })
+ ], Object.extend({
+ beforeStartInternal: function(effect) {
+ effect.effects[0].element.makePositioned().makeClipping();
+ },
+ afterFinishInternal: function(effect) {
+ effect.effects[0].element.hide().undoClipping().undoPositioned().setStyle(oldStyle); }
+ }, options)
+ );
+}
+
+Effect.Pulsate = function(element) {
+ element = $(element);
+ var options = arguments[1] || {};
+ var oldOpacity = element.getInlineOpacity();
+ var transition = options.transition || Effect.Transitions.sinoidal;
+ var reverser = function(pos){ return transition(1-Effect.Transitions.pulse(pos, options.pulses)) };
+ reverser.bind(transition);
+ return new Effect.Opacity(element,
+ Object.extend(Object.extend({ duration: 2.0, from: 0,
+ afterFinishInternal: function(effect) { effect.element.setStyle({opacity: oldOpacity}); }
+ }, options), {transition: reverser}));
+}
+
+Effect.Fold = function(element) {
+ element = $(element);
+ var oldStyle = {
+ top: element.style.top,
+ left: element.style.left,
+ width: element.style.width,
+ height: element.style.height };
+ element.makeClipping();
+ return new Effect.Scale(element, 5, Object.extend({
+ scaleContent: false,
+ scaleX: false,
+ afterFinishInternal: function(effect) {
+ new Effect.Scale(element, 1, {
+ scaleContent: false,
+ scaleY: false,
+ afterFinishInternal: function(effect) {
+ effect.element.hide().undoClipping().setStyle(oldStyle);
+ } });
+ }}, arguments[1] || {}));
+};
+
+Effect.Morph = Class.create();
+Object.extend(Object.extend(Effect.Morph.prototype, Effect.Base.prototype), {
+ initialize: function(element) {
+ this.element = $(element);
+ if(!this.element) throw(Effect._elementDoesNotExistError);
+ var options = Object.extend({
+ style: {}
+ }, arguments[1] || {});
+ if (typeof options.style == 'string') {
+ if(options.style.indexOf(':') == -1) {
+ var cssText = '', selector = '.' + options.style;
+ $A(document.styleSheets).reverse().each(function(styleSheet) {
+ if (styleSheet.cssRules) cssRules = styleSheet.cssRules;
+ else if (styleSheet.rules) cssRules = styleSheet.rules;
+ $A(cssRules).reverse().each(function(rule) {
+ if (selector == rule.selectorText) {
+ cssText = rule.style.cssText;
+ throw $break;
+ }
+ });
+ if (cssText) throw $break;
+ });
+ this.style = cssText.parseStyle();
+ options.afterFinishInternal = function(effect){
+ effect.element.addClassName(effect.options.style);
+ effect.transforms.each(function(transform) {
+ if(transform.style != 'opacity')
+ effect.element.style[transform.style] = '';
+ });
+ }
+ } else this.style = options.style.parseStyle();
+ } else this.style = $H(options.style)
+ this.start(options);
+ },
+ setup: function(){
+ function parseColor(color){
+ if(!color || ['rgba(0, 0, 0, 0)','transparent'].include(color)) color = '#ffffff';
+ color = color.parseColor();
+ return $R(0,2).map(function(i){
+ return parseInt( color.slice(i*2+1,i*2+3), 16 )
+ });
+ }
+ this.transforms = this.style.map(function(pair){
+ var property = pair[0], value = pair[1], unit = null;
+
+ if(value.parseColor('#zzzzzz') != '#zzzzzz') {
+ value = value.parseColor();
+ unit = 'color';
+ } else if(property == 'opacity') {
+ value = parseFloat(value);
+ if(Prototype.Browser.IE && (!this.element.currentStyle.hasLayout))
+ this.element.setStyle({zoom: 1});
+ } else if(Element.CSS_LENGTH.test(value)) {
+ var components = value.match(/^([\+\-]?[0-9\.]+)(.*)$/);
+ value = parseFloat(components[1]);
+ unit = (components.length == 3) ? components[2] : null;
+ }
+
+ var originalValue = this.element.getStyle(property);
+ return {
+ style: property.camelize(),
+ originalValue: unit=='color' ? parseColor(originalValue) : parseFloat(originalValue || 0),
+ targetValue: unit=='color' ? parseColor(value) : value,
+ unit: unit
+ };
+ }.bind(this)).reject(function(transform){
+ return (
+ (transform.originalValue == transform.targetValue) ||
+ (
+ transform.unit != 'color' &&
+ (isNaN(transform.originalValue) || isNaN(transform.targetValue))
+ )
+ )
+ });
+ },
+ update: function(position) {
+ var style = {}, transform, i = this.transforms.length;
+ while(i--)
+ style[(transform = this.transforms[i]).style] =
+ transform.unit=='color' ? '#'+
+ (Math.round(transform.originalValue[0]+
+ (transform.targetValue[0]-transform.originalValue[0])*position)).toColorPart() +
+ (Math.round(transform.originalValue[1]+
+ (transform.targetValue[1]-transform.originalValue[1])*position)).toColorPart() +
+ (Math.round(transform.originalValue[2]+
+ (transform.targetValue[2]-transform.originalValue[2])*position)).toColorPart() :
+ transform.originalValue + Math.round(
+ ((transform.targetValue - transform.originalValue) * position) * 1000)/1000 + transform.unit;
+ this.element.setStyle(style, true);
+ }
+});
+
+Effect.Transform = Class.create();
+Object.extend(Effect.Transform.prototype, {
+ initialize: function(tracks){
+ this.tracks = [];
+ this.options = arguments[1] || {};
+ this.addTracks(tracks);
+ },
+ addTracks: function(tracks){
+ tracks.each(function(track){
+ var data = $H(track).values().first();
+ this.tracks.push($H({
+ ids: $H(track).keys().first(),
+ effect: Effect.Morph,
+ options: { style: data }
+ }));
+ }.bind(this));
+ return this;
+ },
+ play: function(){
+ return new Effect.Parallel(
+ this.tracks.map(function(track){
+ var elements = [$(track.ids) || $$(track.ids)].flatten();
+ return elements.map(function(e){ return new track.effect(e, Object.extend({ sync:true }, track.options)) });
+ }).flatten(),
+ this.options
+ );
+ }
+});
+
+Element.CSS_PROPERTIES = $w(
+ 'backgroundColor backgroundPosition borderBottomColor borderBottomStyle ' +
+ 'borderBottomWidth borderLeftColor borderLeftStyle borderLeftWidth ' +
+ 'borderRightColor borderRightStyle borderRightWidth borderSpacing ' +
+ 'borderTopColor borderTopStyle borderTopWidth bottom clip color ' +
+ 'fontSize fontWeight height left letterSpacing lineHeight ' +
+ 'marginBottom marginLeft marginRight marginTop markerOffset maxHeight '+
+ 'maxWidth minHeight minWidth opacity outlineColor outlineOffset ' +
+ 'outlineWidth paddingBottom paddingLeft paddingRight paddingTop ' +
+ 'right textIndent top width wordSpacing zIndex');
+
+Element.CSS_LENGTH = /^(([\+\-]?[0-9\.]+)(em|ex|px|in|cm|mm|pt|pc|\%))|0$/;
+
+String.prototype.parseStyle = function(){
+ var element = document.createElement('div');
+ element.innerHTML = '<div style="' + this + '"></div>';
+ var style = element.childNodes[0].style, styleRules = $H();
+
+ Element.CSS_PROPERTIES.each(function(property){
+ if(style[property]) styleRules[property] = style[property];
+ });
+ if(Prototype.Browser.IE && this.indexOf('opacity') > -1) {
+ styleRules.opacity = this.match(/opacity:\s*((?:0|1)?(?:\.\d*)?)/)[1];
+ }
+ return styleRules;
+};
+
+Element.morph = function(element, style) {
+ new Effect.Morph(element, Object.extend({ style: style }, arguments[2] || {}));
+ return element;
+};
+
+['getInlineOpacity','forceRerendering','setContentZoom',
+ 'collectTextNodes','collectTextNodesIgnoreClass','morph'].each(
+ function(f) { Element.Methods[f] = Element[f]; }
+);
+
+Element.Methods.visualEffect = function(element, effect, options) {
+ s = effect.dasherize().camelize();
+ effect_class = s.charAt(0).toUpperCase() + s.substring(1);
+ new Effect[effect_class](element, options);
+ return $(element);
+};
+
+Element.addMethods(); \ No newline at end of file
diff --git a/ipa-server/ipa-gui/ipagui/static/javascript/ipautil.js b/ipa-server/ipa-gui/ipagui/static/javascript/ipautil.js
new file mode 100644
index 00000000..de747c5c
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/static/javascript/ipautil.js
@@ -0,0 +1,24 @@
+/* Copyright (C) 2007 Red Hat
+ * see file 'COPYING' for use and warranty information
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; version 2 only
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ */
+
+/*
+ * Escapes the ' " and \ characters in a string, so
+ * it can be embedded inside a dynamically generated string.
+ */
+function jsStringEscape(input) {
+ return input.gsub(/(['"\\])/, function(match){ return "\\" + match[0];} );
+}
diff --git a/ipa-server/ipa-gui/ipagui/static/javascript/prototype.js b/ipa-server/ipa-gui/ipagui/static/javascript/prototype.js
new file mode 100644
index 00000000..a3f21ac7
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/static/javascript/prototype.js
@@ -0,0 +1,3277 @@
+/* Prototype JavaScript framework, version 1.5.1.1
+ * (c) 2005-2007 Sam Stephenson
+ *
+ * Prototype is freely distributable under the terms of an MIT-style license.
+ * For details, see the Prototype web site: http://www.prototypejs.org/
+ *
+/*--------------------------------------------------------------------------*/
+
+var Prototype = {
+ Version: '1.5.1.1',
+
+ Browser: {
+ IE: !!(window.attachEvent && !window.opera),
+ Opera: !!window.opera,
+ WebKit: navigator.userAgent.indexOf('AppleWebKit/') > -1,
+ Gecko: navigator.userAgent.indexOf('Gecko') > -1 && navigator.userAgent.indexOf('KHTML') == -1
+ },
+
+ BrowserFeatures: {
+ XPath: !!document.evaluate,
+ ElementExtensions: !!window.HTMLElement,
+ SpecificElementExtensions:
+ (document.createElement('div').__proto__ !==
+ document.createElement('form').__proto__)
+ },
+
+ ScriptFragment: '<script[^>]*>([\\S\\s]*?)<\/script>',
+ JSONFilter: /^\/\*-secure-([\s\S]*)\*\/\s*$/,
+
+ emptyFunction: function() { },
+ K: function(x) { return x }
+}
+
+var Class = {
+ create: function() {
+ return function() {
+ this.initialize.apply(this, arguments);
+ }
+ }
+}
+
+var Abstract = new Object();
+
+Object.extend = function(destination, source) {
+ for (var property in source) {
+ destination[property] = source[property];
+ }
+ return destination;
+}
+
+Object.extend(Object, {
+ inspect: function(object) {
+ try {
+ if (object === undefined) return 'undefined';
+ if (object === null) return 'null';
+ return object.inspect ? object.inspect() : object.toString();
+ } catch (e) {
+ if (e instanceof RangeError) return '...';
+ throw e;
+ }
+ },
+
+ toJSON: function(object) {
+ var type = typeof object;
+ switch(type) {
+ case 'undefined':
+ case 'function':
+ case 'unknown': return;
+ case 'boolean': return object.toString();
+ }
+ if (object === null) return 'null';
+ if (object.toJSON) return object.toJSON();
+ if (object.ownerDocument === document) return;
+ var results = [];
+ for (var property in object) {
+ var value = Object.toJSON(object[property]);
+ if (value !== undefined)
+ results.push(property.toJSON() + ': ' + value);
+ }
+ return '{' + results.join(', ') + '}';
+ },
+
+ keys: function(object) {
+ var keys = [];
+ for (var property in object)
+ keys.push(property);
+ return keys;
+ },
+
+ values: function(object) {
+ var values = [];
+ for (var property in object)
+ values.push(object[property]);
+ return values;
+ },
+
+ clone: function(object) {
+ return Object.extend({}, object);
+ }
+});
+
+Function.prototype.bind = function() {
+ var __method = this, args = $A(arguments), object = args.shift();
+ return function() {
+ return __method.apply(object, args.concat($A(arguments)));
+ }
+}
+
+Function.prototype.bindAsEventListener = function(object) {
+ var __method = this, args = $A(arguments), object = args.shift();
+ return function(event) {
+ return __method.apply(object, [event || window.event].concat(args));
+ }
+}
+
+Object.extend(Number.prototype, {
+ toColorPart: function() {
+ return this.toPaddedString(2, 16);
+ },
+
+ succ: function() {
+ return this + 1;
+ },
+
+ times: function(iterator) {
+ $R(0, this, true).each(iterator);
+ return this;
+ },
+
+ toPaddedString: function(length, radix) {
+ var string = this.toString(radix || 10);
+ return '0'.times(length - string.length) + string;
+ },
+
+ toJSON: function() {
+ return isFinite(this) ? this.toString() : 'null';
+ }
+});
+
+Date.prototype.toJSON = function() {
+ return '"' + this.getFullYear() + '-' +
+ (this.getMonth() + 1).toPaddedString(2) + '-' +
+ this.getDate().toPaddedString(2) + 'T' +
+ this.getHours().toPaddedString(2) + ':' +
+ this.getMinutes().toPaddedString(2) + ':' +
+ this.getSeconds().toPaddedString(2) + '"';
+};
+
+var Try = {
+ these: function() {
+ var returnValue;
+
+ for (var i = 0, length = arguments.length; i < length; i++) {
+ var lambda = arguments[i];
+ try {
+ returnValue = lambda();
+ break;
+ } catch (e) {}
+ }
+
+ return returnValue;
+ }
+}
+
+/*--------------------------------------------------------------------------*/
+
+var PeriodicalExecuter = Class.create();
+PeriodicalExecuter.prototype = {
+ initialize: function(callback, frequency) {
+ this.callback = callback;
+ this.frequency = frequency;
+ this.currentlyExecuting = false;
+
+ this.registerCallback();
+ },
+
+ registerCallback: function() {
+ this.timer = setInterval(this.onTimerEvent.bind(this), this.frequency * 1000);
+ },
+
+ stop: function() {
+ if (!this.timer) return;
+ clearInterval(this.timer);
+ this.timer = null;
+ },
+
+ onTimerEvent: function() {
+ if (!this.currentlyExecuting) {
+ try {
+ this.currentlyExecuting = true;
+ this.callback(this);
+ } finally {
+ this.currentlyExecuting = false;
+ }
+ }
+ }
+}
+Object.extend(String, {
+ interpret: function(value) {
+ return value == null ? '' : String(value);
+ },
+ specialChar: {
+ '\b': '\\b',
+ '\t': '\\t',
+ '\n': '\\n',
+ '\f': '\\f',
+ '\r': '\\r',
+ '\\': '\\\\'
+ }
+});
+
+Object.extend(String.prototype, {
+ gsub: function(pattern, replacement) {
+ var result = '', source = this, match;
+ replacement = arguments.callee.prepareReplacement(replacement);
+
+ while (source.length > 0) {
+ if (match = source.match(pattern)) {
+ result += source.slice(0, match.index);
+ result += String.interpret(replacement(match));
+ source = source.slice(match.index + match[0].length);
+ } else {
+ result += source, source = '';
+ }
+ }
+ return result;
+ },
+
+ sub: function(pattern, replacement, count) {
+ replacement = this.gsub.prepareReplacement(replacement);
+ count = count === undefined ? 1 : count;
+
+ return this.gsub(pattern, function(match) {
+ if (--count < 0) return match[0];
+ return replacement(match);
+ });
+ },
+
+ scan: function(pattern, iterator) {
+ this.gsub(pattern, iterator);
+ return this;
+ },
+
+ truncate: function(length, truncation) {
+ length = length || 30;
+ truncation = truncation === undefined ? '...' : truncation;
+ return this.length > length ?
+ this.slice(0, length - truncation.length) + truncation : this;
+ },
+
+ strip: function() {
+ return this.replace(/^\s+/, '').replace(/\s+$/, '');
+ },
+
+ stripTags: function() {
+ return this.replace(/<\/?[^>]+>/gi, '');
+ },
+
+ stripScripts: function() {
+ return this.replace(new RegExp(Prototype.ScriptFragment, 'img'), '');
+ },
+
+ extractScripts: function() {
+ var matchAll = new RegExp(Prototype.ScriptFragment, 'img');
+ var matchOne = new RegExp(Prototype.ScriptFragment, 'im');
+ return (this.match(matchAll) || []).map(function(scriptTag) {
+ return (scriptTag.match(matchOne) || ['', ''])[1];
+ });
+ },
+
+ evalScripts: function() {
+ return this.extractScripts().map(function(script) { return eval(script) });
+ },
+
+ escapeHTML: function() {
+ var self = arguments.callee;
+ self.text.data = this;
+ return self.div.innerHTML;
+ },
+
+ unescapeHTML: function() {
+ var div = document.createElement('div');
+ div.innerHTML = this.stripTags();
+ return div.childNodes[0] ? (div.childNodes.length > 1 ?
+ $A(div.childNodes).inject('', function(memo, node) { return memo+node.nodeValue }) :
+ div.childNodes[0].nodeValue) : '';
+ },
+
+ toQueryParams: function(separator) {
+ var match = this.strip().match(/([^?#]*)(#.*)?$/);
+ if (!match) return {};
+
+ return match[1].split(separator || '&').inject({}, function(hash, pair) {
+ if ((pair = pair.split('='))[0]) {
+ var key = decodeURIComponent(pair.shift());
+ var value = pair.length > 1 ? pair.join('=') : pair[0];
+ if (value != undefined) value = decodeURIComponent(value);
+
+ if (key in hash) {
+ if (hash[key].constructor != Array) hash[key] = [hash[key]];
+ hash[key].push(value);
+ }
+ else hash[key] = value;
+ }
+ return hash;
+ });
+ },
+
+ toArray: function() {
+ return this.split('');
+ },
+
+ succ: function() {
+ return this.slice(0, this.length - 1) +
+ String.fromCharCode(this.charCodeAt(this.length - 1) + 1);
+ },
+
+ times: function(count) {
+ var result = '';
+ for (var i = 0; i < count; i++) result += this;
+ return result;
+ },
+
+ camelize: function() {
+ var parts = this.split('-'), len = parts.length;
+ if (len == 1) return parts[0];
+
+ var camelized = this.charAt(0) == '-'
+ ? parts[0].charAt(0).toUpperCase() + parts[0].substring(1)
+ : parts[0];
+
+ for (var i = 1; i < len; i++)
+ camelized += parts[i].charAt(0).toUpperCase() + parts[i].substring(1);
+
+ return camelized;
+ },
+
+ capitalize: function() {
+ return this.charAt(0).toUpperCase() + this.substring(1).toLowerCase();
+ },
+
+ underscore: function() {
+ return this.gsub(/::/, '/').gsub(/([A-Z]+)([A-Z][a-z])/,'#{1}_#{2}').gsub(/([a-z\d])([A-Z])/,'#{1}_#{2}').gsub(/-/,'_').toLowerCase();
+ },
+
+ dasherize: function() {
+ return this.gsub(/_/,'-');
+ },
+
+ inspect: function(useDoubleQuotes) {
+ var escapedString = this.gsub(/[\x00-\x1f\\]/, function(match) {
+ var character = String.specialChar[match[0]];
+ return character ? character : '\\u00' + match[0].charCodeAt().toPaddedString(2, 16);
+ });
+ if (useDoubleQuotes) return '"' + escapedString.replace(/"/g, '\\"') + '"';
+ return "'" + escapedString.replace(/'/g, '\\\'') + "'";
+ },
+
+ toJSON: function() {
+ return this.inspect(true);
+ },
+
+ unfilterJSON: function(filter) {
+ return this.sub(filter || Prototype.JSONFilter, '#{1}');
+ },
+
+ isJSON: function() {
+ var str = this.replace(/\\./g, '@').replace(/"[^"\\\n\r]*"/g, '');
+ return (/^[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]*$/).test(str);
+ },
+
+ evalJSON: function(sanitize) {
+ var json = this.unfilterJSON();
+ try {
+ if (!sanitize || json.isJSON()) return eval('(' + json + ')');
+ } catch (e) { }
+ throw new SyntaxError('Badly formed JSON string: ' + this.inspect());
+ },
+
+ include: function(pattern) {
+ return this.indexOf(pattern) > -1;
+ },
+
+ startsWith: function(pattern) {
+ return this.indexOf(pattern) === 0;
+ },
+
+ endsWith: function(pattern) {
+ var d = this.length - pattern.length;
+ return d >= 0 && this.lastIndexOf(pattern) === d;
+ },
+
+ empty: function() {
+ return this == '';
+ },
+
+ blank: function() {
+ return /^\s*$/.test(this);
+ }
+});
+
+if (Prototype.Browser.WebKit || Prototype.Browser.IE) Object.extend(String.prototype, {
+ escapeHTML: function() {
+ return this.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
+ },
+ unescapeHTML: function() {
+ return this.replace(/&amp;/g,'&').replace(/&lt;/g,'<').replace(/&gt;/g,'>');
+ }
+});
+
+String.prototype.gsub.prepareReplacement = function(replacement) {
+ if (typeof replacement == 'function') return replacement;
+ var template = new Template(replacement);
+ return function(match) { return template.evaluate(match) };
+}
+
+String.prototype.parseQuery = String.prototype.toQueryParams;
+
+Object.extend(String.prototype.escapeHTML, {
+ div: document.createElement('div'),
+ text: document.createTextNode('')
+});
+
+with (String.prototype.escapeHTML) div.appendChild(text);
+
+var Template = Class.create();
+Template.Pattern = /(^|.|\r|\n)(#\{(.*?)\})/;
+Template.prototype = {
+ initialize: function(template, pattern) {
+ this.template = template.toString();
+ this.pattern = pattern || Template.Pattern;
+ },
+
+ evaluate: function(object) {
+ return this.template.gsub(this.pattern, function(match) {
+ var before = match[1];
+ if (before == '\\') return match[2];
+ return before + String.interpret(object[match[3]]);
+ });
+ }
+}
+
+var $break = {}, $continue = new Error('"throw $continue" is deprecated, use "return" instead');
+
+var Enumerable = {
+ each: function(iterator) {
+ var index = 0;
+ try {
+ this._each(function(value) {
+ iterator(value, index++);
+ });
+ } catch (e) {
+ if (e != $break) throw e;
+ }
+ return this;
+ },
+
+ eachSlice: function(number, iterator) {
+ var index = -number, slices = [], array = this.toArray();
+ while ((index += number) < array.length)
+ slices.push(array.slice(index, index+number));
+ return slices.map(iterator);
+ },
+
+ all: function(iterator) {
+ var result = true;
+ this.each(function(value, index) {
+ result = result && !!(iterator || Prototype.K)(value, index);
+ if (!result) throw $break;
+ });
+ return result;
+ },
+
+ any: function(iterator) {
+ var result = false;
+ this.each(function(value, index) {
+ if (result = !!(iterator || Prototype.K)(value, index))
+ throw $break;
+ });
+ return result;
+ },
+
+ collect: function(iterator) {
+ var results = [];
+ this.each(function(value, index) {
+ results.push((iterator || Prototype.K)(value, index));
+ });
+ return results;
+ },
+
+ detect: function(iterator) {
+ var result;
+ this.each(function(value, index) {
+ if (iterator(value, index)) {
+ result = value;
+ throw $break;
+ }
+ });
+ return result;
+ },
+
+ findAll: function(iterator) {
+ var results = [];
+ this.each(function(value, index) {
+ if (iterator(value, index))
+ results.push(value);
+ });
+ return results;
+ },
+
+ grep: function(pattern, iterator) {
+ var results = [];
+ this.each(function(value, index) {
+ var stringValue = value.toString();
+ if (stringValue.match(pattern))
+ results.push((iterator || Prototype.K)(value, index));
+ })
+ return results;
+ },
+
+ include: function(object) {
+ var found = false;
+ this.each(function(value) {
+ if (value == object) {
+ found = true;
+ throw $break;
+ }
+ });
+ return found;
+ },
+
+ inGroupsOf: function(number, fillWith) {
+ fillWith = fillWith === undefined ? null : fillWith;
+ return this.eachSlice(number, function(slice) {
+ while(slice.length < number) slice.push(fillWith);
+ return slice;
+ });
+ },
+
+ inject: function(memo, iterator) {
+ this.each(function(value, index) {
+ memo = iterator(memo, value, index);
+ });
+ return memo;
+ },
+
+ invoke: function(method) {
+ var args = $A(arguments).slice(1);
+ return this.map(function(value) {
+ return value[method].apply(value, args);
+ });
+ },
+
+ max: function(iterator) {
+ var result;
+ this.each(function(value, index) {
+ value = (iterator || Prototype.K)(value, index);
+ if (result == undefined || value >= result)
+ result = value;
+ });
+ return result;
+ },
+
+ min: function(iterator) {
+ var result;
+ this.each(function(value, index) {
+ value = (iterator || Prototype.K)(value, index);
+ if (result == undefined || value < result)
+ result = value;
+ });
+ return result;
+ },
+
+ partition: function(iterator) {
+ var trues = [], falses = [];
+ this.each(function(value, index) {
+ ((iterator || Prototype.K)(value, index) ?
+ trues : falses).push(value);
+ });
+ return [trues, falses];
+ },
+
+ pluck: function(property) {
+ var results = [];
+ this.each(function(value, index) {
+ results.push(value[property]);
+ });
+ return results;
+ },
+
+ reject: function(iterator) {
+ var results = [];
+ this.each(function(value, index) {
+ if (!iterator(value, index))
+ results.push(value);
+ });
+ return results;
+ },
+
+ sortBy: function(iterator) {
+ return this.map(function(value, index) {
+ return {value: value, criteria: iterator(value, index)};
+ }).sort(function(left, right) {
+ var a = left.criteria, b = right.criteria;
+ return a < b ? -1 : a > b ? 1 : 0;
+ }).pluck('value');
+ },
+
+ toArray: function() {
+ return this.map();
+ },
+
+ zip: function() {
+ var iterator = Prototype.K, args = $A(arguments);
+ if (typeof args.last() == 'function')
+ iterator = args.pop();
+
+ var collections = [this].concat(args).map($A);
+ return this.map(function(value, index) {
+ return iterator(collections.pluck(index));
+ });
+ },
+
+ size: function() {
+ return this.toArray().length;
+ },
+
+ inspect: function() {
+ return '#<Enumerable:' + this.toArray().inspect() + '>';
+ }
+}
+
+Object.extend(Enumerable, {
+ map: Enumerable.collect,
+ find: Enumerable.detect,
+ select: Enumerable.findAll,
+ member: Enumerable.include,
+ entries: Enumerable.toArray
+});
+var $A = Array.from = function(iterable) {
+ if (!iterable) return [];
+ if (iterable.toArray) {
+ return iterable.toArray();
+ } else {
+ var results = [];
+ for (var i = 0, length = iterable.length; i < length; i++)
+ results.push(iterable[i]);
+ return results;
+ }
+}
+
+if (Prototype.Browser.WebKit) {
+ $A = Array.from = function(iterable) {
+ if (!iterable) return [];
+ if (!(typeof iterable == 'function' && iterable == '[object NodeList]') &&
+ iterable.toArray) {
+ return iterable.toArray();
+ } else {
+ var results = [];
+ for (var i = 0, length = iterable.length; i < length; i++)
+ results.push(iterable[i]);
+ return results;
+ }
+ }
+}
+
+Object.extend(Array.prototype, Enumerable);
+
+if (!Array.prototype._reverse)
+ Array.prototype._reverse = Array.prototype.reverse;
+
+Object.extend(Array.prototype, {
+ _each: function(iterator) {
+ for (var i = 0, length = this.length; i < length; i++)
+ iterator(this[i]);
+ },
+
+ clear: function() {
+ this.length = 0;
+ return this;
+ },
+
+ first: function() {
+ return this[0];
+ },
+
+ last: function() {
+ return this[this.length - 1];
+ },
+
+ compact: function() {
+ return this.select(function(value) {
+ return value != null;
+ });
+ },
+
+ flatten: function() {
+ return this.inject([], function(array, value) {
+ return array.concat(value && value.constructor == Array ?
+ value.flatten() : [value]);
+ });
+ },
+
+ without: function() {
+ var values = $A(arguments);
+ return this.select(function(value) {
+ return !values.include(value);
+ });
+ },
+
+ indexOf: function(object) {
+ for (var i = 0, length = this.length; i < length; i++)
+ if (this[i] == object) return i;
+ return -1;
+ },
+
+ reverse: function(inline) {
+ return (inline !== false ? this : this.toArray())._reverse();
+ },
+
+ reduce: function() {
+ return this.length > 1 ? this : this[0];
+ },
+
+ uniq: function(sorted) {
+ return this.inject([], function(array, value, index) {
+ if (0 == index || (sorted ? array.last() != value : !array.include(value)))
+ array.push(value);
+ return array;
+ });
+ },
+
+ clone: function() {
+ return [].concat(this);
+ },
+
+ size: function() {
+ return this.length;
+ },
+
+ inspect: function() {
+ return '[' + this.map(Object.inspect).join(', ') + ']';
+ },
+
+ toJSON: function() {
+ var results = [];
+ this.each(function(object) {
+ var value = Object.toJSON(object);
+ if (value !== undefined) results.push(value);
+ });
+ return '[' + results.join(', ') + ']';
+ }
+});
+
+Array.prototype.toArray = Array.prototype.clone;
+
+function $w(string) {
+ string = string.strip();
+ return string ? string.split(/\s+/) : [];
+}
+
+if (Prototype.Browser.Opera){
+ Array.prototype.concat = function() {
+ var array = [];
+ for (var i = 0, length = this.length; i < length; i++) array.push(this[i]);
+ for (var i = 0, length = arguments.length; i < length; i++) {
+ if (arguments[i].constructor == Array) {
+ for (var j = 0, arrayLength = arguments[i].length; j < arrayLength; j++)
+ array.push(arguments[i][j]);
+ } else {
+ array.push(arguments[i]);
+ }
+ }
+ return array;
+ }
+}
+var Hash = function(object) {
+ if (object instanceof Hash) this.merge(object);
+ else Object.extend(this, object || {});
+};
+
+Object.extend(Hash, {
+ toQueryString: function(obj) {
+ var parts = [];
+ parts.add = arguments.callee.addPair;
+
+ this.prototype._each.call(obj, function(pair) {
+ if (!pair.key) return;
+ var value = pair.value;
+
+ if (value && typeof value == 'object') {
+ if (value.constructor == Array) value.each(function(value) {
+ parts.add(pair.key, value);
+ });
+ return;
+ }
+ parts.add(pair.key, value);
+ });
+
+ return parts.join('&');
+ },
+
+ toJSON: function(object) {
+ var results = [];
+ this.prototype._each.call(object, function(pair) {
+ var value = Object.toJSON(pair.value);
+ if (value !== undefined) results.push(pair.key.toJSON() + ': ' + value);
+ });
+ return '{' + results.join(', ') + '}';
+ }
+});
+
+Hash.toQueryString.addPair = function(key, value, prefix) {
+ key = encodeURIComponent(key);
+ if (value === undefined) this.push(key);
+ else this.push(key + '=' + (value == null ? '' : encodeURIComponent(value)));
+}
+
+Object.extend(Hash.prototype, Enumerable);
+Object.extend(Hash.prototype, {
+ _each: function(iterator) {
+ for (var key in this) {
+ var value = this[key];
+ if (value && value == Hash.prototype[key]) continue;
+
+ var pair = [key, value];
+ pair.key = key;
+ pair.value = value;
+ iterator(pair);
+ }
+ },
+
+ keys: function() {
+ return this.pluck('key');
+ },
+
+ values: function() {
+ return this.pluck('value');
+ },
+
+ merge: function(hash) {
+ return $H(hash).inject(this, function(mergedHash, pair) {
+ mergedHash[pair.key] = pair.value;
+ return mergedHash;
+ });
+ },
+
+ remove: function() {
+ var result;
+ for(var i = 0, length = arguments.length; i < length; i++) {
+ var value = this[arguments[i]];
+ if (value !== undefined){
+ if (result === undefined) result = value;
+ else {
+ if (result.constructor != Array) result = [result];
+ result.push(value)
+ }
+ }
+ delete this[arguments[i]];
+ }
+ return result;
+ },
+
+ toQueryString: function() {
+ return Hash.toQueryString(this);
+ },
+
+ inspect: function() {
+ return '#<Hash:{' + this.map(function(pair) {
+ return pair.map(Object.inspect).join(': ');
+ }).join(', ') + '}>';
+ },
+
+ toJSON: function() {
+ return Hash.toJSON(this);
+ }
+});
+
+function $H(object) {
+ if (object instanceof Hash) return object;
+ return new Hash(object);
+};
+
+// Safari iterates over shadowed properties
+if (function() {
+ var i = 0, Test = function(value) { this.key = value };
+ Test.prototype.key = 'foo';
+ for (var property in new Test('bar')) i++;
+ return i > 1;
+}()) Hash.prototype._each = function(iterator) {
+ var cache = [];
+ for (var key in this) {
+ var value = this[key];
+ if ((value && value == Hash.prototype[key]) || cache.include(key)) continue;
+ cache.push(key);
+ var pair = [key, value];
+ pair.key = key;
+ pair.value = value;
+ iterator(pair);
+ }
+};
+ObjectRange = Class.create();
+Object.extend(ObjectRange.prototype, Enumerable);
+Object.extend(ObjectRange.prototype, {
+ initialize: function(start, end, exclusive) {
+ this.start = start;
+ this.end = end;
+ this.exclusive = exclusive;
+ },
+
+ _each: function(iterator) {
+ var value = this.start;
+ while (this.include(value)) {
+ iterator(value);
+ value = value.succ();
+ }
+ },
+
+ include: function(value) {
+ if (value < this.start)
+ return false;
+ if (this.exclusive)
+ return value < this.end;
+ return value <= this.end;
+ }
+});
+
+var $R = function(start, end, exclusive) {
+ return new ObjectRange(start, end, exclusive);
+}
+
+var Ajax = {
+ getTransport: function() {
+ return Try.these(
+ function() {return new XMLHttpRequest()},
+ function() {return new ActiveXObject('Msxml2.XMLHTTP')},
+ function() {return new ActiveXObject('Microsoft.XMLHTTP')}
+ ) || false;
+ },
+
+ activeRequestCount: 0
+}
+
+Ajax.Responders = {
+ responders: [],
+
+ _each: function(iterator) {
+ this.responders._each(iterator);
+ },
+
+ register: function(responder) {
+ if (!this.include(responder))
+ this.responders.push(responder);
+ },
+
+ unregister: function(responder) {
+ this.responders = this.responders.without(responder);
+ },
+
+ dispatch: function(callback, request, transport, json) {
+ this.each(function(responder) {
+ if (typeof responder[callback] == 'function') {
+ try {
+ responder[callback].apply(responder, [request, transport, json]);
+ } catch (e) {}
+ }
+ });
+ }
+};
+
+Object.extend(Ajax.Responders, Enumerable);
+
+Ajax.Responders.register({
+ onCreate: function() {
+ Ajax.activeRequestCount++;
+ },
+ onComplete: function() {
+ Ajax.activeRequestCount--;
+ }
+});
+
+Ajax.Base = function() {};
+Ajax.Base.prototype = {
+ setOptions: function(options) {
+ this.options = {
+ method: 'post',
+ asynchronous: true,
+ contentType: 'application/x-www-form-urlencoded',
+ encoding: 'UTF-8',
+ parameters: ''
+ }
+ Object.extend(this.options, options || {});
+
+ this.options.method = this.options.method.toLowerCase();
+ if (typeof this.options.parameters == 'string')
+ this.options.parameters = this.options.parameters.toQueryParams();
+ }
+}
+
+Ajax.Request = Class.create();
+Ajax.Request.Events =
+ ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete'];
+
+Ajax.Request.prototype = Object.extend(new Ajax.Base(), {
+ _complete: false,
+
+ initialize: function(url, options) {
+ this.transport = Ajax.getTransport();
+ this.setOptions(options);
+ this.request(url);
+ },
+
+ request: function(url) {
+ this.url = url;
+ this.method = this.options.method;
+ var params = Object.clone(this.options.parameters);
+
+ if (!['get', 'post'].include(this.method)) {
+ // simulate other verbs over post
+ params['_method'] = this.method;
+ this.method = 'post';
+ }
+
+ this.parameters = params;
+
+ if (params = Hash.toQueryString(params)) {
+ // when GET, append parameters to URL
+ if (this.method == 'get')
+ this.url += (this.url.include('?') ? '&' : '?') + params;
+ else if (/Konqueror|Safari|KHTML/.test(navigator.userAgent))
+ params += '&_=';
+ }
+
+ try {
+ if (this.options.onCreate) this.options.onCreate(this.transport);
+ Ajax.Responders.dispatch('onCreate', this, this.transport);
+
+ this.transport.open(this.method.toUpperCase(), this.url,
+ this.options.asynchronous);
+
+ if (this.options.asynchronous)
+ setTimeout(function() { this.respondToReadyState(1) }.bind(this), 10);
+
+ this.transport.onreadystatechange = this.onStateChange.bind(this);
+ this.setRequestHeaders();
+
+ this.body = this.method == 'post' ? (this.options.postBody || params) : null;
+ this.transport.send(this.body);
+
+ /* Force Firefox to handle ready state 4 for synchronous requests */
+ if (!this.options.asynchronous && this.transport.overrideMimeType)
+ this.onStateChange();
+
+ }
+ catch (e) {
+ this.dispatchException(e);
+ }
+ },
+
+ onStateChange: function() {
+ var readyState = this.transport.readyState;
+ if (readyState > 1 && !((readyState == 4) && this._complete))
+ this.respondToReadyState(this.transport.readyState);
+ },
+
+ setRequestHeaders: function() {
+ var headers = {
+ 'X-Requested-With': 'XMLHttpRequest',
+ 'X-Prototype-Version': Prototype.Version,
+ 'Accept': 'text/javascript, text/html, application/xml, text/xml, */*'
+ };
+
+ if (this.method == 'post') {
+ headers['Content-type'] = this.options.contentType +
+ (this.options.encoding ? '; charset=' + this.options.encoding : '');
+
+ /* Force "Connection: close" for older Mozilla browsers to work
+ * around a bug where XMLHttpRequest sends an incorrect
+ * Content-length header. See Mozilla Bugzilla #246651.
+ */
+ if (this.transport.overrideMimeType &&
+ (navigator.userAgent.match(/Gecko\/(\d{4})/) || [0,2005])[1] < 2005)
+ headers['Connection'] = 'close';
+ }
+
+ // user-defined headers
+ if (typeof this.options.requestHeaders == 'object') {
+ var extras = this.options.requestHeaders;
+
+ if (typeof extras.push == 'function')
+ for (var i = 0, length = extras.length; i < length; i += 2)
+ headers[extras[i]] = extras[i+1];
+ else
+ $H(extras).each(function(pair) { headers[pair.key] = pair.value });
+ }
+
+ for (var name in headers)
+ this.transport.setRequestHeader(name, headers[name]);
+ },
+
+ success: function() {
+ return !this.transport.status
+ || (this.transport.status >= 200 && this.transport.status < 300);
+ },
+
+ respondToReadyState: function(readyState) {
+ var state = Ajax.Request.Events[readyState];
+ var transport = this.transport, json = this.evalJSON();
+
+ if (state == 'Complete') {
+ try {
+ this._complete = true;
+ (this.options['on' + this.transport.status]
+ || this.options['on' + (this.success() ? 'Success' : 'Failure')]
+ || Prototype.emptyFunction)(transport, json);
+ } catch (e) {
+ this.dispatchException(e);
+ }
+
+ var contentType = this.getHeader('Content-type');
+ if (contentType && contentType.strip().
+ match(/^(text|application)\/(x-)?(java|ecma)script(;.*)?$/i))
+ this.evalResponse();
+ }
+
+ try {
+ (this.options['on' + state] || Prototype.emptyFunction)(transport, json);
+ Ajax.Responders.dispatch('on' + state, this, transport, json);
+ } catch (e) {
+ this.dispatchException(e);
+ }
+
+ if (state == 'Complete') {
+ // avoid memory leak in MSIE: clean up
+ this.transport.onreadystatechange = Prototype.emptyFunction;
+ }
+ },
+
+ getHeader: function(name) {
+ try {
+ return this.transport.getResponseHeader(name);
+ } catch (e) { return null }
+ },
+
+ evalJSON: function() {
+ try {
+ var json = this.getHeader('X-JSON');
+ return json ? json.evalJSON() : null;
+ } catch (e) { return null }
+ },
+
+ evalResponse: function() {
+ try {
+ return eval((this.transport.responseText || '').unfilterJSON());
+ } catch (e) {
+ this.dispatchException(e);
+ }
+ },
+
+ dispatchException: function(exception) {
+ (this.options.onException || Prototype.emptyFunction)(this, exception);
+ Ajax.Responders.dispatch('onException', this, exception);
+ }
+});
+
+Ajax.Updater = Class.create();
+
+Object.extend(Object.extend(Ajax.Updater.prototype, Ajax.Request.prototype), {
+ initialize: function(container, url, options) {
+ this.container = {
+ success: (container.success || container),
+ failure: (container.failure || (container.success ? null : container))
+ }
+
+ this.transport = Ajax.getTransport();
+ this.setOptions(options);
+
+ var onComplete = this.options.onComplete || Prototype.emptyFunction;
+ this.options.onComplete = (function(transport, param) {
+ this.updateContent();
+ onComplete(transport, param);
+ }).bind(this);
+
+ this.request(url);
+ },
+
+ updateContent: function() {
+ var receiver = this.container[this.success() ? 'success' : 'failure'];
+ var response = this.transport.responseText;
+
+ if (!this.options.evalScripts) response = response.stripScripts();
+
+ if (receiver = $(receiver)) {
+ if (this.options.insertion)
+ new this.options.insertion(receiver, response);
+ else
+ receiver.update(response);
+ }
+
+ if (this.success()) {
+ if (this.onComplete)
+ setTimeout(this.onComplete.bind(this), 10);
+ }
+ }
+});
+
+Ajax.PeriodicalUpdater = Class.create();
+Ajax.PeriodicalUpdater.prototype = Object.extend(new Ajax.Base(), {
+ initialize: function(container, url, options) {
+ this.setOptions(options);
+ this.onComplete = this.options.onComplete;
+
+ this.frequency = (this.options.frequency || 2);
+ this.decay = (this.options.decay || 1);
+
+ this.updater = {};
+ this.container = container;
+ this.url = url;
+
+ this.start();
+ },
+
+ start: function() {
+ this.options.onComplete = this.updateComplete.bind(this);
+ this.onTimerEvent();
+ },
+
+ stop: function() {
+ this.updater.options.onComplete = undefined;
+ clearTimeout(this.timer);
+ (this.onComplete || Prototype.emptyFunction).apply(this, arguments);
+ },
+
+ updateComplete: function(request) {
+ if (this.options.decay) {
+ this.decay = (request.responseText == this.lastText ?
+ this.decay * this.options.decay : 1);
+
+ this.lastText = request.responseText;
+ }
+ this.timer = setTimeout(this.onTimerEvent.bind(this),
+ this.decay * this.frequency * 1000);
+ },
+
+ onTimerEvent: function() {
+ this.updater = new Ajax.Updater(this.container, this.url, this.options);
+ }
+});
+function $(element) {
+ if (arguments.length > 1) {
+ for (var i = 0, elements = [], length = arguments.length; i < length; i++)
+ elements.push($(arguments[i]));
+ return elements;
+ }
+ if (typeof element == 'string')
+ element = document.getElementById(element);
+ return Element.extend(element);
+}
+
+if (Prototype.BrowserFeatures.XPath) {
+ document._getElementsByXPath = function(expression, parentElement) {
+ var results = [];
+ var query = document.evaluate(expression, $(parentElement) || document,
+ null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
+ for (var i = 0, length = query.snapshotLength; i < length; i++)
+ results.push(query.snapshotItem(i));
+ return results;
+ };
+
+ document.getElementsByClassName = function(className, parentElement) {
+ var q = ".//*[contains(concat(' ', @class, ' '), ' " + className + " ')]";
+ return document._getElementsByXPath(q, parentElement);
+ }
+
+} else document.getElementsByClassName = function(className, parentElement) {
+ var children = ($(parentElement) || document.body).getElementsByTagName('*');
+ var elements = [], child, pattern = new RegExp("(^|\\s)" + className + "(\\s|$)");
+ for (var i = 0, length = children.length; i < length; i++) {
+ child = children[i];
+ var elementClassName = child.className;
+ if (elementClassName.length == 0) continue;
+ if (elementClassName == className || elementClassName.match(pattern))
+ elements.push(Element.extend(child));
+ }
+ return elements;
+};
+
+/*--------------------------------------------------------------------------*/
+
+if (!window.Element) var Element = {};
+
+Element.extend = function(element) {
+ var F = Prototype.BrowserFeatures;
+ if (!element || !element.tagName || element.nodeType == 3 ||
+ element._extended || F.SpecificElementExtensions || element == window)
+ return element;
+
+ var methods = {}, tagName = element.tagName, cache = Element.extend.cache,
+ T = Element.Methods.ByTag;
+
+ // extend methods for all tags (Safari doesn't need this)
+ if (!F.ElementExtensions) {
+ Object.extend(methods, Element.Methods),
+ Object.extend(methods, Element.Methods.Simulated);
+ }
+
+ // extend methods for specific tags
+ if (T[tagName]) Object.extend(methods, T[tagName]);
+
+ for (var property in methods) {
+ var value = methods[property];
+ if (typeof value == 'function' && !(property in element))
+ element[property] = cache.findOrStore(value);
+ }
+
+ element._extended = Prototype.emptyFunction;
+ return element;
+};
+
+Element.extend.cache = {
+ findOrStore: function(value) {
+ return this[value] = this[value] || function() {
+ return value.apply(null, [this].concat($A(arguments)));
+ }
+ }
+};
+
+Element.Methods = {
+ visible: function(element) {
+ return $(element).style.display != 'none';
+ },
+
+ toggle: function(element) {
+ element = $(element);
+ Element[Element.visible(element) ? 'hide' : 'show'](element);
+ return element;
+ },
+
+ hide: function(element) {
+ $(element).style.display = 'none';
+ return element;
+ },
+
+ show: function(element) {
+ $(element).style.display = '';
+ return element;
+ },
+
+ remove: function(element) {
+ element = $(element);
+ element.parentNode.removeChild(element);
+ return element;
+ },
+
+ update: function(element, html) {
+ html = typeof html == 'undefined' ? '' : html.toString();
+ $(element).innerHTML = html.stripScripts();
+ setTimeout(function() {html.evalScripts()}, 10);
+ return element;
+ },
+
+ replace: function(element, html) {
+ element = $(element);
+ html = typeof html == 'undefined' ? '' : html.toString();
+ if (element.outerHTML) {
+ element.outerHTML = html.stripScripts();
+ } else {
+ var range = element.ownerDocument.createRange();
+ range.selectNodeContents(element);
+ element.parentNode.replaceChild(
+ range.createContextualFragment(html.stripScripts()), element);
+ }
+ setTimeout(function() {html.evalScripts()}, 10);
+ return element;
+ },
+
+ inspect: function(element) {
+ element = $(element);
+ var result = '<' + element.tagName.toLowerCase();
+ $H({'id': 'id', 'className': 'class'}).each(function(pair) {
+ var property = pair.first(), attribute = pair.last();
+ var value = (element[property] || '').toString();
+ if (value) result += ' ' + attribute + '=' + value.inspect(true);
+ });
+ return result + '>';
+ },
+
+ recursivelyCollect: function(element, property) {
+ element = $(element);
+ var elements = [];
+ while (element = element[property])
+ if (element.nodeType == 1)
+ elements.push(Element.extend(element));
+ return elements;
+ },
+
+ ancestors: function(element) {
+ return $(element).recursivelyCollect('parentNode');
+ },
+
+ descendants: function(element) {
+ return $A($(element).getElementsByTagName('*')).each(Element.extend);
+ },
+
+ firstDescendant: function(element) {
+ element = $(element).firstChild;
+ while (element && element.nodeType != 1) element = element.nextSibling;
+ return $(element);
+ },
+
+ immediateDescendants: function(element) {
+ if (!(element = $(element).firstChild)) return [];
+ while (element && element.nodeType != 1) element = element.nextSibling;
+ if (element) return [element].concat($(element).nextSiblings());
+ return [];
+ },
+
+ previousSiblings: function(element) {
+ return $(element).recursivelyCollect('previousSibling');
+ },
+
+ nextSiblings: function(element) {
+ return $(element).recursivelyCollect('nextSibling');
+ },
+
+ siblings: function(element) {
+ element = $(element);
+ return element.previousSiblings().reverse().concat(element.nextSiblings());
+ },
+
+ match: function(element, selector) {
+ if (typeof selector == 'string')
+ selector = new Selector(selector);
+ return selector.match($(element));
+ },
+
+ up: function(element, expression, index) {
+ element = $(element);
+ if (arguments.length == 1) return $(element.parentNode);
+ var ancestors = element.ancestors();
+ return expression ? Selector.findElement(ancestors, expression, index) :
+ ancestors[index || 0];
+ },
+
+ down: function(element, expression, index) {
+ element = $(element);
+ if (arguments.length == 1) return element.firstDescendant();
+ var descendants = element.descendants();
+ return expression ? Selector.findElement(descendants, expression, index) :
+ descendants[index || 0];
+ },
+
+ previous: function(element, expression, index) {
+ element = $(element);
+ if (arguments.length == 1) return $(Selector.handlers.previousElementSibling(element));
+ var previousSiblings = element.previousSiblings();
+ return expression ? Selector.findElement(previousSiblings, expression, index) :
+ previousSiblings[index || 0];
+ },
+
+ next: function(element, expression, index) {
+ element = $(element);
+ if (arguments.length == 1) return $(Selector.handlers.nextElementSibling(element));
+ var nextSiblings = element.nextSiblings();
+ return expression ? Selector.findElement(nextSiblings, expression, index) :
+ nextSiblings[index || 0];
+ },
+
+ getElementsBySelector: function() {
+ var args = $A(arguments), element = $(args.shift());
+ return Selector.findChildElements(element, args);
+ },
+
+ getElementsByClassName: function(element, className) {
+ return document.getElementsByClassName(className, element);
+ },
+
+ readAttribute: function(element, name) {
+ element = $(element);
+ if (Prototype.Browser.IE) {
+ if (!element.attributes) return null;
+ var t = Element._attributeTranslations;
+ if (t.values[name]) return t.values[name](element, name);
+ if (t.names[name]) name = t.names[name];
+ var attribute = element.attributes[name];
+ return attribute ? attribute.nodeValue : null;
+ }
+ return element.getAttribute(name);
+ },
+
+ getHeight: function(element) {
+ return $(element).getDimensions().height;
+ },
+
+ getWidth: function(element) {
+ return $(element).getDimensions().width;
+ },
+
+ classNames: function(element) {
+ return new Element.ClassNames(element);
+ },
+
+ hasClassName: function(element, className) {
+ if (!(element = $(element))) return;
+ var elementClassName = element.className;
+ if (elementClassName.length == 0) return false;
+ if (elementClassName == className ||
+ elementClassName.match(new RegExp("(^|\\s)" + className + "(\\s|$)")))
+ return true;
+ return false;
+ },
+
+ addClassName: function(element, className) {
+ if (!(element = $(element))) return;
+ Element.classNames(element).add(className);
+ return element;
+ },
+
+ removeClassName: function(element, className) {
+ if (!(element = $(element))) return;
+ Element.classNames(element).remove(className);
+ return element;
+ },
+
+ toggleClassName: function(element, className) {
+ if (!(element = $(element))) return;
+ Element.classNames(element)[element.hasClassName(className) ? 'remove' : 'add'](className);
+ return element;
+ },
+
+ observe: function() {
+ Event.observe.apply(Event, arguments);
+ return $A(arguments).first();
+ },
+
+ stopObserving: function() {
+ Event.stopObserving.apply(Event, arguments);
+ return $A(arguments).first();
+ },
+
+ // removes whitespace-only text node children
+ cleanWhitespace: function(element) {
+ element = $(element);
+ var node = element.firstChild;
+ while (node) {
+ var nextNode = node.nextSibling;
+ if (node.nodeType == 3 && !/\S/.test(node.nodeValue))
+ element.removeChild(node);
+ node = nextNode;
+ }
+ return element;
+ },
+
+ empty: function(element) {
+ return $(element).innerHTML.blank();
+ },
+
+ descendantOf: function(element, ancestor) {
+ element = $(element), ancestor = $(ancestor);
+ while (element = element.parentNode)
+ if (element == ancestor) return true;
+ return false;
+ },
+
+ scrollTo: function(element) {
+ element = $(element);
+ var pos = Position.cumulativeOffset(element);
+ window.scrollTo(pos[0], pos[1]);
+ return element;
+ },
+
+ getStyle: function(element, style) {
+ element = $(element);
+ style = style == 'float' ? 'cssFloat' : style.camelize();
+ var value = element.style[style];
+ if (!value) {
+ var css = document.defaultView.getComputedStyle(element, null);
+ value = css ? css[style] : null;
+ }
+ if (style == 'opacity') return value ? parseFloat(value) : 1.0;
+ return value == 'auto' ? null : value;
+ },
+
+ getOpacity: function(element) {
+ return $(element).getStyle('opacity');
+ },
+
+ setStyle: function(element, styles, camelized) {
+ element = $(element);
+ var elementStyle = element.style;
+
+ for (var property in styles)
+ if (property == 'opacity') element.setOpacity(styles[property])
+ else
+ elementStyle[(property == 'float' || property == 'cssFloat') ?
+ (elementStyle.styleFloat === undefined ? 'cssFloat' : 'styleFloat') :
+ (camelized ? property : property.camelize())] = styles[property];
+
+ return element;
+ },
+
+ setOpacity: function(element, value) {
+ element = $(element);
+ element.style.opacity = (value == 1 || value === '') ? '' :
+ (value < 0.00001) ? 0 : value;
+ return element;
+ },
+
+ getDimensions: function(element) {
+ element = $(element);
+ var display = $(element).getStyle('display');
+ if (display != 'none' && display != null) // Safari bug
+ return {width: element.offsetWidth, height: element.offsetHeight};
+
+ // All *Width and *Height properties give 0 on elements with display none,
+ // so enable the element temporarily
+ var els = element.style;
+ var originalVisibility = els.visibility;
+ var originalPosition = els.position;
+ var originalDisplay = els.display;
+ els.visibility = 'hidden';
+ els.position = 'absolute';
+ els.display = 'block';
+ var originalWidth = element.clientWidth;
+ var originalHeight = element.clientHeight;
+ els.display = originalDisplay;
+ els.position = originalPosition;
+ els.visibility = originalVisibility;
+ return {width: originalWidth, height: originalHeight};
+ },
+
+ makePositioned: function(element) {
+ element = $(element);
+ var pos = Element.getStyle(element, 'position');
+ if (pos == 'static' || !pos) {
+ element._madePositioned = true;
+ element.style.position = 'relative';
+ // Opera returns the offset relative to the positioning context, when an
+ // element is position relative but top and left have not been defined
+ if (window.opera) {
+ element.style.top = 0;
+ element.style.left = 0;
+ }
+ }
+ return element;
+ },
+
+ undoPositioned: function(element) {
+ element = $(element);
+ if (element._madePositioned) {
+ element._madePositioned = undefined;
+ element.style.position =
+ element.style.top =
+ element.style.left =
+ element.style.bottom =
+ element.style.right = '';
+ }
+ return element;
+ },
+
+ makeClipping: function(element) {
+ element = $(element);
+ if (element._overflow) return element;
+ element._overflow = element.style.overflow || 'auto';
+ if ((Element.getStyle(element, 'overflow') || 'visible') != 'hidden')
+ element.style.overflow = 'hidden';
+ return element;
+ },
+
+ undoClipping: function(element) {
+ element = $(element);
+ if (!element._overflow) return element;
+ element.style.overflow = element._overflow == 'auto' ? '' : element._overflow;
+ element._overflow = null;
+ return element;
+ }
+};
+
+Object.extend(Element.Methods, {
+ childOf: Element.Methods.descendantOf,
+ childElements: Element.Methods.immediateDescendants
+});
+
+if (Prototype.Browser.Opera) {
+ Element.Methods._getStyle = Element.Methods.getStyle;
+ Element.Methods.getStyle = function(element, style) {
+ switch(style) {
+ case 'left':
+ case 'top':
+ case 'right':
+ case 'bottom':
+ if (Element._getStyle(element, 'position') == 'static') return null;
+ default: return Element._getStyle(element, style);
+ }
+ };
+}
+else if (Prototype.Browser.IE) {
+ Element.Methods.getStyle = function(element, style) {
+ element = $(element);
+ style = (style == 'float' || style == 'cssFloat') ? 'styleFloat' : style.camelize();
+ var value = element.style[style];
+ if (!value && element.currentStyle) value = element.currentStyle[style];
+
+ if (style == 'opacity') {
+ if (value = (element.getStyle('filter') || '').match(/alpha\(opacity=(.*)\)/))
+ if (value[1]) return parseFloat(value[1]) / 100;
+ return 1.0;
+ }
+
+ if (value == 'auto') {
+ if ((style == 'width' || style == 'height') && (element.getStyle('display') != 'none'))
+ return element['offset'+style.capitalize()] + 'px';
+ return null;
+ }
+ return value;
+ };
+
+ Element.Methods.setOpacity = function(element, value) {
+ element = $(element);
+ var filter = element.getStyle('filter'), style = element.style;
+ if (value == 1 || value === '') {
+ style.filter = filter.replace(/alpha\([^\)]*\)/gi,'');
+ return element;
+ } else if (value < 0.00001) value = 0;
+ style.filter = filter.replace(/alpha\([^\)]*\)/gi, '') +
+ 'alpha(opacity=' + (value * 100) + ')';
+ return element;
+ };
+
+ // IE is missing .innerHTML support for TABLE-related elements
+ Element.Methods.update = function(element, html) {
+ element = $(element);
+ html = typeof html == 'undefined' ? '' : html.toString();
+ var tagName = element.tagName.toUpperCase();
+ if (['THEAD','TBODY','TR','TD'].include(tagName)) {
+ var div = document.createElement('div');
+ switch (tagName) {
+ case 'THEAD':
+ case 'TBODY':
+ div.innerHTML = '<table><tbody>' + html.stripScripts() + '</tbody></table>';
+ depth = 2;
+ break;
+ case 'TR':
+ div.innerHTML = '<table><tbody><tr>' + html.stripScripts() + '</tr></tbody></table>';
+ depth = 3;
+ break;
+ case 'TD':
+ div.innerHTML = '<table><tbody><tr><td>' + html.stripScripts() + '</td></tr></tbody></table>';
+ depth = 4;
+ }
+ $A(element.childNodes).each(function(node) { element.removeChild(node) });
+ depth.times(function() { div = div.firstChild });
+ $A(div.childNodes).each(function(node) { element.appendChild(node) });
+ } else {
+ element.innerHTML = html.stripScripts();
+ }
+ setTimeout(function() { html.evalScripts() }, 10);
+ return element;
+ }
+}
+else if (Prototype.Browser.Gecko) {
+ Element.Methods.setOpacity = function(element, value) {
+ element = $(element);
+ element.style.opacity = (value == 1) ? 0.999999 :
+ (value === '') ? '' : (value < 0.00001) ? 0 : value;
+ return element;
+ };
+}
+
+Element._attributeTranslations = {
+ names: {
+ colspan: "colSpan",
+ rowspan: "rowSpan",
+ valign: "vAlign",
+ datetime: "dateTime",
+ accesskey: "accessKey",
+ tabindex: "tabIndex",
+ enctype: "encType",
+ maxlength: "maxLength",
+ readonly: "readOnly",
+ longdesc: "longDesc"
+ },
+ values: {
+ _getAttr: function(element, attribute) {
+ return element.getAttribute(attribute, 2);
+ },
+ _flag: function(element, attribute) {
+ return $(element).hasAttribute(attribute) ? attribute : null;
+ },
+ style: function(element) {
+ return element.style.cssText.toLowerCase();
+ },
+ title: function(element) {
+ var node = element.getAttributeNode('title');
+ return node.specified ? node.nodeValue : null;
+ }
+ }
+};
+
+(function() {
+ Object.extend(this, {
+ href: this._getAttr,
+ src: this._getAttr,
+ type: this._getAttr,
+ disabled: this._flag,
+ checked: this._flag,
+ readonly: this._flag,
+ multiple: this._flag
+ });
+}).call(Element._attributeTranslations.values);
+
+Element.Methods.Simulated = {
+ hasAttribute: function(element, attribute) {
+ var t = Element._attributeTranslations, node;
+ attribute = t.names[attribute] || attribute;
+ node = $(element).getAttributeNode(attribute);
+ return node && node.specified;
+ }
+};
+
+Element.Methods.ByTag = {};
+
+Object.extend(Element, Element.Methods);
+
+if (!Prototype.BrowserFeatures.ElementExtensions &&
+ document.createElement('div').__proto__) {
+ window.HTMLElement = {};
+ window.HTMLElement.prototype = document.createElement('div').__proto__;
+ Prototype.BrowserFeatures.ElementExtensions = true;
+}
+
+Element.hasAttribute = function(element, attribute) {
+ if (element.hasAttribute) return element.hasAttribute(attribute);
+ return Element.Methods.Simulated.hasAttribute(element, attribute);
+};
+
+Element.addMethods = function(methods) {
+ var F = Prototype.BrowserFeatures, T = Element.Methods.ByTag;
+
+ if (!methods) {
+ Object.extend(Form, Form.Methods);
+ Object.extend(Form.Element, Form.Element.Methods);
+ Object.extend(Element.Methods.ByTag, {
+ "FORM": Object.clone(Form.Methods),
+ "INPUT": Object.clone(Form.Element.Methods),
+ "SELECT": Object.clone(Form.Element.Methods),
+ "TEXTAREA": Object.clone(Form.Element.Methods)
+ });
+ }
+
+ if (arguments.length == 2) {
+ var tagName = methods;
+ methods = arguments[1];
+ }
+
+ if (!tagName) Object.extend(Element.Methods, methods || {});
+ else {
+ if (tagName.constructor == Array) tagName.each(extend);
+ else extend(tagName);
+ }
+
+ function extend(tagName) {
+ tagName = tagName.toUpperCase();
+ if (!Element.Methods.ByTag[tagName])
+ Element.Methods.ByTag[tagName] = {};
+ Object.extend(Element.Methods.ByTag[tagName], methods);
+ }
+
+ function copy(methods, destination, onlyIfAbsent) {
+ onlyIfAbsent = onlyIfAbsent || false;
+ var cache = Element.extend.cache;
+ for (var property in methods) {
+ var value = methods[property];
+ if (!onlyIfAbsent || !(property in destination))
+ destination[property] = cache.findOrStore(value);
+ }
+ }
+
+ function findDOMClass(tagName) {
+ var klass;
+ var trans = {
+ "OPTGROUP": "OptGroup", "TEXTAREA": "TextArea", "P": "Paragraph",
+ "FIELDSET": "FieldSet", "UL": "UList", "OL": "OList", "DL": "DList",
+ "DIR": "Directory", "H1": "Heading", "H2": "Heading", "H3": "Heading",
+ "H4": "Heading", "H5": "Heading", "H6": "Heading", "Q": "Quote",
+ "INS": "Mod", "DEL": "Mod", "A": "Anchor", "IMG": "Image", "CAPTION":
+ "TableCaption", "COL": "TableCol", "COLGROUP": "TableCol", "THEAD":
+ "TableSection", "TFOOT": "TableSection", "TBODY": "TableSection", "TR":
+ "TableRow", "TH": "TableCell", "TD": "TableCell", "FRAMESET":
+ "FrameSet", "IFRAME": "IFrame"
+ };
+ if (trans[tagName]) klass = 'HTML' + trans[tagName] + 'Element';
+ if (window[klass]) return window[klass];
+ klass = 'HTML' + tagName + 'Element';
+ if (window[klass]) return window[klass];
+ klass = 'HTML' + tagName.capitalize() + 'Element';
+ if (window[klass]) return window[klass];
+
+ window[klass] = {};
+ window[klass].prototype = document.createElement(tagName).__proto__;
+ return window[klass];
+ }
+
+ if (F.ElementExtensions) {
+ copy(Element.Methods, HTMLElement.prototype);
+ copy(Element.Methods.Simulated, HTMLElement.prototype, true);
+ }
+
+ if (F.SpecificElementExtensions) {
+ for (var tag in Element.Methods.ByTag) {
+ var klass = findDOMClass(tag);
+ if (typeof klass == "undefined") continue;
+ copy(T[tag], klass.prototype);
+ }
+ }
+
+ Object.extend(Element, Element.Methods);
+ delete Element.ByTag;
+};
+
+var Toggle = { display: Element.toggle };
+
+/*--------------------------------------------------------------------------*/
+
+Abstract.Insertion = function(adjacency) {
+ this.adjacency = adjacency;
+}
+
+Abstract.Insertion.prototype = {
+ initialize: function(element, content) {
+ this.element = $(element);
+ this.content = content.stripScripts();
+
+ if (this.adjacency && this.element.insertAdjacentHTML) {
+ try {
+ this.element.insertAdjacentHTML(this.adjacency, this.content);
+ } catch (e) {
+ var tagName = this.element.tagName.toUpperCase();
+ if (['TBODY', 'TR'].include(tagName)) {
+ this.insertContent(this.contentFromAnonymousTable());
+ } else {
+ throw e;
+ }
+ }
+ } else {
+ this.range = this.element.ownerDocument.createRange();
+ if (this.initializeRange) this.initializeRange();
+ this.insertContent([this.range.createContextualFragment(this.content)]);
+ }
+
+ setTimeout(function() {content.evalScripts()}, 10);
+ },
+
+ contentFromAnonymousTable: function() {
+ var div = document.createElement('div');
+ div.innerHTML = '<table><tbody>' + this.content + '</tbody></table>';
+ return $A(div.childNodes[0].childNodes[0].childNodes);
+ }
+}
+
+var Insertion = new Object();
+
+Insertion.Before = Class.create();
+Insertion.Before.prototype = Object.extend(new Abstract.Insertion('beforeBegin'), {
+ initializeRange: function() {
+ this.range.setStartBefore(this.element);
+ },
+
+ insertContent: function(fragments) {
+ fragments.each((function(fragment) {
+ this.element.parentNode.insertBefore(fragment, this.element);
+ }).bind(this));
+ }
+});
+
+Insertion.Top = Class.create();
+Insertion.Top.prototype = Object.extend(new Abstract.Insertion('afterBegin'), {
+ initializeRange: function() {
+ this.range.selectNodeContents(this.element);
+ this.range.collapse(true);
+ },
+
+ insertContent: function(fragments) {
+ fragments.reverse(false).each((function(fragment) {
+ this.element.insertBefore(fragment, this.element.firstChild);
+ }).bind(this));
+ }
+});
+
+Insertion.Bottom = Class.create();
+Insertion.Bottom.prototype = Object.extend(new Abstract.Insertion('beforeEnd'), {
+ initializeRange: function() {
+ this.range.selectNodeContents(this.element);
+ this.range.collapse(this.element);
+ },
+
+ insertContent: function(fragments) {
+ fragments.each((function(fragment) {
+ this.element.appendChild(fragment);
+ }).bind(this));
+ }
+});
+
+Insertion.After = Class.create();
+Insertion.After.prototype = Object.extend(new Abstract.Insertion('afterEnd'), {
+ initializeRange: function() {
+ this.range.setStartAfter(this.element);
+ },
+
+ insertContent: function(fragments) {
+ fragments.each((function(fragment) {
+ this.element.parentNode.insertBefore(fragment,
+ this.element.nextSibling);
+ }).bind(this));
+ }
+});
+
+/*--------------------------------------------------------------------------*/
+
+Element.ClassNames = Class.create();
+Element.ClassNames.prototype = {
+ initialize: function(element) {
+ this.element = $(element);
+ },
+
+ _each: function(iterator) {
+ this.element.className.split(/\s+/).select(function(name) {
+ return name.length > 0;
+ })._each(iterator);
+ },
+
+ set: function(className) {
+ this.element.className = className;
+ },
+
+ add: function(classNameToAdd) {
+ if (this.include(classNameToAdd)) return;
+ this.set($A(this).concat(classNameToAdd).join(' '));
+ },
+
+ remove: function(classNameToRemove) {
+ if (!this.include(classNameToRemove)) return;
+ this.set($A(this).without(classNameToRemove).join(' '));
+ },
+
+ toString: function() {
+ return $A(this).join(' ');
+ }
+};
+
+Object.extend(Element.ClassNames.prototype, Enumerable);
+/* Portions of the Selector class are derived from Jack Slocum’s DomQuery,
+ * part of YUI-Ext version 0.40, distributed under the terms of an MIT-style
+ * license. Please see http://www.yui-ext.com/ for more information. */
+
+var Selector = Class.create();
+
+Selector.prototype = {
+ initialize: function(expression) {
+ this.expression = expression.strip();
+ this.compileMatcher();
+ },
+
+ compileMatcher: function() {
+ // Selectors with namespaced attributes can't use the XPath version
+ if (Prototype.BrowserFeatures.XPath && !(/\[[\w-]*?:/).test(this.expression))
+ return this.compileXPathMatcher();
+
+ var e = this.expression, ps = Selector.patterns, h = Selector.handlers,
+ c = Selector.criteria, le, p, m;
+
+ if (Selector._cache[e]) {
+ this.matcher = Selector._cache[e]; return;
+ }
+ this.matcher = ["this.matcher = function(root) {",
+ "var r = root, h = Selector.handlers, c = false, n;"];
+
+ while (e && le != e && (/\S/).test(e)) {
+ le = e;
+ for (var i in ps) {
+ p = ps[i];
+ if (m = e.match(p)) {
+ this.matcher.push(typeof c[i] == 'function' ? c[i](m) :
+ new Template(c[i]).evaluate(m));
+ e = e.replace(m[0], '');
+ break;
+ }
+ }
+ }
+
+ this.matcher.push("return h.unique(n);\n}");
+ eval(this.matcher.join('\n'));
+ Selector._cache[this.expression] = this.matcher;
+ },
+
+ compileXPathMatcher: function() {
+ var e = this.expression, ps = Selector.patterns,
+ x = Selector.xpath, le, m;
+
+ if (Selector._cache[e]) {
+ this.xpath = Selector._cache[e]; return;
+ }
+
+ this.matcher = ['.//*'];
+ while (e && le != e && (/\S/).test(e)) {
+ le = e;
+ for (var i in ps) {
+ if (m = e.match(ps[i])) {
+ this.matcher.push(typeof x[i] == 'function' ? x[i](m) :
+ new Template(x[i]).evaluate(m));
+ e = e.replace(m[0], '');
+ break;
+ }
+ }
+ }
+
+ this.xpath = this.matcher.join('');
+ Selector._cache[this.expression] = this.xpath;
+ },
+
+ findElements: function(root) {
+ root = root || document;
+ if (this.xpath) return document._getElementsByXPath(this.xpath, root);
+ return this.matcher(root);
+ },
+
+ match: function(element) {
+ return this.findElements(document).include(element);
+ },
+
+ toString: function() {
+ return this.expression;
+ },
+
+ inspect: function() {
+ return "#<Selector:" + this.expression.inspect() + ">";
+ }
+};
+
+Object.extend(Selector, {
+ _cache: {},
+
+ xpath: {
+ descendant: "//*",
+ child: "/*",
+ adjacent: "/following-sibling::*[1]",
+ laterSibling: '/following-sibling::*',
+ tagName: function(m) {
+ if (m[1] == '*') return '';
+ return "[local-name()='" + m[1].toLowerCase() +
+ "' or local-name()='" + m[1].toUpperCase() + "']";
+ },
+ className: "[contains(concat(' ', @class, ' '), ' #{1} ')]",
+ id: "[@id='#{1}']",
+ attrPresence: "[@#{1}]",
+ attr: function(m) {
+ m[3] = m[5] || m[6];
+ return new Template(Selector.xpath.operators[m[2]]).evaluate(m);
+ },
+ pseudo: function(m) {
+ var h = Selector.xpath.pseudos[m[1]];
+ if (!h) return '';
+ if (typeof h === 'function') return h(m);
+ return new Template(Selector.xpath.pseudos[m[1]]).evaluate(m);
+ },
+ operators: {
+ '=': "[@#{1}='#{3}']",
+ '!=': "[@#{1}!='#{3}']",
+ '^=': "[starts-with(@#{1}, '#{3}')]",
+ '$=': "[substring(@#{1}, (string-length(@#{1}) - string-length('#{3}') + 1))='#{3}']",
+ '*=': "[contains(@#{1}, '#{3}')]",
+ '~=': "[contains(concat(' ', @#{1}, ' '), ' #{3} ')]",
+ '|=': "[contains(concat('-', @#{1}, '-'), '-#{3}-')]"
+ },
+ pseudos: {
+ 'first-child': '[not(preceding-sibling::*)]',
+ 'last-child': '[not(following-sibling::*)]',
+ 'only-child': '[not(preceding-sibling::* or following-sibling::*)]',
+ 'empty': "[count(*) = 0 and (count(text()) = 0 or translate(text(), ' \t\r\n', '') = '')]",
+ 'checked': "[@checked]",
+ 'disabled': "[@disabled]",
+ 'enabled': "[not(@disabled)]",
+ 'not': function(m) {
+ var e = m[6], p = Selector.patterns,
+ x = Selector.xpath, le, m, v;
+
+ var exclusion = [];
+ while (e && le != e && (/\S/).test(e)) {
+ le = e;
+ for (var i in p) {
+ if (m = e.match(p[i])) {
+ v = typeof x[i] == 'function' ? x[i](m) : new Template(x[i]).evaluate(m);
+ exclusion.push("(" + v.substring(1, v.length - 1) + ")");
+ e = e.replace(m[0], '');
+ break;
+ }
+ }
+ }
+ return "[not(" + exclusion.join(" and ") + ")]";
+ },
+ 'nth-child': function(m) {
+ return Selector.xpath.pseudos.nth("(count(./preceding-sibling::*) + 1) ", m);
+ },
+ 'nth-last-child': function(m) {
+ return Selector.xpath.pseudos.nth("(count(./following-sibling::*) + 1) ", m);
+ },
+ 'nth-of-type': function(m) {
+ return Selector.xpath.pseudos.nth("position() ", m);
+ },
+ 'nth-last-of-type': function(m) {
+ return Selector.xpath.pseudos.nth("(last() + 1 - position()) ", m);
+ },
+ 'first-of-type': function(m) {
+ m[6] = "1"; return Selector.xpath.pseudos['nth-of-type'](m);
+ },
+ 'last-of-type': function(m) {
+ m[6] = "1"; return Selector.xpath.pseudos['nth-last-of-type'](m);
+ },
+ 'only-of-type': function(m) {
+ var p = Selector.xpath.pseudos; return p['first-of-type'](m) + p['last-of-type'](m);
+ },
+ nth: function(fragment, m) {
+ var mm, formula = m[6], predicate;
+ if (formula == 'even') formula = '2n+0';
+ if (formula == 'odd') formula = '2n+1';
+ if (mm = formula.match(/^(\d+)$/)) // digit only
+ return '[' + fragment + "= " + mm[1] + ']';
+ if (mm = formula.match(/^(-?\d*)?n(([+-])(\d+))?/)) { // an+b
+ if (mm[1] == "-") mm[1] = -1;
+ var a = mm[1] ? Number(mm[1]) : 1;
+ var b = mm[2] ? Number(mm[2]) : 0;
+ predicate = "[((#{fragment} - #{b}) mod #{a} = 0) and " +
+ "((#{fragment} - #{b}) div #{a} >= 0)]";
+ return new Template(predicate).evaluate({
+ fragment: fragment, a: a, b: b });
+ }
+ }
+ }
+ },
+
+ criteria: {
+ tagName: 'n = h.tagName(n, r, "#{1}", c); c = false;',
+ className: 'n = h.className(n, r, "#{1}", c); c = false;',
+ id: 'n = h.id(n, r, "#{1}", c); c = false;',
+ attrPresence: 'n = h.attrPresence(n, r, "#{1}"); c = false;',
+ attr: function(m) {
+ m[3] = (m[5] || m[6]);
+ return new Template('n = h.attr(n, r, "#{1}", "#{3}", "#{2}"); c = false;').evaluate(m);
+ },
+ pseudo: function(m) {
+ if (m[6]) m[6] = m[6].replace(/"/g, '\\"');
+ return new Template('n = h.pseudo(n, "#{1}", "#{6}", r, c); c = false;').evaluate(m);
+ },
+ descendant: 'c = "descendant";',
+ child: 'c = "child";',
+ adjacent: 'c = "adjacent";',
+ laterSibling: 'c = "laterSibling";'
+ },
+
+ patterns: {
+ // combinators must be listed first
+ // (and descendant needs to be last combinator)
+ laterSibling: /^\s*~\s*/,
+ child: /^\s*>\s*/,
+ adjacent: /^\s*\+\s*/,
+ descendant: /^\s/,
+
+ // selectors follow
+ tagName: /^\s*(\*|[\w\-]+)(\b|$)?/,
+ id: /^#([\w\-\*]+)(\b|$)/,
+ className: /^\.([\w\-\*]+)(\b|$)/,
+ pseudo: /^:((first|last|nth|nth-last|only)(-child|-of-type)|empty|checked|(en|dis)abled|not)(\((.*?)\))?(\b|$|\s|(?=:))/,
+ attrPresence: /^\[([\w]+)\]/,
+ attr: /\[((?:[\w-]*:)?[\w-]+)\s*(?:([!^$*~|]?=)\s*((['"])([^\]]*?)\4|([^'"][^\]]*?)))?\]/
+ },
+
+ handlers: {
+ // UTILITY FUNCTIONS
+ // joins two collections
+ concat: function(a, b) {
+ for (var i = 0, node; node = b[i]; i++)
+ a.push(node);
+ return a;
+ },
+
+ // marks an array of nodes for counting
+ mark: function(nodes) {
+ for (var i = 0, node; node = nodes[i]; i++)
+ node._counted = true;
+ return nodes;
+ },
+
+ unmark: function(nodes) {
+ for (var i = 0, node; node = nodes[i]; i++)
+ node._counted = undefined;
+ return nodes;
+ },
+
+ // mark each child node with its position (for nth calls)
+ // "ofType" flag indicates whether we're indexing for nth-of-type
+ // rather than nth-child
+ index: function(parentNode, reverse, ofType) {
+ parentNode._counted = true;
+ if (reverse) {
+ for (var nodes = parentNode.childNodes, i = nodes.length - 1, j = 1; i >= 0; i--) {
+ node = nodes[i];
+ if (node.nodeType == 1 && (!ofType || node._counted)) node.nodeIndex = j++;
+ }
+ } else {
+ for (var i = 0, j = 1, nodes = parentNode.childNodes; node = nodes[i]; i++)
+ if (node.nodeType == 1 && (!ofType || node._counted)) node.nodeIndex = j++;
+ }
+ },
+
+ // filters out duplicates and extends all nodes
+ unique: function(nodes) {
+ if (nodes.length == 0) return nodes;
+ var results = [], n;
+ for (var i = 0, l = nodes.length; i < l; i++)
+ if (!(n = nodes[i])._counted) {
+ n._counted = true;
+ results.push(Element.extend(n));
+ }
+ return Selector.handlers.unmark(results);
+ },
+
+ // COMBINATOR FUNCTIONS
+ descendant: function(nodes) {
+ var h = Selector.handlers;
+ for (var i = 0, results = [], node; node = nodes[i]; i++)
+ h.concat(results, node.getElementsByTagName('*'));
+ return results;
+ },
+
+ child: function(nodes) {
+ var h = Selector.handlers;
+ for (var i = 0, results = [], node; node = nodes[i]; i++) {
+ for (var j = 0, children = [], child; child = node.childNodes[j]; j++)
+ if (child.nodeType == 1 && child.tagName != '!') results.push(child);
+ }
+ return results;
+ },
+
+ adjacent: function(nodes) {
+ for (var i = 0, results = [], node; node = nodes[i]; i++) {
+ var next = this.nextElementSibling(node);
+ if (next) results.push(next);
+ }
+ return results;
+ },
+
+ laterSibling: function(nodes) {
+ var h = Selector.handlers;
+ for (var i = 0, results = [], node; node = nodes[i]; i++)
+ h.concat(results, Element.nextSiblings(node));
+ return results;
+ },
+
+ nextElementSibling: function(node) {
+ while (node = node.nextSibling)
+ if (node.nodeType == 1) return node;
+ return null;
+ },
+
+ previousElementSibling: function(node) {
+ while (node = node.previousSibling)
+ if (node.nodeType == 1) return node;
+ return null;
+ },
+
+ // TOKEN FUNCTIONS
+ tagName: function(nodes, root, tagName, combinator) {
+ tagName = tagName.toUpperCase();
+ var results = [], h = Selector.handlers;
+ if (nodes) {
+ if (combinator) {
+ // fastlane for ordinary descendant combinators
+ if (combinator == "descendant") {
+ for (var i = 0, node; node = nodes[i]; i++)
+ h.concat(results, node.getElementsByTagName(tagName));
+ return results;
+ } else nodes = this[combinator](nodes);
+ if (tagName == "*") return nodes;
+ }
+ for (var i = 0, node; node = nodes[i]; i++)
+ if (node.tagName.toUpperCase() == tagName) results.push(node);
+ return results;
+ } else return root.getElementsByTagName(tagName);
+ },
+
+ id: function(nodes, root, id, combinator) {
+ var targetNode = $(id), h = Selector.handlers;
+ if (!nodes && root == document) return targetNode ? [targetNode] : [];
+ if (nodes) {
+ if (combinator) {
+ if (combinator == 'child') {
+ for (var i = 0, node; node = nodes[i]; i++)
+ if (targetNode.parentNode == node) return [targetNode];
+ } else if (combinator == 'descendant') {
+ for (var i = 0, node; node = nodes[i]; i++)
+ if (Element.descendantOf(targetNode, node)) return [targetNode];
+ } else if (combinator == 'adjacent') {
+ for (var i = 0, node; node = nodes[i]; i++)
+ if (Selector.handlers.previousElementSibling(targetNode) == node)
+ return [targetNode];
+ } else nodes = h[combinator](nodes);
+ }
+ for (var i = 0, node; node = nodes[i]; i++)
+ if (node == targetNode) return [targetNode];
+ return [];
+ }
+ return (targetNode && Element.descendantOf(targetNode, root)) ? [targetNode] : [];
+ },
+
+ className: function(nodes, root, className, combinator) {
+ if (nodes && combinator) nodes = this[combinator](nodes);
+ return Selector.handlers.byClassName(nodes, root, className);
+ },
+
+ byClassName: function(nodes, root, className) {
+ if (!nodes) nodes = Selector.handlers.descendant([root]);
+ var needle = ' ' + className + ' ';
+ for (var i = 0, results = [], node, nodeClassName; node = nodes[i]; i++) {
+ nodeClassName = node.className;
+ if (nodeClassName.length == 0) continue;
+ if (nodeClassName == className || (' ' + nodeClassName + ' ').include(needle))
+ results.push(node);
+ }
+ return results;
+ },
+
+ attrPresence: function(nodes, root, attr) {
+ var results = [];
+ for (var i = 0, node; node = nodes[i]; i++)
+ if (Element.hasAttribute(node, attr)) results.push(node);
+ return results;
+ },
+
+ attr: function(nodes, root, attr, value, operator) {
+ if (!nodes) nodes = root.getElementsByTagName("*");
+ var handler = Selector.operators[operator], results = [];
+ for (var i = 0, node; node = nodes[i]; i++) {
+ var nodeValue = Element.readAttribute(node, attr);
+ if (nodeValue === null) continue;
+ if (handler(nodeValue, value)) results.push(node);
+ }
+ return results;
+ },
+
+ pseudo: function(nodes, name, value, root, combinator) {
+ if (nodes && combinator) nodes = this[combinator](nodes);
+ if (!nodes) nodes = root.getElementsByTagName("*");
+ return Selector.pseudos[name](nodes, value, root);
+ }
+ },
+
+ pseudos: {
+ 'first-child': function(nodes, value, root) {
+ for (var i = 0, results = [], node; node = nodes[i]; i++) {
+ if (Selector.handlers.previousElementSibling(node)) continue;
+ results.push(node);
+ }
+ return results;
+ },
+ 'last-child': function(nodes, value, root) {
+ for (var i = 0, results = [], node; node = nodes[i]; i++) {
+ if (Selector.handlers.nextElementSibling(node)) continue;
+ results.push(node);
+ }
+ return results;
+ },
+ 'only-child': function(nodes, value, root) {
+ var h = Selector.handlers;
+ for (var i = 0, results = [], node; node = nodes[i]; i++)
+ if (!h.previousElementSibling(node) && !h.nextElementSibling(node))
+ results.push(node);
+ return results;
+ },
+ 'nth-child': function(nodes, formula, root) {
+ return Selector.pseudos.nth(nodes, formula, root);
+ },
+ 'nth-last-child': function(nodes, formula, root) {
+ return Selector.pseudos.nth(nodes, formula, root, true);
+ },
+ 'nth-of-type': function(nodes, formula, root) {
+ return Selector.pseudos.nth(nodes, formula, root, false, true);
+ },
+ 'nth-last-of-type': function(nodes, formula, root) {
+ return Selector.pseudos.nth(nodes, formula, root, true, true);
+ },
+ 'first-of-type': function(nodes, formula, root) {
+ return Selector.pseudos.nth(nodes, "1", root, false, true);
+ },
+ 'last-of-type': function(nodes, formula, root) {
+ return Selector.pseudos.nth(nodes, "1", root, true, true);
+ },
+ 'only-of-type': function(nodes, formula, root) {
+ var p = Selector.pseudos;
+ return p['last-of-type'](p['first-of-type'](nodes, formula, root), formula, root);
+ },
+
+ // handles the an+b logic
+ getIndices: function(a, b, total) {
+ if (a == 0) return b > 0 ? [b] : [];
+ return $R(1, total).inject([], function(memo, i) {
+ if (0 == (i - b) % a && (i - b) / a >= 0) memo.push(i);
+ return memo;
+ });
+ },
+
+ // handles nth(-last)-child, nth(-last)-of-type, and (first|last)-of-type
+ nth: function(nodes, formula, root, reverse, ofType) {
+ if (nodes.length == 0) return [];
+ if (formula == 'even') formula = '2n+0';
+ if (formula == 'odd') formula = '2n+1';
+ var h = Selector.handlers, results = [], indexed = [], m;
+ h.mark(nodes);
+ for (var i = 0, node; node = nodes[i]; i++) {
+ if (!node.parentNode._counted) {
+ h.index(node.parentNode, reverse, ofType);
+ indexed.push(node.parentNode);
+ }
+ }
+ if (formula.match(/^\d+$/)) { // just a number
+ formula = Number(formula);
+ for (var i = 0, node; node = nodes[i]; i++)
+ if (node.nodeIndex == formula) results.push(node);
+ } else if (m = formula.match(/^(-?\d*)?n(([+-])(\d+))?/)) { // an+b
+ if (m[1] == "-") m[1] = -1;
+ var a = m[1] ? Number(m[1]) : 1;
+ var b = m[2] ? Number(m[2]) : 0;
+ var indices = Selector.pseudos.getIndices(a, b, nodes.length);
+ for (var i = 0, node, l = indices.length; node = nodes[i]; i++) {
+ for (var j = 0; j < l; j++)
+ if (node.nodeIndex == indices[j]) results.push(node);
+ }
+ }
+ h.unmark(nodes);
+ h.unmark(indexed);
+ return results;
+ },
+
+ 'empty': function(nodes, value, root) {
+ for (var i = 0, results = [], node; node = nodes[i]; i++) {
+ // IE treats comments as element nodes
+ if (node.tagName == '!' || (node.firstChild && !node.innerHTML.match(/^\s*$/))) continue;
+ results.push(node);
+ }
+ return results;
+ },
+
+ 'not': function(nodes, selector, root) {
+ var h = Selector.handlers, selectorType, m;
+ var exclusions = new Selector(selector).findElements(root);
+ h.mark(exclusions);
+ for (var i = 0, results = [], node; node = nodes[i]; i++)
+ if (!node._counted) results.push(node);
+ h.unmark(exclusions);
+ return results;
+ },
+
+ 'enabled': function(nodes, value, root) {
+ for (var i = 0, results = [], node; node = nodes[i]; i++)
+ if (!node.disabled) results.push(node);
+ return results;
+ },
+
+ 'disabled': function(nodes, value, root) {
+ for (var i = 0, results = [], node; node = nodes[i]; i++)
+ if (node.disabled) results.push(node);
+ return results;
+ },
+
+ 'checked': function(nodes, value, root) {
+ for (var i = 0, results = [], node; node = nodes[i]; i++)
+ if (node.checked) results.push(node);
+ return results;
+ }
+ },
+
+ operators: {
+ '=': function(nv, v) { return nv == v; },
+ '!=': function(nv, v) { return nv != v; },
+ '^=': function(nv, v) { return nv.startsWith(v); },
+ '$=': function(nv, v) { return nv.endsWith(v); },
+ '*=': function(nv, v) { return nv.include(v); },
+ '~=': function(nv, v) { return (' ' + nv + ' ').include(' ' + v + ' '); },
+ '|=': function(nv, v) { return ('-' + nv.toUpperCase() + '-').include('-' + v.toUpperCase() + '-'); }
+ },
+
+ matchElements: function(elements, expression) {
+ var matches = new Selector(expression).findElements(), h = Selector.handlers;
+ h.mark(matches);
+ for (var i = 0, results = [], element; element = elements[i]; i++)
+ if (element._counted) results.push(element);
+ h.unmark(matches);
+ return results;
+ },
+
+ findElement: function(elements, expression, index) {
+ if (typeof expression == 'number') {
+ index = expression; expression = false;
+ }
+ return Selector.matchElements(elements, expression || '*')[index || 0];
+ },
+
+ findChildElements: function(element, expressions) {
+ var exprs = expressions.join(','), expressions = [];
+ exprs.scan(/(([\w#:.~>+()\s-]+|\*|\[.*?\])+)\s*(,|$)/, function(m) {
+ expressions.push(m[1].strip());
+ });
+ var results = [], h = Selector.handlers;
+ for (var i = 0, l = expressions.length, selector; i < l; i++) {
+ selector = new Selector(expressions[i].strip());
+ h.concat(results, selector.findElements(element));
+ }
+ return (l > 1) ? h.unique(results) : results;
+ }
+});
+
+function $$() {
+ return Selector.findChildElements(document, $A(arguments));
+}
+var Form = {
+ reset: function(form) {
+ $(form).reset();
+ return form;
+ },
+
+ serializeElements: function(elements, getHash) {
+ var data = elements.inject({}, function(result, element) {
+ if (!element.disabled && element.name) {
+ var key = element.name, value = $(element).getValue();
+ if (value != null) {
+ if (key in result) {
+ if (result[key].constructor != Array) result[key] = [result[key]];
+ result[key].push(value);
+ }
+ else result[key] = value;
+ }
+ }
+ return result;
+ });
+
+ return getHash ? data : Hash.toQueryString(data);
+ }
+};
+
+Form.Methods = {
+ serialize: function(form, getHash) {
+ return Form.serializeElements(Form.getElements(form), getHash);
+ },
+
+ getElements: function(form) {
+ return $A($(form).getElementsByTagName('*')).inject([],
+ function(elements, child) {
+ if (Form.Element.Serializers[child.tagName.toLowerCase()])
+ elements.push(Element.extend(child));
+ return elements;
+ }
+ );
+ },
+
+ getInputs: function(form, typeName, name) {
+ form = $(form);
+ var inputs = form.getElementsByTagName('input');
+
+ if (!typeName && !name) return $A(inputs).map(Element.extend);
+
+ for (var i = 0, matchingInputs = [], length = inputs.length; i < length; i++) {
+ var input = inputs[i];
+ if ((typeName && input.type != typeName) || (name && input.name != name))
+ continue;
+ matchingInputs.push(Element.extend(input));
+ }
+
+ return matchingInputs;
+ },
+
+ disable: function(form) {
+ form = $(form);
+ Form.getElements(form).invoke('disable');
+ return form;
+ },
+
+ enable: function(form) {
+ form = $(form);
+ Form.getElements(form).invoke('enable');
+ return form;
+ },
+
+ findFirstElement: function(form) {
+ return $(form).getElements().find(function(element) {
+ return element.type != 'hidden' && !element.disabled &&
+ ['input', 'select', 'textarea'].include(element.tagName.toLowerCase());
+ });
+ },
+
+ focusFirstElement: function(form) {
+ form = $(form);
+ form.findFirstElement().activate();
+ return form;
+ },
+
+ request: function(form, options) {
+ form = $(form), options = Object.clone(options || {});
+
+ var params = options.parameters;
+ options.parameters = form.serialize(true);
+
+ if (params) {
+ if (typeof params == 'string') params = params.toQueryParams();
+ Object.extend(options.parameters, params);
+ }
+
+ if (form.hasAttribute('method') && !options.method)
+ options.method = form.method;
+
+ return new Ajax.Request(form.readAttribute('action'), options);
+ }
+}
+
+/*--------------------------------------------------------------------------*/
+
+Form.Element = {
+ focus: function(element) {
+ $(element).focus();
+ return element;
+ },
+
+ select: function(element) {
+ $(element).select();
+ return element;
+ }
+}
+
+Form.Element.Methods = {
+ serialize: function(element) {
+ element = $(element);
+ if (!element.disabled && element.name) {
+ var value = element.getValue();
+ if (value != undefined) {
+ var pair = {};
+ pair[element.name] = value;
+ return Hash.toQueryString(pair);
+ }
+ }
+ return '';
+ },
+
+ getValue: function(element) {
+ element = $(element);
+ var method = element.tagName.toLowerCase();
+ return Form.Element.Serializers[method](element);
+ },
+
+ clear: function(element) {
+ $(element).value = '';
+ return element;
+ },
+
+ present: function(element) {
+ return $(element).value != '';
+ },
+
+ activate: function(element) {
+ element = $(element);
+ try {
+ element.focus();
+ if (element.select && (element.tagName.toLowerCase() != 'input' ||
+ !['button', 'reset', 'submit'].include(element.type)))
+ element.select();
+ } catch (e) {}
+ return element;
+ },
+
+ disable: function(element) {
+ element = $(element);
+ element.blur();
+ element.disabled = true;
+ return element;
+ },
+
+ enable: function(element) {
+ element = $(element);
+ element.disabled = false;
+ return element;
+ }
+}
+
+/*--------------------------------------------------------------------------*/
+
+var Field = Form.Element;
+var $F = Form.Element.Methods.getValue;
+
+/*--------------------------------------------------------------------------*/
+
+Form.Element.Serializers = {
+ input: function(element) {
+ switch (element.type.toLowerCase()) {
+ case 'checkbox':
+ case 'radio':
+ return Form.Element.Serializers.inputSelector(element);
+ default:
+ return Form.Element.Serializers.textarea(element);
+ }
+ },
+
+ inputSelector: function(element) {
+ return element.checked ? element.value : null;
+ },
+
+ textarea: function(element) {
+ return element.value;
+ },
+
+ select: function(element) {
+ return this[element.type == 'select-one' ?
+ 'selectOne' : 'selectMany'](element);
+ },
+
+ selectOne: function(element) {
+ var index = element.selectedIndex;
+ return index >= 0 ? this.optionValue(element.options[index]) : null;
+ },
+
+ selectMany: function(element) {
+ var values, length = element.length;
+ if (!length) return null;
+
+ for (var i = 0, values = []; i < length; i++) {
+ var opt = element.options[i];
+ if (opt.selected) values.push(this.optionValue(opt));
+ }
+ return values;
+ },
+
+ optionValue: function(opt) {
+ // extend element because hasAttribute may not be native
+ return Element.extend(opt).hasAttribute('value') ? opt.value : opt.text;
+ }
+}
+
+/*--------------------------------------------------------------------------*/
+
+Abstract.TimedObserver = function() {}
+Abstract.TimedObserver.prototype = {
+ initialize: function(element, frequency, callback) {
+ this.frequency = frequency;
+ this.element = $(element);
+ this.callback = callback;
+
+ this.lastValue = this.getValue();
+ this.registerCallback();
+ },
+
+ registerCallback: function() {
+ setInterval(this.onTimerEvent.bind(this), this.frequency * 1000);
+ },
+
+ onTimerEvent: function() {
+ var value = this.getValue();
+ var changed = ('string' == typeof this.lastValue && 'string' == typeof value
+ ? this.lastValue != value : String(this.lastValue) != String(value));
+ if (changed) {
+ this.callback(this.element, value);
+ this.lastValue = value;
+ }
+ }
+}
+
+Form.Element.Observer = Class.create();
+Form.Element.Observer.prototype = Object.extend(new Abstract.TimedObserver(), {
+ getValue: function() {
+ return Form.Element.getValue(this.element);
+ }
+});
+
+Form.Observer = Class.create();
+Form.Observer.prototype = Object.extend(new Abstract.TimedObserver(), {
+ getValue: function() {
+ return Form.serialize(this.element);
+ }
+});
+
+/*--------------------------------------------------------------------------*/
+
+Abstract.EventObserver = function() {}
+Abstract.EventObserver.prototype = {
+ initialize: function(element, callback) {
+ this.element = $(element);
+ this.callback = callback;
+
+ this.lastValue = this.getValue();
+ if (this.element.tagName.toLowerCase() == 'form')
+ this.registerFormCallbacks();
+ else
+ this.registerCallback(this.element);
+ },
+
+ onElementEvent: function() {
+ var value = this.getValue();
+ if (this.lastValue != value) {
+ this.callback(this.element, value);
+ this.lastValue = value;
+ }
+ },
+
+ registerFormCallbacks: function() {
+ Form.getElements(this.element).each(this.registerCallback.bind(this));
+ },
+
+ registerCallback: function(element) {
+ if (element.type) {
+ switch (element.type.toLowerCase()) {
+ case 'checkbox':
+ case 'radio':
+ Event.observe(element, 'click', this.onElementEvent.bind(this));
+ break;
+ default:
+ Event.observe(element, 'change', this.onElementEvent.bind(this));
+ break;
+ }
+ }
+ }
+}
+
+Form.Element.EventObserver = Class.create();
+Form.Element.EventObserver.prototype = Object.extend(new Abstract.EventObserver(), {
+ getValue: function() {
+ return Form.Element.getValue(this.element);
+ }
+});
+
+Form.EventObserver = Class.create();
+Form.EventObserver.prototype = Object.extend(new Abstract.EventObserver(), {
+ getValue: function() {
+ return Form.serialize(this.element);
+ }
+});
+if (!window.Event) {
+ var Event = new Object();
+}
+
+Object.extend(Event, {
+ KEY_BACKSPACE: 8,
+ KEY_TAB: 9,
+ KEY_RETURN: 13,
+ KEY_ESC: 27,
+ KEY_LEFT: 37,
+ KEY_UP: 38,
+ KEY_RIGHT: 39,
+ KEY_DOWN: 40,
+ KEY_DELETE: 46,
+ KEY_HOME: 36,
+ KEY_END: 35,
+ KEY_PAGEUP: 33,
+ KEY_PAGEDOWN: 34,
+
+ element: function(event) {
+ return $(event.target || event.srcElement);
+ },
+
+ isLeftClick: function(event) {
+ return (((event.which) && (event.which == 1)) ||
+ ((event.button) && (event.button == 1)));
+ },
+
+ pointerX: function(event) {
+ return event.pageX || (event.clientX +
+ (document.documentElement.scrollLeft || document.body.scrollLeft));
+ },
+
+ pointerY: function(event) {
+ return event.pageY || (event.clientY +
+ (document.documentElement.scrollTop || document.body.scrollTop));
+ },
+
+ stop: function(event) {
+ if (event.preventDefault) {
+ event.preventDefault();
+ event.stopPropagation();
+ } else {
+ event.returnValue = false;
+ event.cancelBubble = true;
+ }
+ },
+
+ // find the first node with the given tagName, starting from the
+ // node the event was triggered on; traverses the DOM upwards
+ findElement: function(event, tagName) {
+ var element = Event.element(event);
+ while (element.parentNode && (!element.tagName ||
+ (element.tagName.toUpperCase() != tagName.toUpperCase())))
+ element = element.parentNode;
+ return element;
+ },
+
+ observers: false,
+
+ _observeAndCache: function(element, name, observer, useCapture) {
+ if (!this.observers) this.observers = [];
+ if (element.addEventListener) {
+ this.observers.push([element, name, observer, useCapture]);
+ element.addEventListener(name, observer, useCapture);
+ } else if (element.attachEvent) {
+ this.observers.push([element, name, observer, useCapture]);
+ element.attachEvent('on' + name, observer);
+ }
+ },
+
+ unloadCache: function() {
+ if (!Event.observers) return;
+ for (var i = 0, length = Event.observers.length; i < length; i++) {
+ Event.stopObserving.apply(this, Event.observers[i]);
+ Event.observers[i][0] = null;
+ }
+ Event.observers = false;
+ },
+
+ observe: function(element, name, observer, useCapture) {
+ element = $(element);
+ useCapture = useCapture || false;
+
+ if (name == 'keypress' &&
+ (Prototype.Browser.WebKit || element.attachEvent))
+ name = 'keydown';
+
+ Event._observeAndCache(element, name, observer, useCapture);
+ },
+
+ stopObserving: function(element, name, observer, useCapture) {
+ element = $(element);
+ useCapture = useCapture || false;
+
+ if (name == 'keypress' &&
+ (Prototype.Browser.WebKit || element.attachEvent))
+ name = 'keydown';
+
+ if (element.removeEventListener) {
+ element.removeEventListener(name, observer, useCapture);
+ } else if (element.detachEvent) {
+ try {
+ element.detachEvent('on' + name, observer);
+ } catch (e) {}
+ }
+ }
+});
+
+/* prevent memory leaks in IE */
+if (Prototype.Browser.IE)
+ Event.observe(window, 'unload', Event.unloadCache, false);
+var Position = {
+ // set to true if needed, warning: firefox performance problems
+ // NOT neeeded for page scrolling, only if draggable contained in
+ // scrollable elements
+ includeScrollOffsets: false,
+
+ // must be called before calling withinIncludingScrolloffset, every time the
+ // page is scrolled
+ prepare: function() {
+ this.deltaX = window.pageXOffset
+ || document.documentElement.scrollLeft
+ || document.body.scrollLeft
+ || 0;
+ this.deltaY = window.pageYOffset
+ || document.documentElement.scrollTop
+ || document.body.scrollTop
+ || 0;
+ },
+
+ realOffset: function(element) {
+ var valueT = 0, valueL = 0;
+ do {
+ valueT += element.scrollTop || 0;
+ valueL += element.scrollLeft || 0;
+ element = element.parentNode;
+ } while (element);
+ return [valueL, valueT];
+ },
+
+ cumulativeOffset: function(element) {
+ var valueT = 0, valueL = 0;
+ do {
+ valueT += element.offsetTop || 0;
+ valueL += element.offsetLeft || 0;
+ element = element.offsetParent;
+ } while (element);
+ return [valueL, valueT];
+ },
+
+ positionedOffset: function(element) {
+ var valueT = 0, valueL = 0;
+ do {
+ valueT += element.offsetTop || 0;
+ valueL += element.offsetLeft || 0;
+ element = element.offsetParent;
+ if (element) {
+ if(element.tagName=='BODY') break;
+ var p = Element.getStyle(element, 'position');
+ if (p == 'relative' || p == 'absolute') break;
+ }
+ } while (element);
+ return [valueL, valueT];
+ },
+
+ offsetParent: function(element) {
+ if (element.offsetParent) return element.offsetParent;
+ if (element == document.body) return element;
+
+ while ((element = element.parentNode) && element != document.body)
+ if (Element.getStyle(element, 'position') != 'static')
+ return element;
+
+ return document.body;
+ },
+
+ // caches x/y coordinate pair to use with overlap
+ within: function(element, x, y) {
+ if (this.includeScrollOffsets)
+ return this.withinIncludingScrolloffsets(element, x, y);
+ this.xcomp = x;
+ this.ycomp = y;
+ this.offset = this.cumulativeOffset(element);
+
+ return (y >= this.offset[1] &&
+ y < this.offset[1] + element.offsetHeight &&
+ x >= this.offset[0] &&
+ x < this.offset[0] + element.offsetWidth);
+ },
+
+ withinIncludingScrolloffsets: function(element, x, y) {
+ var offsetcache = this.realOffset(element);
+
+ this.xcomp = x + offsetcache[0] - this.deltaX;
+ this.ycomp = y + offsetcache[1] - this.deltaY;
+ this.offset = this.cumulativeOffset(element);
+
+ return (this.ycomp >= this.offset[1] &&
+ this.ycomp < this.offset[1] + element.offsetHeight &&
+ this.xcomp >= this.offset[0] &&
+ this.xcomp < this.offset[0] + element.offsetWidth);
+ },
+
+ // within must be called directly before
+ overlap: function(mode, element) {
+ if (!mode) return 0;
+ if (mode == 'vertical')
+ return ((this.offset[1] + element.offsetHeight) - this.ycomp) /
+ element.offsetHeight;
+ if (mode == 'horizontal')
+ return ((this.offset[0] + element.offsetWidth) - this.xcomp) /
+ element.offsetWidth;
+ },
+
+ page: function(forElement) {
+ var valueT = 0, valueL = 0;
+
+ var element = forElement;
+ do {
+ valueT += element.offsetTop || 0;
+ valueL += element.offsetLeft || 0;
+
+ // Safari fix
+ if (element.offsetParent == document.body)
+ if (Element.getStyle(element,'position')=='absolute') break;
+
+ } while (element = element.offsetParent);
+
+ element = forElement;
+ do {
+ if (!window.opera || element.tagName=='BODY') {
+ valueT -= element.scrollTop || 0;
+ valueL -= element.scrollLeft || 0;
+ }
+ } while (element = element.parentNode);
+
+ return [valueL, valueT];
+ },
+
+ clone: function(source, target) {
+ var options = Object.extend({
+ setLeft: true,
+ setTop: true,
+ setWidth: true,
+ setHeight: true,
+ offsetTop: 0,
+ offsetLeft: 0
+ }, arguments[2] || {})
+
+ // find page position of source
+ source = $(source);
+ var p = Position.page(source);
+
+ // find coordinate system to use
+ target = $(target);
+ var delta = [0, 0];
+ var parent = null;
+ // delta [0,0] will do fine with position: fixed elements,
+ // position:absolute needs offsetParent deltas
+ if (Element.getStyle(target,'position') == 'absolute') {
+ parent = Position.offsetParent(target);
+ delta = Position.page(parent);
+ }
+
+ // correct by body offsets (fixes Safari)
+ if (parent == document.body) {
+ delta[0] -= document.body.offsetLeft;
+ delta[1] -= document.body.offsetTop;
+ }
+
+ // set position
+ if(options.setLeft) target.style.left = (p[0] - delta[0] + options.offsetLeft) + 'px';
+ if(options.setTop) target.style.top = (p[1] - delta[1] + options.offsetTop) + 'px';
+ if(options.setWidth) target.style.width = source.offsetWidth + 'px';
+ if(options.setHeight) target.style.height = source.offsetHeight + 'px';
+ },
+
+ absolutize: function(element) {
+ element = $(element);
+ if (element.style.position == 'absolute') return;
+ Position.prepare();
+
+ var offsets = Position.positionedOffset(element);
+ var top = offsets[1];
+ var left = offsets[0];
+ var width = element.clientWidth;
+ var height = element.clientHeight;
+
+ element._originalLeft = left - parseFloat(element.style.left || 0);
+ element._originalTop = top - parseFloat(element.style.top || 0);
+ element._originalWidth = element.style.width;
+ element._originalHeight = element.style.height;
+
+ element.style.position = 'absolute';
+ element.style.top = top + 'px';
+ element.style.left = left + 'px';
+ element.style.width = width + 'px';
+ element.style.height = height + 'px';
+ },
+
+ relativize: function(element) {
+ element = $(element);
+ if (element.style.position == 'relative') return;
+ Position.prepare();
+
+ element.style.position = 'relative';
+ var top = parseFloat(element.style.top || 0) - (element._originalTop || 0);
+ var left = parseFloat(element.style.left || 0) - (element._originalLeft || 0);
+
+ element.style.top = top + 'px';
+ element.style.left = left + 'px';
+ element.style.height = element._originalHeight;
+ element.style.width = element._originalWidth;
+ }
+}
+
+// Safari returns margins on body which is incorrect if the child is absolutely
+// positioned. For performance reasons, redefine Position.cumulativeOffset for
+// KHTML/WebKit only.
+if (Prototype.Browser.WebKit) {
+ Position.cumulativeOffset = function(element) {
+ var valueT = 0, valueL = 0;
+ do {
+ valueT += element.offsetTop || 0;
+ valueL += element.offsetLeft || 0;
+ if (element.offsetParent == document.body)
+ if (Element.getStyle(element, 'position') == 'absolute') break;
+
+ element = element.offsetParent;
+ } while (element);
+
+ return [valueL, valueT];
+ }
+}
+
+Element.addMethods(); \ No newline at end of file
diff --git a/ipa-server/ipa-gui/ipagui/static/javascript/scriptaculous.js b/ipa-server/ipa-gui/ipagui/static/javascript/scriptaculous.js
new file mode 100644
index 00000000..7c472a60
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/static/javascript/scriptaculous.js
@@ -0,0 +1,58 @@
+// script.aculo.us scriptaculous.js v1.7.1_beta3, Fri May 25 17:19:41 +0200 2007
+
+// Copyright (c) 2005-2007 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
+//
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+// For details, see the script.aculo.us web site: http://script.aculo.us/
+
+var Scriptaculous = {
+ Version: '1.7.1_beta3',
+ require: function(libraryName) {
+ // inserting via DOM fails in Safari 2.0, so brute force approach
+ document.write('<script type="text/javascript" src="'+libraryName+'"></script>');
+ },
+ REQUIRED_PROTOTYPE: '1.5.1',
+ load: function() {
+ function convertVersionString(versionString){
+ var r = versionString.split('.');
+ return parseInt(r[0])*100000 + parseInt(r[1])*1000 + parseInt(r[2]);
+ }
+
+ if((typeof Prototype=='undefined') ||
+ (typeof Element == 'undefined') ||
+ (typeof Element.Methods=='undefined') ||
+ (convertVersionString(Prototype.Version) <
+ convertVersionString(Scriptaculous.REQUIRED_PROTOTYPE)))
+ throw("script.aculo.us requires the Prototype JavaScript framework >= " +
+ Scriptaculous.REQUIRED_PROTOTYPE);
+
+ $A(document.getElementsByTagName("script")).findAll( function(s) {
+ return (s.src && s.src.match(/scriptaculous\.js(\?.*)?$/))
+ }).each( function(s) {
+ var path = s.src.replace(/scriptaculous\.js(\?.*)?$/,'');
+ var includes = s.src.match(/\?.*load=([a-z,]*)/);
+ (includes ? includes[1] : 'builder,effects,dragdrop,controls,slider,sound').split(',').each(
+ function(include) { Scriptaculous.require(path+include+'.js') });
+ });
+ }
+}
+
+Scriptaculous.load(); \ No newline at end of file
diff --git a/ipa-server/ipa-gui/ipagui/static/javascript/tablekit.js b/ipa-server/ipa-gui/ipagui/static/javascript/tablekit.js
new file mode 100644
index 00000000..54036948
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/static/javascript/tablekit.js
@@ -0,0 +1,848 @@
+/*
+*
+* Copyright (c) 2007 Andrew Tetlaw & Millstream Web Software
+* http://www.millstream.com.au/view/code/tablekit/
+* Version: 1.2.1 2007-03-11
+*
+* Permission is hereby granted, free of charge, to any person
+* obtaining a copy of this software and associated documentation
+* files (the "Software"), to deal in the Software without
+* restriction, including without limitation the rights to use, copy,
+* modify, merge, publish, distribute, sublicense, and/or sell copies
+* of the Software, and to permit persons to whom the Software is
+* furnished to do so, subject to the following conditions:
+*
+* The above copyright notice and this permission notice shall be
+* included in all copies or substantial portions of the Software.
+*
+* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+* SOFTWARE.
+* *
+*/
+
+// Use the TableKit class constructure if you'd prefer to init your tables as JS objects
+var TableKit = Class.create();
+
+TableKit.prototype = {
+ initialize : function(elm, options) {
+ var table = $(elm);
+ if(table.tagName !== "TABLE") {
+ return;
+ }
+ TableKit.register(table,Object.extend(TableKit.options,options || {}));
+ this.id = table.id;
+ var op = TableKit.option('sortable resizable editable', this.id);
+ if(op.sortable) {
+ TableKit.Sortable.init(table);
+ }
+ if(op.resizable) {
+ TableKit.Resizable.init(table);
+ }
+ if(op.editable) {
+ TableKit.Editable.init(table);
+ }
+ },
+ sort : function(column, order) {
+ TableKit.Sortable.sort(this.id, column, order);
+ },
+ resizeColumn : function(column, w) {
+ TableKit.Resizable.resize(this.id, column, w);
+ },
+ editCell : function(row, column) {
+ TableKit.Editable.editCell(this.id, row, column);
+ }
+};
+
+Object.extend(TableKit, {
+ getBodyRows : function(table) {
+ table = $(table);
+ var id = table.id;
+ if(!TableKit.rows[id]) {
+ TableKit.rows[id] = (table.tHead && table.tHead.rows.length > 0) ? $A(table.tBodies[0].rows) : $A(table.rows).without(table.rows[0]);
+ }
+ return TableKit.rows[id];
+ },
+ getHeaderCells : function(table, cell) {
+ if(!table) { table = $(cell).up('table'); }
+ var id = table.id;
+ if(!TableKit.heads[id]) {
+ TableKit.heads[id] = $A((table.tHead && table.tHead.rows.length > 0) ? table.tHead.rows[table.tHead.rows.length-1].cells : table.rows[0].cells);
+ }
+ return TableKit.heads[id];
+ },
+ getCellIndex : function(cell) {
+ return $A(cell.parentNode.cells).indexOf(cell);
+ },
+ getRowIndex : function(row) {
+ return $A(row.parentNode.rows).indexOf(row);
+ },
+ getCellText : function(cell, refresh) {
+ if(!cell) { return ""; }
+ TableKit.registerCell(cell);
+ var data = TableKit.cells[cell.id];
+ if(refresh || data.refresh || !data.textContent) {
+ data.textContent = cell.textContent ? cell.textContent : cell.innerText;
+ data.refresh = false;
+ }
+ return data.textContent;
+ },
+ register : function(table, options) {
+ if(!table.id) {
+ TableKit._tblcount += 1;
+ table.id = "tablekit-table-" + TableKit._tblcount;
+ }
+ var id = table.id;
+ TableKit.tables[id] = TableKit.tables[id] ? Object.extend(TableKit.tables[id], options || {}) : Object.extend({sortable:false,resizable:false,editable:false}, options || {});
+ },
+ registerCell : function(cell) {
+ if(!cell.id) {
+ TableKit._cellcount += 1;
+ cell.id = "tablekit-cell-" + TableKit._cellcount;
+ }
+ if(!TableKit.cells[cell.id]) {
+ TableKit.cells[cell.id] = {textContent : '', htmlContent : '', active : false};
+ }
+ },
+ isSortable : function(table) {
+ return TableKit.tables[table.id] ? TableKit.tables[table.id].sortable : false;
+ },
+ isResizable : function(table) {
+ return TableKit.tables[table.id] ? TableKit.tables[table.id].resizable : false;
+ },
+ isEditable : function(table) {
+ return TableKit.tables[table.id] ? TableKit.tables[table.id].editable : false;
+ },
+ setup : function(o) {
+ Object.extend(TableKit.options, o || {} );
+ },
+ option : function(s, id, o1, o2) {
+ o1 = o1 || TableKit.options;
+ o2 = o2 || (id ? (TableKit.tables[id] ? TableKit.tables[id] : {}) : {});
+ var key = id + s;
+ if(!TableKit._opcache[key]){
+ TableKit._opcache[key] = $A($w(s)).inject([],function(a,v){
+ a.push(a[v] = o2[v] || o1[v]);
+ return a;
+ });
+ }
+ return TableKit._opcache[key];
+ },
+ e : function(event) {
+ return event || window.event;
+ },
+ tables : {},
+ _opcache : {},
+ cells : {},
+ rows : {},
+ heads : {},
+ options : {
+ autoLoad : true,
+ stripe : true,
+ sortable : true,
+ resizable : true,
+ editable : true,
+ rowEvenClass : 'roweven',
+ rowOddClass : 'rowodd',
+ sortableSelector : ['table.sortable'],
+ columnClass : 'sortcol',
+ descendingClass : 'sortdesc',
+ ascendingClass : 'sortasc',
+ noSortClass : 'nosort',
+ sortFirstAscendingClass : 'sortfirstasc',
+ sortFirstDecendingClass : 'sortfirstdesc',
+ resizableSelector : ['table.resizable'],
+ minWidth : 10,
+ showHandle : true,
+ resizeOnHandleClass : 'resize-handle-active',
+ editableSelector : ['table.editable'],
+ formClassName : 'editable-cell-form',
+ noEditClass : 'noedit',
+ editAjaxURI : '/',
+ editAjaxOptions : {}
+ },
+ _tblcount : 0,
+ _cellcount : 0,
+ load : function() {
+ if(TableKit.options.autoLoad) {
+ if(TableKit.options.sortable) {
+ $A(TableKit.options.sortableSelector).each(function(s){
+ $$(s).each(function(t) {
+ TableKit.Sortable.init(t);
+ });
+ });
+ }
+ if(TableKit.options.resizable) {
+ $A(TableKit.options.resizableSelector).each(function(s){
+ $$(s).each(function(t) {
+ TableKit.Resizable.init(t);
+ });
+ });
+ }
+ if(TableKit.options.editable) {
+ $A(TableKit.options.editableSelector).each(function(s){
+ $$(s).each(function(t) {
+ TableKit.Editable.init(t);
+ });
+ });
+ }
+ }
+ }
+});
+
+TableKit.Rows = {
+ stripe : function(table) {
+ var rows = TableKit.getBodyRows(table);
+ rows.each(function(r,i) {
+ TableKit.Rows.addStripeClass(table,r,i);
+ });
+ },
+ addStripeClass : function(t,r,i) {
+ t = t || r.up('table');
+ var op = TableKit.option('rowEvenClass rowOddClass', t.id);
+ var css = ((i+1)%2 === 0 ? op[0] : op[1]);
+ // using prototype's assClassName/RemoveClassName was not efficient for large tables, hence:
+ var cn = r.className.split(/\s+/);
+ var newCn = [];
+ for(var x = 0, l = cn.length; x < l; x += 1) {
+ if(cn[x] !== op[0] && cn[x] !== op[1]) { newCn.push(cn[x]); }
+ }
+ newCn.push(css);
+ r.className = newCn.join(" ");
+ }
+};
+
+TableKit.Sortable = {
+ init : function(elm, options){
+ var table = $(elm);
+ if(table.tagName !== "TABLE") {
+ return;
+ }
+ TableKit.register(table,Object.extend(options || {},{sortable:true}));
+ var sortFirst;
+ var cells = TableKit.getHeaderCells(table);
+ var op = TableKit.option('noSortClass columnClass sortFirstAscendingClass sortFirstDecendingClass', table.id);
+ cells.each(function(c){
+ c = $(c);
+ if(!c.hasClassName(op.noSortClass)) {
+ Event.observe(c, 'mousedown', TableKit.Sortable._sort);
+ c.addClassName(op.columnClass);
+ if(c.hasClassName(op.sortFirstAscendingClass) || c.hasClassName(op.sortFirstDecendingClass)) {
+ sortFirst = c;
+ }
+ }
+ });
+
+ if(sortFirst) {
+ if(sortFirst.hasClassName(op.sortFirstAscendingClass)) {
+ TableKit.Sortable.sort(table, sortFirst, 1);
+ } else {
+ TableKit.Sortable.sort(table, sortFirst, -1);
+ }
+ } else { // just add row stripe classes
+ TableKit.Rows.stripe(table);
+ }
+ },
+ reload : function(table) {
+ table = $(table);
+ var cells = TableKit.getHeaderCells(table);
+ var op = TableKit.option('noSortClass columnClass', table.id);
+ cells.each(function(c){
+ c = $(c);
+ if(!c.hasClassName(op.noSortClass)) {
+ Event.stopObserving(c, 'mousedown', TableKit.Sortable._sort);
+ c.removeClassName(op.columnClass);
+ }
+ });
+ TableKit.Sortable.init(table);
+ },
+ _sort : function(e) {
+ if(TableKit.Resizable._onHandle) {return;}
+ e = TableKit.e(e);
+ Event.stop(e);
+ var cell = Event.element(e);
+ while(!(cell.tagName && cell.tagName.match(/td|th/gi))) {
+ cell = cell.parentNode;
+ }
+ TableKit.Sortable.sort(null, cell);
+ },
+ sort : function(table, index, order) {
+ var cell;
+ if(typeof index === 'number') {
+ if(!table || (table.tagName && table.tagName !== "TABLE")) {
+ return;
+ }
+ table = $(table);
+ index = Math.min(table.rows[0].cells.length, index);
+ index = Math.max(1, index);
+ index -= 1;
+ cell = (table.tHead && table.tHead.rows.length > 0) ? $(table.tHead.rows[table.tHead.rows.length-1].cells[index]) : $(table.rows[0].cells[index]);
+ } else {
+ cell = $(index);
+ table = table ? $(table) : cell.up('table');
+ index = TableKit.getCellIndex(cell);
+ }
+ var op = TableKit.option('noSortClass descendingClass ascendingClass', table.id);
+
+ if(cell.hasClassName(op.noSortClass)) {return;}
+
+ // order = order ? order : (cell.hasClassName(op.descendingClass) ? 1 : -1);
+ // kmccarth - change default sort order to ascending
+ order = order ? order : (cell.hasClassName(op.ascendingClass) ? -1 : 1);
+ var rows = TableKit.getBodyRows(table);
+
+ if(cell.hasClassName(op.ascendingClass) || cell.hasClassName(op.descendingClass)) {
+ rows.reverse(); // if it was already sorted we just need to reverse it.
+ } else {
+ var datatype = TableKit.Sortable.getDataType(cell,index,table);
+ var tkst = TableKit.Sortable.types;
+ rows.sort(function(a,b) {
+ return order * tkst[datatype].compare(TableKit.getCellText(a.cells[index]),TableKit.getCellText(b.cells[index]));
+ });
+ }
+ var tb = table.tBodies[0];
+ var tkr = TableKit.Rows;
+ rows.each(function(r,i) {
+ tb.appendChild(r);
+ tkr.addStripeClass(table,r,i);
+ });
+ var hcells = TableKit.getHeaderCells(null, cell);
+ $A(hcells).each(function(c,i){
+ c = $(c);
+ c.removeClassName(op.ascendingClass);
+ c.removeClassName(op.descendingClass);
+ if(index === i) {
+ if(order === 1) {
+ c.removeClassName(op.descendingClass);
+ c.addClassName(op.ascendingClass);
+ } else {
+ c.removeClassName(op.ascendingClass);
+ c.addClassName(op.descendingClass);
+ }
+ }
+ });
+ },
+ types : {},
+ detectors : [],
+ addSortType : function() {
+ $A(arguments).each(function(o){
+ TableKit.Sortable.types[o.name] = o;
+ });
+ },
+ getDataType : function(cell,index,table) {
+ cell = $(cell);
+ index = (index || index === 0) ? index : TableKit.getCellIndex(cell);
+
+ var colcache = TableKit.Sortable._coltypecache;
+ var cache = colcache[table.id] ? colcache[table.id] : (colcache[table.id] = {});
+
+ if(!cache[index]) {
+ var t = '';
+ // first look for a data type id on the heading row cell
+ if(cell.id && TableKit.Sortable.types[cell.id]) {
+ t = cell.id;
+ }
+ t = cell.classNames().detect(function(n){ // then look for a data type classname on the heading row cell
+ return (TableKit.Sortable.types[n]) ? true : false;
+ });
+ if(!t) {
+ var rows = TableKit.getBodyRows(table);
+ cell = rows[0].cells[index]; // grab same index cell from body row to try and match data type
+ t = TableKit.Sortable.detectors.detect(
+ function(d){
+ return TableKit.Sortable.types[d].detect(TableKit.getCellText(cell));
+ });
+ }
+ cache[index] = t;
+ }
+ return cache[index];
+ },
+ _coltypecache : {}
+};
+
+TableKit.Sortable.detectors = $A($w('date-iso date date-eu date-au time currency datasize number casesensitivetext text')); // setting it here because Safari complained when I did it above...
+
+TableKit.Sortable.Type = Class.create();
+TableKit.Sortable.Type.prototype = {
+ initialize : function(name, options){
+ this.name = name;
+ options = Object.extend({
+ normal : function(v){
+ return v;
+ },
+ pattern : /.*/
+ }, options || {});
+ this.normal = options.normal;
+ this.pattern = options.pattern;
+ if(options.compare) {
+ this.compare = options.compare;
+ }
+ if(options.detect) {
+ this.detect = options.detect;
+ }
+ },
+ compare : function(a,b){
+ return TableKit.Sortable.Type.compare(this.normal(a), this.normal(b));
+ },
+ detect : function(v){
+ return this.pattern.test(v);
+ }
+};
+
+TableKit.Sortable.Type.compare = function(a,b) {
+ return a < b ? -1 : a === b ? 0 : 1;
+};
+
+TableKit.Sortable.addSortType(
+ new TableKit.Sortable.Type('number', {
+ pattern : /^[-+]?[\d]*\.?[\d]+(?:[eE][-+]?[\d]+)?/,
+ normal : function(v) {
+ // This will grab the first thing that looks like a number from a string, so you can use it to order a column of various srings containing numbers.
+ v = parseFloat(v.replace(/^.*?([-+]?[\d]*\.?[\d]+(?:[eE][-+]?[\d]+)?).*$/,"$1"));
+ return isNaN(v) ? 0 : v;
+ }}),
+ new TableKit.Sortable.Type('text',{
+ normal : function(v) {
+ return v ? v.toLowerCase() : '';
+ }}),
+ new TableKit.Sortable.Type('casesensitivetext',{pattern : /^[A-Z]+$/}),
+ new TableKit.Sortable.Type('datasize',{
+ pattern : /^[-+]?[\d]*\.?[\d]+(?:[eE][-+]?[\d]+)?\s?[k|m|g|t]b$/i,
+ normal : function(v) {
+ var r = v.match(/^([-+]?[\d]*\.?[\d]+([eE][-+]?[\d]+)?)\s?([k|m|g|t]?b)?/i);
+ var b = r[1] ? Number(r[1]).valueOf() : 0;
+ var m = r[3] ? r[3].substr(0,1).toLowerCase() : '';
+ var result = b;
+ switch(m) {
+ case 'k':
+ result = b * 1024;
+ break;
+ case 'm':
+ result = b * 1024 * 1024;
+ break;
+ case 'g':
+ result = b * 1024 * 1024 * 1024;
+ break;
+ case 't':
+ result = b * 1024 * 1024 * 1024 * 1024;
+ break;
+ }
+ return result;
+ }}),
+ new TableKit.Sortable.Type('date-au',{
+ pattern : /^\d{2}\/\d{2}\/\d{4}\s?(?:\d{1,2}\:\d{2}(?:\:\d{2})?\s?[a|p]?m?)?/i,
+ normal : function(v) {
+ if(!this.pattern.test(v)) {return 0;}
+ var r = v.match(/^(\d{2})\/(\d{2})\/(\d{4})\s?(?:(\d{1,2})\:(\d{2})(?:\:(\d{2}))?\s?([a|p]?m?))?/i);
+ var yr_num = r[3];
+ var mo_num = parseInt(r[2],10)-1;
+ var day_num = r[1];
+ var hr_num = r[4] ? r[4] : 0;
+ if(r[7] && r[7].toLowerCase().indexOf('p') !== -1) {
+ hr_num = parseInt(r[4],10) + 12;
+ }
+ var min_num = r[5] ? r[5] : 0;
+ var sec_num = r[6] ? r[6] : 0;
+ return new Date(yr_num, mo_num, day_num, hr_num, min_num, sec_num, 0).valueOf();
+ }}),
+ new TableKit.Sortable.Type('date-us',{
+ pattern : /^\d{2}\/\d{2}\/\d{4}\s?(?:\d{1,2}\:\d{2}(?:\:\d{2})?\s?[a|p]?m?)?/i,
+ normal : function(v) {
+ if(!this.pattern.test(v)) {return 0;}
+ var r = v.match(/^(\d{2})\/(\d{2})\/(\d{4})\s?(?:(\d{1,2})\:(\d{2})(?:\:(\d{2}))?\s?([a|p]?m?))?/i);
+ var yr_num = r[3];
+ var mo_num = parseInt(r[1],10)-1;
+ var day_num = r[2];
+ var hr_num = r[4] ? r[4] : 0;
+ if(r[7] && r[7].toLowerCase().indexOf('p') !== -1) {
+ hr_num = parseInt(r[4],10) + 12;
+ }
+ var min_num = r[5] ? r[5] : 0;
+ var sec_num = r[6] ? r[6] : 0;
+ return new Date(yr_num, mo_num, day_num, hr_num, min_num, sec_num, 0).valueOf();
+ }}),
+ new TableKit.Sortable.Type('date-eu',{
+ pattern : /^\d{2}-\d{2}-\d{4}/i,
+ normal : function(v) {
+ if(!this.pattern.test(v)) {return 0;}
+ var r = v.match(/^(\d{2})-(\d{2})-(\d{4})/);
+ var yr_num = r[3];
+ var mo_num = parseInt(r[2],10)-1;
+ var day_num = r[1];
+ return new Date(yr_num, mo_num, day_num).valueOf();
+ }}),
+ new TableKit.Sortable.Type('date-iso',{
+ pattern : /[\d]{4}-[\d]{2}-[\d]{2}(?:T[\d]{2}\:[\d]{2}(?:\:[\d]{2}(?:\.[\d]+)?)?(Z|([-+][\d]{2}:[\d]{2})?)?)?/, // 2005-03-26T19:51:34Z
+ normal : function(v) {
+ if(!this.pattern.test(v)) {return 0;}
+ var d = v.match(/([\d]{4})(-([\d]{2})(-([\d]{2})(T([\d]{2}):([\d]{2})(:([\d]{2})(\.([\d]+))?)?(Z|(([-+])([\d]{2}):([\d]{2})))?)?)?)?/);
+ var offset = 0;
+ var date = new Date(d[1], 0, 1);
+ if (d[3]) { date.setMonth(d[3] - 1) ;}
+ if (d[5]) { date.setDate(d[5]); }
+ if (d[7]) { date.setHours(d[7]); }
+ if (d[8]) { date.setMinutes(d[8]); }
+ if (d[10]) { date.setSeconds(d[10]); }
+ if (d[12]) { date.setMilliseconds(Number("0." + d[12]) * 1000); }
+ if (d[14]) {
+ offset = (Number(d[16]) * 60) + Number(d[17]);
+ offset *= ((d[15] === '-') ? 1 : -1);
+ }
+ offset -= date.getTimezoneOffset();
+ if(offset !== 0) {
+ var time = (Number(date) + (offset * 60 * 1000));
+ date.setTime(Number(time));
+ }
+ return date.valueOf();
+ }}),
+ new TableKit.Sortable.Type('date',{
+ pattern: /^(?:sun|mon|tue|wed|thu|fri|sat)\,\s\d{1,2}\s(?:jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)\s\d{4}(?:\s\d{2}\:\d{2}(?:\:\d{2})?(?:\sGMT(?:[+-]\d{4})?)?)?/i, //Mon, 18 Dec 1995 17:28:35 GMT
+ compare : function(a,b) { // must be standard javascript date format
+ if(a && b) {
+ return TableKit.Sortable.Type.compare(new Date(a),new Date(b));
+ } else {
+ return TableKit.Sortable.Type.compare(a ? 1 : 0, b ? 1 : 0);
+ }
+ }}),
+ new TableKit.Sortable.Type('time',{
+ pattern : /^\d{1,2}\:\d{2}(?:\:\d{2})?(?:\s[a|p]m)?$/i,
+ compare : function(a,b) {
+ var d = new Date();
+ var ds = d.getMonth() + "/" + d.getDate() + "/" + d.getFullYear() + " ";
+ return TableKit.Sortable.Type.compare(new Date(ds + a),new Date(ds + b));
+ }}),
+ new TableKit.Sortable.Type('currency',{
+ pattern : /^[$£¥€¤]/, // dollar,pound,yen,euro,generic currency symbol
+ normal : function(v) {
+ return v ? parseFloat(v.replace(/[^-\d\.]/g,'')) : 0;
+ }})
+);
+
+TableKit.Resizable = {
+ init : function(elm, options){
+ var table = $(elm);
+ if(table.tagName !== "TABLE") {return;}
+ TableKit.register(table,Object.extend(options || {},{resizable:true}));
+ var cells = TableKit.getHeaderCells(table);
+ cells.each(function(c){
+ c = $(c);
+ Event.observe(c, 'mouseover', TableKit.Resizable.initDetect);
+ Event.observe(c, 'mouseout', TableKit.Resizable.killDetect);
+ });
+ },
+ resize : function(table, index, w) {
+ var cell;
+ if(typeof index === 'number') {
+ if(!table || (table.tagName && table.tagName !== "TABLE")) {return;}
+ table = $(table);
+ index = Math.min(table.rows[0].cells.length, index);
+ index = Math.max(1, index);
+ index -= 1;
+ cell = (table.tHead && table.tHead.rows.length > 0) ? $(table.tHead.rows[table.tHead.rows.length-1].cells[index]) : $(table.rows[0].cells[index]);
+ } else {
+ cell = $(index);
+ table = table ? $(table) : cell.up('table');
+ index = TableKit.getCellIndex(cell);
+ }
+ var pad = parseInt(cell.getStyle('paddingLeft'),10) + parseInt(cell.getStyle('paddingRight'),10);
+ w = Math.max(w-pad, TableKit.option('minWidth', table.id)[0]);
+
+ cell.setStyle({'width' : w + 'px'});
+ },
+ initDetect : function(e) {
+ e = TableKit.e(e);
+ var cell = Event.element(e);
+ Event.observe(cell, 'mousemove', TableKit.Resizable.detectHandle);
+ Event.observe(cell, 'mousedown', TableKit.Resizable.startResize);
+ },
+ detectHandle : function(e) {
+ e = TableKit.e(e);
+ var cell = Event.element(e);
+ if(TableKit.Resizable.pointerPos(cell,Event.pointerX(e),Event.pointerY(e))){
+ cell.addClassName(TableKit.option('resizeOnHandleClass', cell.up('table').id)[0]);
+ TableKit.Resizable._onHandle = true;
+ } else {
+ cell.removeClassName(TableKit.option('resizeOnHandleClass', cell.up('table').id)[0]);
+ TableKit.Resizable._onHandle = false;
+ }
+ },
+ killDetect : function(e) {
+ e = TableKit.e(e);
+ TableKit.Resizable._onHandle = false;
+ var cell = Event.element(e);
+ Event.stopObserving(cell, 'mousemove', TableKit.Resizable.detectHandle);
+ Event.stopObserving(cell, 'mousedown', TableKit.Resizable.startResize);
+ cell.removeClassName(TableKit.option('resizeOnHandleClass', cell.up('table').id)[0]);
+ },
+ startResize : function(e) {
+ e = TableKit.e(e);
+ if(!TableKit.Resizable._onHandle) {return;}
+ var cell = Event.element(e);
+ Event.stopObserving(cell, 'mousemove', TableKit.Resizable.detectHandle);
+ Event.stopObserving(cell, 'mousedown', TableKit.Resizable.startResize);
+ Event.stopObserving(cell, 'mouseout', TableKit.Resizable.killDetect);
+ TableKit.Resizable._cell = cell;
+ var table = cell.up('table');
+ TableKit.Resizable._tbl = table;
+ if(TableKit.option('showHandle', table.id)[0]) {
+ TableKit.Resizable._handle = $(document.createElement('div')).addClassName('resize-handle').setStyle({
+ 'top' : Position.cumulativeOffset(cell)[1] + 'px',
+ 'left' : Event.pointerX(e) + 'px',
+ 'height' : table.getDimensions().height + 'px'
+ });
+ document.body.appendChild(TableKit.Resizable._handle);
+ }
+ Event.observe(document, 'mousemove', TableKit.Resizable.drag);
+ Event.observe(document, 'mouseup', TableKit.Resizable.endResize);
+ Event.stop(e);
+ },
+ endResize : function(e) {
+ e = TableKit.e(e);
+ var cell = TableKit.Resizable._cell;
+ TableKit.Resizable.resize(null, cell, (Event.pointerX(e) - Position.cumulativeOffset(cell)[0]));
+ Event.stopObserving(document, 'mousemove', TableKit.Resizable.drag);
+ Event.stopObserving(document, 'mouseup', TableKit.Resizable.endResize);
+ if(TableKit.option('showHandle', TableKit.Resizable._tbl.id)[0]) {
+ $$('div.resize-handle').each(function(elm){
+ document.body.removeChild(elm);
+ });
+ }
+ Event.observe(cell, 'mouseout', TableKit.Resizable.killDetect);
+ TableKit.Resizable._tbl = TableKit.Resizable._handle = TableKit.Resizable._cell = null;
+ Event.stop(e);
+ },
+ drag : function(e) {
+ e = TableKit.e(e);
+ if(TableKit.Resizable._handle === null) {
+ try {
+ TableKit.Resizable.resize(TableKit.Resizable._tbl, TableKit.Resizable._cell, (Event.pointerX(e) - Position.cumulativeOffset(TableKit.Resizable._cell)[0]));
+ } catch(e) {}
+ } else {
+ TableKit.Resizable._handle.setStyle({'left' : Event.pointerX(e) + 'px'});
+ }
+ return false;
+ },
+ pointerPos : function(element, x, y) {
+ var offset = Position.cumulativeOffset(element);
+ return (y >= offset[1] &&
+ y < offset[1] + element.offsetHeight &&
+ x >= offset[0] + element.offsetWidth - 5 &&
+ x < offset[0] + element.offsetWidth);
+ },
+ _onHandle : false,
+ _cell : null,
+ _tbl : null,
+ _handle : null
+};
+
+
+TableKit.Editable = {
+ init : function(elm, options){
+ var table = $(elm);
+ if(table.tagName !== "TABLE") {return;}
+ TableKit.register(table,Object.extend(options || {},{editable:true}));
+ Event.observe(table.tBodies[0], 'click', TableKit.Editable._editCell);
+ },
+ _editCell : function(e) {
+ e = TableKit.e(e);
+ var cell = Event.findElement(e,'td');
+ TableKit.Editable.editCell(null, cell);
+ },
+ editCell : function(table, index, cindex) {
+ var cell, row;
+ if(typeof index === 'number') {
+ if(!table || (table.tagName && table.tagName !== "TABLE")) {return;}
+ table = $(table);
+ index = Math.min(table.tBodies[0].rows.length, index);
+ index = Math.max(1, index);
+ index -= 1;
+ cindex = Math.min(table.rows[0].cells.length, cindex);
+ cindex = Math.max(1, cindex);
+ cindex -= 1;
+ row = $(table.tBodies[0].rows[index]);
+ cell = $(row.cells[cindex]);
+ } else {
+ cell = $(index);
+ table = (table && table.tagName && table.tagName !== "TABLE") ? $(table) : cell.up('table');
+ row = cell.up('tr');
+ }
+ var op = TableKit.option('noEditClass', table.id);
+ if(cell.hasClassName(op.noEditClass)) {return;}
+
+ var head = $(TableKit.getHeaderCells(table, cell)[TableKit.getCellIndex(cell)]);
+ if(head.hasClassName(op.noEditClass)) {return;}
+
+ TableKit.registerCell(cell);
+ var data = TableKit.cells[cell.id];
+ if(data.active) {return;}
+ data.htmlContent = cell.innerHTML;
+ var ftype = TableKit.Editable.types['text-input'];
+ if(head.id && TableKit.Editable.types[head.id]) {
+ ftype = TableKit.Editable.types[head.id];
+ } else {
+ var n = head.classNames().detect(function(n){
+ return (TableKit.Editable.types[n]) ? true : false;
+ });
+ ftype = n ? TableKit.Editable.types[n] : ftype;
+ }
+ ftype.edit(cell);
+ data.active = true;
+ },
+ types : {},
+ addCellEditor : function(o) {
+ if(o && o.name) { TableKit.Editable.types[o.name] = o; }
+ }
+};
+
+TableKit.Editable.CellEditor = Class.create();
+TableKit.Editable.CellEditor.prototype = {
+ initialize : function(name, options){
+ this.name = name;
+ this.options = Object.extend({
+ element : 'input',
+ attributes : {name : 'value', type : 'text'},
+ selectOptions : [],
+ showSubmit : true,
+ submitText : 'OK',
+ showCancel : true,
+ cancelText : 'Cancel',
+ ajaxURI : null,
+ ajaxOptions : null
+ }, options || {});
+ },
+ edit : function(cell) {
+ cell = $(cell);
+ var op = this.options;
+ var table = cell.up('table');
+
+ var form = $(document.createElement("form"));
+ form.id = cell.id + '-form';
+ form.addClassName(TableKit.option('formClassName', table.id)[0]);
+ form.onsubmit = this._submit.bindAsEventListener(this);
+
+ var field = document.createElement(op.element);
+ $H(op.attributes).each(function(v){
+ field[v.key] = v.value;
+ });
+ switch(op.element) {
+ case 'input':
+ case 'textarea':
+ field.value = TableKit.getCellText(cell);
+ break;
+
+ case 'select':
+ var txt = TableKit.getCellText(cell);
+ $A(op.selectOptions).each(function(v){
+ field.options[field.options.length] = new Option(v[0], v[1]);
+ if(txt === v[1]) {
+ field.options[field.options.length-1].selected = 'selected';
+ }
+ });
+ break;
+ }
+ form.appendChild(field);
+ if(op.element === 'textarea') {
+ form.appendChild(document.createElement("br"));
+ }
+ if(op.showSubmit) {
+ var okButton = document.createElement("input");
+ okButton.type = "submit";
+ okButton.value = op.submitText;
+ okButton.className = 'editor_ok_button';
+ form.appendChild(okButton);
+ }
+ if(op.showCancel) {
+ var cancelLink = document.createElement("a");
+ cancelLink.href = "#";
+ cancelLink.appendChild(document.createTextNode(op.cancelText));
+ cancelLink.onclick = this._cancel.bindAsEventListener(this);
+ cancelLink.className = 'editor_cancel';
+ form.appendChild(cancelLink);
+ }
+ cell.innerHTML = '';
+ cell.appendChild(form);
+ },
+ _submit : function(e) {
+ var cell = Event.findElement(e,'td');
+ var form = Event.findElement(e,'form');
+ Event.stop(e);
+ this.submit(cell,form);
+ },
+ submit : function(cell, form) {
+ var op = this.options;
+ form = form ? form : cell.down('form');
+ var head = $(TableKit.getHeaderCells(null, cell)[TableKit.getCellIndex(cell)]);
+ var row = cell.up('tr');
+ var table = cell.up('table');
+ var s = '&row=' + (TableKit.getRowIndex(row)+1) + '&cell=' + (TableKit.getCellIndex(cell)+1) + '&id=' + row.id + '&field=' + head.id + '&' + Form.serialize(form);
+ this.ajax = new Ajax.Updater(cell, op.ajaxURI || TableKit.option('editAjaxURI', table.id)[0], Object.extend(op.ajaxOptions || TableKit.option('editAjaxOptions', table.id)[0], {
+ postBody : s,
+ onComplete : function() {
+ var data = TableKit.cells[cell.id];
+ data.active = false;
+ data.refresh = true; // mark cell cache for refreshing, in case cell contents has changed and sorting is applied
+ }
+ }));
+ },
+ _cancel : function(e) {
+ var cell = Event.findElement(e,'td');
+ Event.stop(e);
+ this.cancel(cell);
+ },
+ cancel : function(cell) {
+ this.ajax = null;
+ var data = TableKit.cells[cell.id];
+ cell.innerHTML = data.htmlContent;
+ data.htmlContent = '';
+ data.active = false;
+ },
+ ajax : null
+};
+
+TableKit.Editable.textInput = function(n,attributes) {
+ TableKit.Editable.addCellEditor(new TableKit.Editable.CellEditor(n, {
+ element : 'input',
+ attributes : Object.extend({name : 'value', type : 'text'}, attributes||{})
+ }));
+};
+TableKit.Editable.textInput('text-input');
+
+TableKit.Editable.multiLineInput = function(n,attributes) {
+ TableKit.Editable.addCellEditor(new TableKit.Editable.CellEditor(n, {
+ element : 'textarea',
+ attributes : Object.extend({name : 'value', rows : '5', cols : '20'}, attributes||{})
+ }));
+};
+TableKit.Editable.multiLineInput('multi-line-input');
+
+TableKit.Editable.selectInput = function(n,attributes,selectOptions) {
+ TableKit.Editable.addCellEditor(new TableKit.Editable.CellEditor(n, {
+ element : 'select',
+ attributes : Object.extend({name : 'value'}, attributes||{}),
+ 'selectOptions' : selectOptions
+ }));
+};
+
+/*
+TableKit.Bench = {
+ bench : [],
+ start : function(){
+ TableKit.Bench.bench[0] = new Date().getTime();
+ },
+ end : function(s){
+ TableKit.Bench.bench[1] = new Date().getTime();
+ alert(s + ' ' + ((TableKit.Bench.bench[1]-TableKit.Bench.bench[0])/1000)+' seconds.') //console.log(s + ' ' + ((TableKit.Bench.bench[1]-TableKit.Bench.bench[0])/1000)+' seconds.')
+ TableKit.Bench.bench = [];
+ }
+} */
+
+if(window.FastInit) {
+ FastInit.addOnLoad(TableKit.load);
+} else {
+ Event.observe(window, 'load', TableKit.load);
+}
diff --git a/ipa-server/ipa-gui/ipagui/subcontrollers/Makefile.am b/ipa-server/ipa-gui/ipagui/subcontrollers/Makefile.am
new file mode 100644
index 00000000..4a7ff58d
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/subcontrollers/Makefile.am
@@ -0,0 +1,21 @@
+NULL =
+
+appdir = $(IPA_DATA_DIR)/ipagui/subcontrollers
+app_PYTHON = \
+ __init__.py \
+ group.py \
+ ipacontroller.py \
+ ipapolicy.py \
+ policy.py \
+ user.py \
+ delegation.py \
+ principal.py \
+ $(NULL)
+
+EXTRA_DIST = \
+ $(NULL)
+
+MAINTAINERCLEANFILES = \
+ *~ \
+ *.pyc \
+ Makefile.in
diff --git a/ipa-server/ipa-gui/ipagui/subcontrollers/__init__.py b/ipa-server/ipa-gui/ipagui/subcontrollers/__init__.py
new file mode 100644
index 00000000..143f486c
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/subcontrollers/__init__.py
@@ -0,0 +1 @@
+# __init__.py
diff --git a/ipa-server/ipa-gui/ipagui/subcontrollers/delegation.py b/ipa-server/ipa-gui/ipagui/subcontrollers/delegation.py
new file mode 100644
index 00000000..3f80da52
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/subcontrollers/delegation.py
@@ -0,0 +1,415 @@
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+import os
+from pickle import dumps, loads
+from base64 import b64encode, b64decode
+import copy
+import logging
+
+import cherrypy
+import turbogears
+from turbogears import controllers, expose, flash
+from turbogears import validators, validate
+from turbogears import widgets, paginate
+from turbogears import error_handler
+from turbogears import identity
+
+from ipacontroller import IPAController
+from ipa.entity import utf8_encode_values
+from ipa import ipaerror
+import ipagui.forms.delegate
+import ipa.aci
+
+import ldap.dn
+import operator
+
+log = logging.getLogger(__name__)
+
+aci_fields = ['*', 'aci']
+
+delegate_form = ipagui.forms.delegate.DelegateForm()
+
+class DelegationController(IPAController):
+
+ @expose()
+ @identity.require(identity.not_anonymous())
+ def index(self, tg_errors=None):
+ raise turbogears.redirect("/delegate/list")
+
+ @expose("ipagui.templates.delegatenew")
+ @identity.require(identity.in_group("admins"))
+ def new(self):
+ """Display delegate page"""
+ client = self.get_ipaclient()
+ delegate = {}
+ delegate['source_group_cn'] = "Please choose:"
+ delegate['dest_group_cn'] = "Please choose:"
+
+ return dict(form=delegate_form, delegate=delegate)
+
+ @expose()
+ @identity.require(identity.in_group("admins"))
+ def create(self, **kw):
+ """Creates a new delegation"""
+ self.restrict_post()
+ client = self.get_ipaclient()
+
+ if kw.get('submit', '').startswith('Cancel'):
+ turbogears.flash("Add delegation cancelled")
+ raise turbogears.redirect('/delegate/list')
+
+ # Try to handle the case where the user entered just some data
+ # into the source/dest group name but didn't do a Find. We'll do
+ # our best to see if a group by that name exists and if so, use it.
+ dest_group_dn = kw.get('dest_group_dn')
+ dest_group_cn = kw.get('dest_group_cn')
+ if not dest_group_dn and dest_group_cn:
+ try:
+ group = client.get_entry_by_cn(dest_group_cn, ['dn'])
+ kw['dest_group_dn'] = group.dn
+ except:
+ kw['dest_group_cn'] = "Please choose:"
+ source_group_dn = kw.get('source_group_dn')
+ source_group_cn = kw.get('source_group_cn')
+ if not source_group_dn and source_group_cn:
+ try:
+ group = client.get_entry_by_cn(source_group_cn, ['dn'])
+ kw['source_group_dn'] = group.dn
+ except:
+ kw['source_group_cn'] = "Please choose:"
+ tg_errors, kw = self.delegatevalidate(**kw)
+ if tg_errors:
+ turbogears.flash("There were validation errors.<br/>" +
+ "Please see the messages below for details.")
+ return dict(form=delegate_form, delegate=kw,
+ tg_template='ipagui.templates.delegatenew')
+
+ try:
+ aci_entry = client.get_aci_entry(aci_fields)
+
+ new_aci = ipa.aci.ACI()
+ new_aci.name = kw.get('name')
+ new_aci.source_group = kw.get('source_group_dn')
+ new_aci.dest_group = kw.get('dest_group_dn')
+ new_aci.attrs = kw.get('attrs')
+ if isinstance(new_aci.attrs, basestring):
+ new_aci.attrs = [new_aci.attrs]
+
+ # Look for an existing ACI of the same name
+ aci_str_list = aci_entry.getValues('aci')
+ if aci_str_list is None:
+ aci_str_list = []
+ if not(isinstance(aci_str_list,list) or isinstance(aci_str_list,tuple)):
+ aci_str_list = [aci_str_list]
+
+ for aci_str in aci_str_list:
+ try:
+ old_aci = ipa.aci.ACI(aci_str)
+ if old_aci.name == new_aci.name:
+ turbogears.flash("Delgate add failed: a delegation of that name already exists")
+ return dict(form=delegate_form, delegate=kw,
+ tg_template='ipagui.templates.delegatenew')
+ except SyntaxError:
+ # ignore aci_str's that ACI can't parse
+ pass
+
+
+ # not pulling down existing aci attributes
+ aci_entry = client.get_aci_entry(['dn'])
+ aci_entry.setValue('aci', new_aci.export_to_string())
+
+ client.update_entry(aci_entry)
+
+ # Now add to the editors group so they can make changes in the UI
+ try:
+ group = client.get_entry_by_cn("editors")
+ client.add_group_to_group(new_aci.source_group, group.dn)
+ except ipa.ipaerror.exception_for(ipa.ipaerror.LDAP_EMPTY_MODLIST):
+ # This is ok, ignore it
+ pass
+
+ except ipaerror.IPAError, e:
+ turbogears.flash("Delgate add failed: " + str(e) + "<br/>" + e.detail[0].get('desc','') + ". " + e.detail[0].get('info',''))
+ return dict(form=delegate_form, delegate=kw,
+ tg_template='ipagui.templates.delegatenew')
+
+ turbogears.flash("delegate created")
+ raise turbogears.redirect('/delegate/list')
+
+ @expose("ipagui.templates.delegateedit")
+ @identity.require(identity.in_group("admins"))
+ def edit(self, acistr, tg_errors=None):
+ """Display delegate page"""
+ if tg_errors:
+ turbogears.flash("There were validation errors.<br/>" +
+ "Please see the messages below for details.")
+
+ client = self.get_ipaclient()
+
+ try:
+ aci_entry = client.get_aci_entry(aci_fields)
+ aci = ipa.aci.ACI(acistr)
+ group_dn_to_cn = ipa.aci.extract_group_cns([aci], client)
+
+ delegate = aci.to_dict()
+ delegate['source_group_dn'] = delegate['source_group']
+ delegate['source_group_cn'] = group_dn_to_cn[delegate['source_group_dn']]
+ delegate['dest_group_dn'] = delegate['dest_group']
+ delegate['dest_group_cn'] = group_dn_to_cn[delegate['dest_group_dn']]
+
+ return dict(form=delegate_form, delegate=delegate)
+ except (SyntaxError, ipaerror.IPAError), e:
+ turbogears.flash("Delegation edit failed: " + str(e) + "<br/>" + e.detail[0].get('desc','') + ". " + e.detail[0].get('info',''))
+ raise turbogears.redirect('/delegate/list')
+
+
+ @expose()
+ @identity.require(identity.in_group("admins"))
+ def update(self, **kw):
+ """Display delegate page"""
+ self.restrict_post()
+ client = self.get_ipaclient()
+
+ if kw.get('submit', '').startswith('Cancel'):
+ turbogears.flash("Edit delegation cancelled")
+ raise turbogears.redirect('/delegate/list')
+
+ # Try to handle the case where the user entered just some data
+ # into the source/dest group name but didn't do a Find. We'll do
+ # our best to see if a group by that name exists and if so, use it.
+ dest_group_cn = kw.get('dest_group_cn')
+ if dest_group_cn:
+ try:
+ group = client.get_entry_by_cn(dest_group_cn, ['dn'])
+ kw['dest_group_dn'] = group.dn
+ except:
+ # This _notfound value is used in delegatevalidate()
+ kw['dest_group_cn_notfound'] = True
+ source_group_cn = kw.get('source_group_cn')
+ if source_group_cn:
+ try:
+ group = client.get_entry_by_cn(source_group_cn, ['dn'])
+ kw['source_group_dn'] = group.dn
+ except:
+ # This _notfound value is used in delegatevalidate()
+ kw['source_group_cn_notfound'] = True
+
+ tg_errors, kw = self.delegatevalidate(**kw)
+ if tg_errors:
+ turbogears.flash("There were validation errors.<br/>" +
+ "Please see the messages below for details.")
+ return dict(form=delegate_form, delegate=kw,
+ tg_template='ipagui.templates.delegateedit')
+
+ try:
+ aci_entry = client.get_aci_entry(aci_fields)
+
+ aci_str_list = aci_entry.getValues('aci')
+ if aci_str_list is None:
+ aci_str_list = []
+ if not(isinstance(aci_str_list,list) or isinstance(aci_str_list,tuple)):
+ aci_str_list = [aci_str_list]
+
+ try :
+ old_aci_index = aci_str_list.index(kw['orig_acistr'])
+ except ValueError:
+ turbogears.flash("Delegation update failed:<br />" +
+ "The delegation you were attempting to update has been " +
+ "concurrently modified. Please cancel the edit " +
+ "and try editing the delegation again.")
+ return dict(form=delegate_form, delegate=kw,
+ tg_template='ipagui.templates.delegateedit')
+
+ new_aci = ipa.aci.ACI()
+ new_aci.name = kw.get('name')
+ new_aci.source_group = kw.get('source_group_dn')
+ new_aci.dest_group = kw.get('dest_group_dn')
+ new_aci.attrs = kw.get('attrs')
+ if isinstance(new_aci.attrs, basestring):
+ new_aci.attrs = [new_aci.attrs]
+ new_aci_str = new_aci.export_to_string()
+
+ new_aci_str_list = copy.copy(aci_str_list)
+ old_aci = ipa.aci.ACI(new_aci_str_list[old_aci_index])
+ new_aci_str_list[old_aci_index] = new_aci_str
+ aci_entry.setValue('aci', new_aci_str_list)
+
+ client.update_entry(aci_entry)
+
+ if new_aci.source_group != old_aci.source_group:
+ aci_list = []
+ last = True
+ for aci_str in new_aci_str_list:
+ try:
+ aci = ipa.aci.ACI(aci_str)
+ if aci.source_group == old_aci.source_group:
+ last = False
+ break
+ except SyntaxError:
+ # ignore aci_str's that ACI can't parse
+ pass
+ if last:
+ group = client.get_entry_by_cn("editors")
+ client.remove_member_from_group(old_aci.source_group, group.dn)
+
+ # Now add to the editors group so they can make changes in the UI
+ try:
+ group = client.get_entry_by_cn("editors")
+ client.add_group_to_group(new_aci.source_group, group.dn)
+ except ipa.ipaerror.exception_for(ipa.ipaerror.LDAP_EMPTY_MODLIST):
+ # This is ok, ignore it
+ pass
+
+
+ turbogears.flash("delegate updated")
+ raise turbogears.redirect('/delegate/list')
+ except (SyntaxError, ipaerror.IPAError), e:
+ turbogears.flash("Delegation update failed: " + str(e) + "<br/>" + e.detail[0].get('desc','') + ". " + e.detail[0].get('info',''))
+ return dict(form=delegate_form, delegate=kw,
+ tg_template='ipagui.templates.delegateedit')
+
+ @expose("ipagui.templates.delegatelist")
+ @identity.require(identity.not_anonymous())
+ def list(self):
+ """Display delegate page"""
+ client = self.get_ipaclient()
+
+ try:
+ aci_entry = client.get_aci_entry(aci_fields)
+ except ipaerror.IPAError, e:
+ turbogears.flash("Delegation list failed: " + str(e) + "<br/>" + e.detail[0].get('desc','') + ". " + e.detail[0].get('info',''))
+ raise turbogears.redirect('/')
+
+ aci_str_list = aci_entry.getValues('aci')
+ if aci_str_list is None:
+ aci_str_list = []
+ if not(isinstance(aci_str_list,list) or isinstance(aci_str_list,tuple)):
+ aci_str_list = [aci_str_list]
+
+ aci_list = []
+ for aci_str in aci_str_list:
+ try:
+ aci = ipa.aci.ACI(aci_str)
+ aci_list.append(aci)
+ except SyntaxError:
+ # ignore aci_str's that ACI can't parse
+ pass
+ group_dn_to_cn = ipa.aci.extract_group_cns(aci_list, client)
+
+ aci_list = sorted(aci_list, key=operator.itemgetter(0))
+ # The list page needs to display field labels, not raw
+ # LDAP attributes
+ for aci in aci_list:
+ aci.attrs = map(lambda name:
+ ipagui.forms.delegate.aci_name_to_label.get(name, name),
+ aci.attrs)
+
+ return dict(aci_list=aci_list, group_dn_to_cn=group_dn_to_cn,
+ fields=ipagui.forms.delegate.DelegateFields())
+
+ @expose()
+ @identity.require(identity.in_group("admins"))
+ def delete(self, acistr):
+ """Display delegate page"""
+ self.restrict_post()
+ client = self.get_ipaclient()
+
+ try:
+ aci_entry = client.get_aci_entry(aci_fields)
+
+ aci_str_list = aci_entry.getValues('aci')
+ if aci_str_list is None:
+ aci_str_list = []
+ if not(isinstance(aci_str_list,list) or isinstance(aci_str_list,tuple)):
+ aci_str_list = [aci_str_list]
+
+ try :
+ old_aci_index = aci_str_list.index(acistr)
+ except ValueError:
+ turbogears.flash("Delegation deletion failed:<br />" +
+ "The delegation you were attempting to delete has been " +
+ "concurrently modified.")
+ raise turbogears.redirect('/delegate/list')
+
+ old_aci = ipa.aci.ACI(aci_str_list[old_aci_index])
+ new_aci_str_list = copy.copy(aci_str_list)
+ del new_aci_str_list[old_aci_index]
+ aci_entry.setValue('aci', new_aci_str_list)
+
+ client.update_entry(aci_entry)
+
+ aci_list = []
+ last = True
+ for aci_str in new_aci_str_list:
+ try:
+ aci = ipa.aci.ACI(aci_str)
+ if aci.source_group == old_aci.source_group:
+ last = False
+ break
+ except SyntaxError:
+ # ignore aci_str's that ACI can't parse
+ pass
+ if last:
+ group = client.get_entry_by_cn("editors")
+ client.remove_member_from_group(old_aci.source_group, group.dn)
+
+ turbogears.flash("delegate deleted")
+ raise turbogears.redirect('/delegate/list')
+ except (SyntaxError, ipaerror.IPAError), e:
+ turbogears.flash("Delegation deletion failed: " + str(e) + "<br/>" + e.detail[0].get('desc','') + ". " + e.detail[0].get('info',''))
+ raise turbogears.redirect('/delegate/list')
+
+ @expose("ipagui.templates.delegategroupsearch")
+ @identity.require(identity.not_anonymous())
+ def group_search(self, **kw):
+ """Searches for groups and displays list of results in a table.
+ This method is used for the ajax search on the delegation pages."""
+ client = self.get_ipaclient()
+
+ groups = []
+ groups_counter = 0
+ searchlimit = 100
+ criteria = kw.get('criteria')
+ if criteria != None and len(criteria) > 0:
+ try:
+ groups = client.find_groups(criteria.encode('utf-8'), None,
+ searchlimit)
+ groups_counter = groups[0]
+ groups = groups[1:]
+ except ipaerror.IPAError, e:
+ turbogears.flash("search failed: " + str(e))
+
+ return dict(groups=groups, criteria=criteria,
+ which_group=kw.get('which_group'),
+ counter=groups_counter)
+
+ @validate(form=delegate_form)
+ @identity.require(identity.not_anonymous())
+ def delegatevalidate(self, tg_errors=None, **kw):
+ # We are faking this because otherwise it shows up as one huge
+ # block of color in the UI when it has a not empty validator.
+ if not tg_errors:
+ tg_errors = {}
+ if not kw.get('attrs'):
+ tg_errors['attrs'] = _("Please select at least one value")
+ if kw.get('dest_group_cn_notfound'):
+ tg_errors['dest_group_dn'] = _("Group not found")
+ if kw.get('source_group_cn_notfound'):
+ tg_errors['source_group_dn'] = _("Group not found")
+ cherrypy.request.validation_errors = tg_errors
+ return tg_errors, kw
diff --git a/ipa-server/ipa-gui/ipagui/subcontrollers/group.py b/ipa-server/ipa-gui/ipagui/subcontrollers/group.py
new file mode 100644
index 00000000..6196d13d
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/subcontrollers/group.py
@@ -0,0 +1,484 @@
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+from pickle import dumps, loads
+from base64 import b64encode, b64decode
+import logging
+
+import cherrypy
+import turbogears
+from turbogears import controllers, expose, flash
+from turbogears import validators, validate
+from turbogears import widgets, paginate
+from turbogears import error_handler
+from turbogears import identity
+
+from ipacontroller import IPAController
+import ipa.config
+import ipa.group
+from ipa.entity import utf8_encode_values
+from ipa import ipaerror
+import ipagui.forms.group
+
+log = logging.getLogger(__name__)
+
+group_new_form = ipagui.forms.group.GroupNewForm()
+group_edit_form = ipagui.forms.group.GroupEditForm()
+
+group_fields = ['*', 'nsAccountLock']
+
+class GroupController(IPAController):
+
+
+ #########
+ # Group #
+ #########
+
+ @expose()
+ @identity.require(identity.not_anonymous())
+ def index(self, tg_errors=None):
+ raise turbogears.redirect("/group/list")
+
+ @expose("ipagui.templates.groupnew")
+ @identity.require(identity.in_group("admins"))
+ def new(self, tg_errors=None):
+ """Displays the new group form"""
+ if tg_errors:
+ turbogears.flash("There were validation errors.<br/>" +
+ "Please see the messages below for details.")
+
+ client = self.get_ipaclient()
+
+ return dict(form=group_new_form, group={})
+
+ @expose()
+ @identity.require(identity.in_group("admins"))
+ def create(self, **kw):
+ """Creates a new group"""
+ self.restrict_post()
+ client = self.get_ipaclient()
+
+ if kw.get('submit') == 'Cancel':
+ turbogears.flash("Add group cancelled")
+ raise turbogears.redirect('/')
+
+ tg_errors, kw = self.groupcreatevalidate(**kw)
+ if tg_errors:
+ turbogears.flash("There were validation errors.<br/>" +
+ "Please see the messages below for details.")
+ return dict(form=group_new_form, group=kw,
+ tg_template='ipagui.templates.groupnew')
+
+ #
+ # Create the group itself
+ #
+ try:
+ new_group = ipa.group.Group()
+ new_group.setValue('cn', kw.get('cn'))
+ new_group.setValue('description', kw.get('description'))
+
+ rv = client.add_group(new_group)
+
+ if kw.get('nsAccountLock'):
+ client.mark_group_inactive(kw.get('cn'))
+ except ipaerror.exception_for(ipaerror.LDAP_DUPLICATE):
+ turbogears.flash("Group with name '%s' already exists" %
+ kw.get('cn'))
+ return dict(form=group_new_form, group=kw,
+ tg_template='ipagui.templates.groupnew')
+ except ipaerror.IPAError, e:
+ turbogears.flash("Group add failed: " + str(e) + "<br/>" + e.detail[0].get('desc','') + ". " + e.detail[0].get('info',''))
+ return dict(form=group_new_form, group=kw,
+ tg_template='ipagui.templates.groupnew')
+
+ #
+ # NOTE: from here on, the group now exists.
+ # on any error, we redirect to the _edit_ group page.
+ # this code does data setup, similar to groupedit()
+ #
+ if isinstance(kw['cn'], list):
+ cn0 = kw['cn'][0]
+ else:
+ cn0 = kw['cn']
+ group = client.get_entry_by_cn(cn0, group_fields)
+ group_dict = group.toDict()
+ member_dicts = []
+
+ # store a copy of the original group for the update later
+ group_data = b64encode(dumps(group_dict))
+ member_data = b64encode(dumps(member_dicts))
+ group_dict['group_orig'] = group_data
+ group_dict['member_data'] = member_data
+
+ # preserve group add info in case of errors
+ group_dict['dnadd'] = kw.get('dnadd')
+ group_dict['dn_to_info_json'] = kw.get('dn_to_info_json')
+
+ #
+ # Add members
+ #
+ failed_adds = []
+ try:
+ dnadds = kw.get('dnadd')
+ if dnadds != None:
+ if not(isinstance(dnadds,list) or isinstance(dnadds,tuple)):
+ dnadds = [dnadds]
+ failed_adds = client.add_members_to_group(
+ utf8_encode_values(dnadds), group.dn)
+ kw['dnadd'] = failed_adds
+ except ipaerror.IPAError, e:
+ failed_adds = dnadds
+
+ if len(failed_adds) > 0:
+ message = "Group successfully created.<br />"
+ message += "There was an error adding group members.<br />"
+ message += "Failures have been preserved in the add/remove lists."
+ turbogears.flash(message)
+ return dict(form=group_edit_form, group=group_dict,
+ members=member_dicts,
+ tg_template='ipagui.templates.groupedit')
+
+ turbogears.flash("%s added!" % kw.get('cn'))
+ raise turbogears.redirect('/group/show', cn=kw.get('cn'))
+
+ @expose("ipagui.templates.dynamiceditsearch")
+ @identity.require(identity.not_anonymous())
+ def edit_search(self, **kw):
+ """Searches for users+groups and displays list of results in a table.
+ This method is used for the ajax search on the group edit page."""
+ client = self.get_ipaclient()
+
+ users = []
+ groups = []
+ counter = 0
+ searchlimit = 100
+ criteria = kw.get('criteria')
+ if criteria != None and len(criteria) > 0:
+ try:
+ users = client.find_users(criteria.encode('utf-8'), None, searchlimit)
+ users_counter = users[0]
+ users = users[1:]
+
+ groups = client.find_groups(criteria.encode('utf-8'), None,
+ searchlimit)
+ groups_counter = groups[0]
+ groups = groups[1:]
+
+ if users_counter < 0 or groups_counter < 0:
+ counter = -1
+ else:
+ counter = users_counter + groups_counter
+ except ipaerror.IPAError, e:
+ turbogears.flash("search failed: " + str(e))
+
+ return dict(users=users, groups=groups, criteria=criteria,
+ counter=counter)
+
+
+ @expose("ipagui.templates.groupedit")
+ @identity.require(identity.in_group("admins"))
+ def edit(self, cn, tg_errors=None):
+ """Displays the edit group form"""
+ if tg_errors:
+ turbogears.flash("There were validation errors.<br/>" +
+ "Please see the messages below for details.")
+
+ client = self.get_ipaclient()
+
+ try:
+ group = client.get_entry_by_cn(cn, group_fields)
+
+ group_dict = group.toDict()
+
+ #
+ # convert members to users, for easier manipulation on the page
+ #
+
+ members = client.group_members(group.dn, ['dn', 'givenname', 'sn', 'uid', 'cn'], 1)
+ members = members[1:]
+ members.sort(self.sort_group_member)
+
+ # Map users into an array of dicts, which can be serialized
+ # (so we don't have to do this on each round trip)
+ member_dicts = map(lambda member: member.toDict(), members)
+
+ indirect_members = client.group_members(group.dn, ['dn', 'givenname', 'sn', 'uid', 'cn'], 2)
+ indirect_members = indirect_members[1:]
+ indirect_members.sort(self.sort_group_member)
+
+ # add our own flag
+ for i in range(len(indirect_members)):
+ indirect_members[i].setValue('inherited', True)
+
+ # Map users into an array of dicts, which can be serialized
+ # (so we don't have to do this on each round trip)
+ indirect_members_dicts = map(lambda member: member.toDict(), indirect_members)
+
+ member_dicts = member_dicts + indirect_members_dicts
+
+ # store a copy of the original group for the update later
+ group_data = b64encode(dumps(group_dict))
+ member_data = b64encode(dumps(member_dicts))
+ group_dict['group_orig'] = group_data
+ group_dict['member_data'] = member_data
+
+ return dict(form=group_edit_form, group=group_dict, members=member_dicts)
+ except ipaerror.IPAError, e:
+ turbogears.flash("Group edit failed: " + str(e))
+ raise turbogears.redirect('/group/show', uid=cn)
+
+ @expose()
+ @identity.require(identity.in_group("admins"))
+ def update(self, **kw):
+ """Updates an existing group"""
+ self.restrict_post()
+ client = self.get_ipaclient()
+
+ if kw.get('submit') == 'Cancel Edit':
+ orig_group_dict = loads(b64decode(kw.get('group_orig')))
+ # if cancelling need to use the original group because the one
+ # in kw may not exist yet.
+ cn = orig_group_dict.get('cn')
+ if (isinstance(cn,basestring)):
+ cn = [cn]
+ turbogears.flash("Edit group cancelled")
+ raise turbogears.redirect('/group/show', cn=cn[0])
+
+ if kw.get('editprotected') == '':
+ # if editprotected set these don't get sent in kw
+ orig_group_dict = loads(b64decode(kw.get('group_orig')))
+ kw['cn'] = orig_group_dict['cn']
+ kw['gidnumber'] = orig_group_dict['gidnumber']
+
+ # Decode the member data, in case we need to round trip
+ member_dicts = loads(b64decode(kw.get('member_data')))
+
+ tg_errors, kw = self.groupupdatevalidate(**kw)
+ if tg_errors:
+ turbogears.flash("There were validation errors.<br/>" +
+ "Please see the messages below for details.")
+ return dict(form=group_edit_form, group=kw, members=member_dicts,
+ tg_template='ipagui.templates.groupedit')
+
+ group_modified = False
+
+ #
+ # Update group itself
+ #
+ try:
+ orig_group_dict = loads(b64decode(kw.get('group_orig')))
+
+ new_group = ipa.group.Group(orig_group_dict)
+ if new_group.description != kw.get('description'):
+ group_modified = True
+ new_group.setValue('description', kw.get('description'))
+ if kw.get('editprotected') == 'true':
+ new_gid = str(kw.get('gidnumber'))
+ if new_group.gidnumber != new_gid:
+ group_modified = True
+ new_group.setValue('gidnumber', new_gid)
+ else:
+ new_group.setValue('gidnumber', orig_group_dict.get('gidnumber'))
+ new_group.setValue('cn', orig_group_dict.get('cn'))
+ if new_group.cn != kw.get('cn'):
+ group_modified = True
+ new_group.setValue('cn', kw['cn'])
+
+ if group_modified:
+ rv = client.update_group(new_group)
+ #
+ # If the group update succeeds, but below operations fail, we
+ # need to make sure a subsequent submit doesn't try to update
+ # the group again.
+ #
+ kw['group_orig'] = b64encode(dumps(new_group.toDict()))
+ except ipaerror.IPAError, e:
+ turbogears.flash("Group update failed: " + str(e) + "<br/>" + e.detail[0].get('desc','') + ". " + e.detail[0].get('info',''))
+ return dict(form=group_edit_form, group=kw, members=member_dicts,
+ tg_template='ipagui.templates.groupedit')
+
+ if kw.get('nsAccountLock') == '':
+ kw['nsAccountLock'] = "false"
+
+ modify_no_update = False
+ try:
+ if kw.get('nsAccountLock') == "false" and new_group.getValues('nsaccountlock') == "true":
+ client.mark_group_active(kw.get('cn'))
+ modify_no_update = True
+ elif kw.get('nsAccountLock') == "true" and new_group.nsaccountlock != "true":
+ client.mark_group_inactive(kw.get('cn'))
+ modify_no_update = True
+ except ipaerror.IPAError, e:
+ turbogears.flash("Group status change failed: " + str(e) + "<br/>" + e.detail[0].get('desc','') + ". " + e.detail[0].get('info',''))
+ return dict(form=group_edit_form, group=kw, members=member_dicts,
+ tg_template='ipagui.templates.groupedit')
+
+ #
+ # Add members
+ #
+ failed_adds = []
+ try:
+ dnadds = kw.get('dnadd')
+ if dnadds != None:
+ if not(isinstance(dnadds,list) or isinstance(dnadds,tuple)):
+ dnadds = [dnadds]
+ failed_adds = client.add_members_to_group(
+ utf8_encode_values(dnadds), new_group.dn)
+ kw['dnadd'] = failed_adds
+ group_modified = True
+ except ipaerror.IPAError, e:
+ turbogears.flash("Updating group membership failed: " + str(e) + "<br/>" + e.detail[0].get('desc','') + ". " + e.detail[0].get('info',''))
+ return dict(form=group_edit_form, group=kw, members=member_dicts,
+ tg_template='ipagui.templates.groupedit')
+
+ #
+ # Remove members
+ #
+ failed_dels = []
+ try:
+ dndels = kw.get('dndel')
+ if dndels != None:
+ if not(isinstance(dndels,list) or isinstance(dndels,tuple)):
+ dndels = [dndels]
+ failed_dels = client.remove_members_from_group(
+ utf8_encode_values(dndels), new_group.dn)
+ kw['dndel'] = failed_dels
+ group_modified = True
+ except ipaerror.IPAError, e:
+ turbogears.flash("Updating group membership failed: " + str(e) + "<br/>" + e.detail[0].get('desc','') + ". " + e.detail[0].get('info',''))
+ return dict(form=group_edit_form, group=kw, members=member_dicts,
+ tg_template='ipagui.templates.groupedit')
+
+ #
+ # TODO - check failed ops to see if it's because of another update.
+ # handle "someone else already did it" errors better - perhaps
+ # not even as an error
+ # TODO - update the Group Members list.
+ # (note that we have to handle the above todo first, or else
+ # there will be an error message, but the add/del lists will
+ # be empty)
+ #
+ if (len(failed_adds) > 0) or (len(failed_dels) > 0):
+ message = "There was an error updating group members.<br />"
+ message += "Failures have been preserved in the add/remove lists."
+ if group_modified:
+ message = "Group Details successfully updated.<br />" + message
+ turbogears.flash(message)
+ return dict(form=group_edit_form, group=kw, members=member_dicts,
+ tg_template='ipagui.templates.groupedit')
+
+ if isinstance(kw['cn'], list):
+ cn0 = kw['cn'][0]
+ else:
+ cn0 = kw['cn']
+ if group_modified == True or modify_no_update == True:
+ turbogears.flash("%s updated!" % cn0)
+ else:
+ turbogears.flash("No modifications requested.")
+ raise turbogears.redirect('/group/show', cn=cn0)
+
+
+ @expose("ipagui.templates.grouplist")
+ @identity.require(identity.not_anonymous())
+ def list(self, **kw):
+ """Search for groups and display results"""
+ client = self.get_ipaclient()
+
+ groups = None
+ # counter = 0
+ criteria = kw.get('criteria')
+ if criteria != None and len(criteria) > 0:
+ try:
+ groups = client.find_groups(criteria.encode('utf-8'), None, 0, 2)
+ counter = groups[0]
+ groups = groups[1:]
+ if counter == -1:
+ turbogears.flash("These results are truncated.<br />" +
+ "Please refine your search and try again.")
+ except ipaerror.IPAError, e:
+ turbogears.flash("Find groups failed: " + str(e) + "<br/>" + e.detail[0].get('desc','') + ". " + e.detail[0].get('info',''))
+ raise turbogears.redirect("/group/list")
+
+ return dict(groups=groups, criteria=criteria,
+ fields=ipagui.forms.group.GroupFields())
+
+ @expose("ipagui.templates.groupshow")
+ @identity.require(identity.not_anonymous())
+ def show(self, cn):
+ """Retrieve a single group for display"""
+ client = self.get_ipaclient()
+
+ try:
+ group = client.get_entry_by_cn(cn, group_fields)
+ group_dict = group.toDict()
+
+ #
+ # convert members to users, for display on the page
+ #
+
+ members = client.group_members(group.dn, ['dn', 'givenname', 'sn', 'uid', 'cn'], 1)
+ members = members[1:]
+ members.sort(self.sort_group_member)
+ member_dicts = map(lambda member: member.toDict(), members)
+
+ indirect_members = client.group_members(group.dn, ['dn', 'givenname', 'sn', 'uid', 'cn'], 2)
+ indirect_members = indirect_members[1:]
+ indirect_members.sort(self.sort_group_member)
+
+ # add our own flag
+ for i in range(len(indirect_members)):
+ indirect_members[i].setValue('inherited', True)
+
+ # Map users into an array of dicts, which can be serialized
+ # (so we don't have to do this on each round trip)
+ indirect_members_dicts = map(lambda member: member.toDict(), indirect_members)
+
+ member_dicts = member_dicts + indirect_members_dicts
+ logging.info("%s" % member_dicts)
+
+ return dict(group=group_dict, fields=ipagui.forms.group.GroupFields(),
+ members = member_dicts)
+ except ipaerror.IPAError, e:
+ turbogears.flash("Group show failed: " + str(e))
+ raise turbogears.redirect("/")
+
+ @expose()
+ @identity.require(identity.not_anonymous())
+ def delete(self, dn):
+ """Delete group."""
+ self.restrict_post()
+ client = self.get_ipaclient()
+
+ try:
+ client.delete_group(dn)
+
+ turbogears.flash("group deleted")
+ raise turbogears.redirect('/group/list')
+ except (SyntaxError, ipaerror.IPAError), e:
+ turbogears.flash("Group deletion failed: " + str(e) + "<br/>" + e.detail[0].get('desc','') + ". " + e.detail[0].get('info',''))
+ raise turbogears.redirect('/group/list')
+
+ @validate(form=group_new_form)
+ @identity.require(identity.not_anonymous())
+ def groupcreatevalidate(self, tg_errors=None, **kw):
+ return tg_errors, kw
+
+ @validate(form=group_edit_form)
+ @identity.require(identity.not_anonymous())
+ def groupupdatevalidate(self, tg_errors=None, **kw):
+ return tg_errors, kw
+
diff --git a/ipa-server/ipa-gui/ipagui/subcontrollers/ipacontroller.py b/ipa-server/ipa-gui/ipagui/subcontrollers/ipacontroller.py
new file mode 100644
index 00000000..db7f04cb
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/subcontrollers/ipacontroller.py
@@ -0,0 +1,92 @@
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+import os
+import logging
+
+import cherrypy
+import turbogears
+from turbogears import controllers, expose, flash
+from turbogears import validators, validate
+from turbogears import widgets, paginate
+from turbogears import error_handler
+from turbogears import identity
+
+import ipa.ipaclient
+from ipaserver import funcs
+import ipa.config
+
+log = logging.getLogger(__name__)
+
+ipa.config.init_config()
+
+class IPAController(controllers.Controller):
+ def restrict_post(self):
+ if cherrypy.request.method != "POST":
+ turbogears.flash("This method only accepts posts")
+ raise turbogears.redirect("/")
+
+ def get_ipaclient(self):
+ transport = funcs.IPAServer()
+ client = ipa.ipaclient.IPAClient(transport)
+ client.set_krbccache(os.environ["KRB5CCNAME"])
+ return client
+
+ def utf8_encode(self, value):
+ if value != None:
+ value = value.encode('utf-8')
+ return value
+
+ def sort_group_member(self, a, b):
+ """Comparator function used for sorting group members."""
+ if a.getValue('uid') and b.getValue('uid'):
+ if a.getValue('sn') == b.getValue('sn'):
+ if a.getValue('givenName') == b.getValue('givenName'):
+ if a.getValue('uid') == b.getValue('uid'):
+ return 0
+ elif a.getValue('uid') < b.getValue('uid'):
+ return -1
+ else:
+ return 1
+ elif a.getValue('givenName') < b.getValue('givenName'):
+ return -1
+ else:
+ return 1
+ elif a.getValue('sn') < b.getValue('sn'):
+ return -1
+ else:
+ return 1
+ elif a.getValue('uid'):
+ return -1
+ elif b.getValue('uid'):
+ return 1
+ else:
+ if a.getValue('cn') == b.getValue('cn'):
+ return 0
+ elif a.getValue('cn') < b.getValue('cn'):
+ return -1
+ else:
+ return 1
+
+ def sort_by_cn(self, a, b):
+ """Comparator function used for sorting groups."""
+ if a.getValue('cn') == b.getValue('cn'):
+ return 0
+ elif a.getValue('cn') < b.getValue('cn'):
+ return -1
+ else:
+ return 1
diff --git a/ipa-server/ipa-gui/ipagui/subcontrollers/ipapolicy.py b/ipa-server/ipa-gui/ipagui/subcontrollers/ipapolicy.py
new file mode 100644
index 00000000..1db062b4
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/subcontrollers/ipapolicy.py
@@ -0,0 +1,208 @@
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+import os
+from pickle import dumps, loads
+from base64 import b64encode, b64decode
+import copy
+import logging
+
+import cherrypy
+import turbogears
+from turbogears import controllers, expose, flash
+from turbogears import validators, validate
+from turbogears import widgets, paginate
+from turbogears import error_handler
+from turbogears import identity
+
+from ipacontroller import IPAController
+from ipa.entity import utf8_encode_values
+from ipa import ipaerror
+import ipa.entity
+import ipagui.forms.ipapolicy
+from ipagui.helpers import ipahelper
+
+import ldap.dn
+
+log = logging.getLogger(__name__)
+
+ipapolicy_edit_form = ipagui.forms.ipapolicy.IPAPolicyForm()
+
+class IPAPolicyController(IPAController):
+
+ @expose()
+ @identity.require(identity.in_group("admins"))
+ def index(self):
+ raise turbogears.redirect("/ipapolicy/show")
+
+ @expose("ipagui.templates.ipapolicyshow")
+ @identity.require(identity.in_group("admins"))
+ def show(self, tg_errors=None):
+ """Displays the one policy page"""
+ client = self.get_ipaclient()
+ config = client.get_ipa_config()
+ ipapolicy = config.toDict()
+
+ ppolicy = client.get_password_policy()
+ password = ppolicy.toDict()
+
+ return dict(ipapolicy=ipapolicy,password=password,fields=ipagui.forms.ipapolicy.IPAPolicyFields())
+
+ @expose("ipagui.templates.ipapolicyedit")
+ @identity.require(identity.in_group("admins"))
+ def edit(self, tg_errors=None):
+ """Displays the edit IPA policy form"""
+ if tg_errors:
+ turbogears.flash("There were validation errors.<br/>" +
+ "Please see the messages below for details.")
+
+ try:
+ client = self.get_ipaclient()
+ config = client.get_ipa_config()
+ ipapolicy_dict = config.toDict()
+
+ ppolicy = client.get_password_policy()
+ password_dict = ppolicy.toDict()
+
+ # store a copy of the original policy for the update later
+ ipapolicy_data = b64encode(dumps(ipapolicy_dict))
+ ipapolicy_dict['ipapolicy_orig'] = ipapolicy_data
+
+ # store a copy of the original policy for the update later
+ password_data = b64encode(dumps(password_dict))
+ password_dict['password_orig'] = password_data
+
+ # Combine the 2 dicts to make the form easier
+ ipapolicy_dict.update(password_dict)
+
+ # Load potential multi-valued fields
+ if isinstance(ipapolicy_dict.get('ipauserobjectclasses',''), basestring):
+ ipapolicy_dict['ipauserobjectclasses'] = [ipapolicy_dict.get('ipauserobjectclasses')]
+ ipapolicy_dict['userobjectclasses'] = ipahelper.setup_mv_fields(ipapolicy_dict.get('ipauserobjectclasses'), 'ipauserobjectclasses')
+
+ if isinstance(ipapolicy_dict.get('ipagroupobjectclasses',''), basestring):
+ ipapolicy_dict['ipagroupobjectclasses'] = [ipapolicy_dict.get('ipagroupobjectclasses')]
+ ipapolicy_dict['groupobjectclasses'] = ipahelper.setup_mv_fields(ipapolicy_dict.get('ipagroupobjectclasses'), 'ipagroupobjectclasses')
+
+ return dict(form=ipapolicy_edit_form, ipapolicy=ipapolicy_dict)
+ except ipaerror.IPAError, e:
+ turbogears.flash("IPA Policy edit failed: " + str(e) + "<br/>" + str(e.detail))
+ raise turbogears.redirect('/ipapolicy/show')
+
+
+ @expose()
+ @identity.require(identity.in_group("admins"))
+ def update(self, **kw):
+ """Display delegate page"""
+ self.restrict_post()
+ client = self.get_ipaclient()
+
+ if kw.get('submit', '').startswith('Cancel'):
+ turbogears.flash("Edit policy cancelled")
+ raise turbogears.redirect('/ipapolicy/show')
+
+ # Fix incoming multi-valued fields we created for the form
+ kw = ipahelper.fix_incoming_fields(kw, 'ipauserobjectclasses', 'userobjectclasses')
+ kw = ipahelper.fix_incoming_fields(kw, 'ipagroupobjectclasses', 'groupobjectclasses')
+
+ tg_errors, kw = self.ipapolicyupdatevalidate(**kw)
+ if tg_errors:
+ turbogears.flash("There were validation errors.<br/>" +
+ "Please see the messages below for details.")
+ return dict(form=ipapolicy_edit_form, ipapolicy=kw,
+ tg_template='ipagui.templates.ipapolicyedit')
+
+ policy_modified = False
+ password_modified = False
+
+ try:
+ orig_ipapolicy_dict = loads(b64decode(kw.get('ipapolicy_orig')))
+ orig_password_dict = loads(b64decode(kw.get('password_orig')))
+
+ new_ipapolicy = ipa.entity.Entity(orig_ipapolicy_dict)
+ new_password = ipa.entity.Entity(orig_password_dict)
+
+ if str(new_ipapolicy.getValues('ipasearchtimelimit')) != str(kw.get('ipasearchtimelimit')):
+ policy_modified = True
+ new_ipapolicy.setValue('ipasearchtimelimit', kw.get('ipasearchtimelimit'))
+ if str(new_ipapolicy.getValues('ipasearchrecordslimit')) != str(kw.get('ipasearchrecordslimit')):
+ policy_modified = True
+ new_ipapolicy.setValue('ipasearchrecordslimit', kw.get('ipasearchrecordslimit'))
+ if new_ipapolicy.getValues('ipausersearchfields') != kw.get('ipausersearchfields'):
+ policy_modified = True
+ new_ipapolicy.setValue('ipausersearchfields', kw.get('ipausersearchfields'))
+ if new_ipapolicy.getValues('ipagroupsearchfields') != kw.get('ipagroupsearchfields'):
+ policy_modified = True
+ new_ipapolicy.setValue('ipagroupsearchfields', kw.get('ipagroupsearchfields'))
+ if str(new_ipapolicy.getValues('ipapwdexpadvnotify')) != str(kw.get('ipapwdexpadvnotify')):
+ policy_modified = True
+ new_ipapolicy.setValue('ipapwdexpadvnotify', kw.get('ipapwdexpadvnotify'))
+ if str(new_ipapolicy.getValues('ipamaxusernamelength')) != str(kw.get('ipamaxusernamelength')):
+ policy_modified = True
+ new_ipapolicy.setValue('ipamaxusernamelength', kw.get('ipamaxusernamelength'))
+ if new_ipapolicy.getValues('ipahomesrootdir') != kw.get('ipahomesrootdir'):
+ policy_modified = True
+ new_ipapolicy.setValue('ipahomesrootdir', kw.get('ipahomesrootdir'))
+ if new_ipapolicy.getValues('ipadefaultloginshell') != kw.get('ipadefaultloginshell'):
+ policy_modified = True
+ new_ipapolicy.setValue('ipadefaultloginshell', kw.get('ipadefaultloginshell'))
+ if new_ipapolicy.getValues('ipadefaultprimarygroup') != kw.get('ipadefaultprimarygroup'):
+ policy_modified = True
+ new_ipapolicy.setValue('ipadefaultprimarygroup', kw.get('ipadefaultprimarygroup'))
+# if new_ipapolicy.getValues('ipauserobjectclasses') != kw.get('ipauserobjectclasses'):
+# policy_modified = True
+# new_ipapolicy.setValue('ipauserobjectclasses', kw.get('ipauserobjectclasses'))
+# if new_ipapolicy.getValues('ipagroupobjectclasses') != kw.get('ipagroupobjectclasses'):
+# policy_modified = True
+# new_ipapolicy.setValue('ipagroupobjectclasses', kw.get('ipagroupobjectclasses'))
+ if new_ipapolicy.getValues('ipadefaultemaildomain') != kw.get('ipadefaultemaildomain'):
+ policy_modified = True
+ new_ipapolicy.setValue('ipadefaultemaildomain', kw.get('ipadefaultemaildomain'))
+
+ if policy_modified:
+ rv = client.update_ipa_config(new_ipapolicy)
+
+ # Now check the password policy for updates
+ if str(new_password.getValues('krbmaxpwdlife')) != str(kw.get('krbmaxpwdlife')):
+ password_modified = True
+ new_password.setValue('krbmaxpwdlife', str(kw.get('krbmaxpwdlife')))
+ if str(new_password.getValues('krbminpwdlife')) != str(kw.get('krbminpwdlife')):
+ password_modified = True
+ new_password.setValue('krbminpwdlife', str(kw.get('krbminpwdlife')))
+ if str(new_password.getValues('krbpwdhistorylength')) != str(kw.get('krbpwdhistorylength')):
+ password_modified = True
+ new_password.setValue('krbpwdhistorylength', str(kw.get('krbpwdhistorylength')))
+ if str(new_password.getValues('krbpwdmindiffchars')) != str(kw.get('krbpwdmindiffchars')):
+ password_modified = True
+ new_password.setValue('krbpwdmindiffchars', str(kw.get('krbpwdmindiffchars')))
+ if str(new_password.getValues('krbpwdminlength')) != str(kw.get('krbpwdminlength')):
+ password_modified = True
+ new_password.setValue('krbpwdminlength', str(kw.get('krbpwdminlength')))
+ if password_modified:
+ rv = client.update_password_policy(new_password)
+
+ turbogears.flash("IPA Policy updated")
+ raise turbogears.redirect('/ipapolicy/show')
+ except ipaerror.IPAError, e:
+ turbogears.flash("Policy update failed: " + str(e) + "<br/>" + e.detail[0].get('desc','') + ". " + e.detail[0].get('info',''))
+ return dict(form=ipapolicy_edit_form, ipapolicy=kw,
+ tg_template='ipagui.templates.ipapolicyedit')
+
+ @validate(form=ipapolicy_edit_form)
+ @identity.require(identity.not_anonymous())
+ def ipapolicyupdatevalidate(self, tg_errors=None, **kw):
+ return tg_errors, kw
diff --git a/ipa-server/ipa-gui/ipagui/subcontrollers/policy.py b/ipa-server/ipa-gui/ipagui/subcontrollers/policy.py
new file mode 100644
index 00000000..8b905335
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/subcontrollers/policy.py
@@ -0,0 +1,49 @@
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+import os
+from pickle import dumps, loads
+from base64 import b64encode, b64decode
+import copy
+import logging
+
+import cherrypy
+import turbogears
+from turbogears import controllers, expose, flash
+from turbogears import validators, validate
+from turbogears import widgets, paginate
+from turbogears import error_handler
+from turbogears import identity
+
+from ipacontroller import IPAController
+from ipa.entity import utf8_encode_values
+from ipa import ipaerror
+
+import ldap.dn
+
+log = logging.getLogger(__name__)
+
+class PolicyController(IPAController):
+
+ @expose("ipagui.templates.policyindex")
+ @identity.require(identity.in_group("admins"))
+ def index(self, tg_errors=None):
+ """Displays the one policy page"""
+
+ # TODO: return a dict of the items and URLs to display on
+ # Manage Policy
+ return dict()
diff --git a/ipa-server/ipa-gui/ipagui/subcontrollers/principal.py b/ipa-server/ipa-gui/ipagui/subcontrollers/principal.py
new file mode 100644
index 00000000..3c3d9463
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/subcontrollers/principal.py
@@ -0,0 +1,193 @@
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+import os
+from pickle import dumps, loads
+from base64 import b64encode, b64decode
+import copy
+import logging
+
+import cherrypy
+import turbogears
+from turbogears import controllers, expose, flash
+from turbogears import validators, validate
+from turbogears import widgets, paginate
+from turbogears import error_handler
+from turbogears import identity
+
+from ipacontroller import IPAController
+from ipa.entity import utf8_encode_values
+from ipa import ipaerror
+import ipagui.forms.principal
+
+import ldap.dn
+
+log = logging.getLogger(__name__)
+
+principal_new_form = ipagui.forms.principal.PrincipalNewForm()
+principal_fields = ['*']
+
+class PrincipalController(IPAController):
+
+ @expose()
+ @identity.require(identity.in_group("admins"))
+ def index(self, tg_errors=None):
+ raise turbogears.redirect("/principal/list")
+
+ @expose("ipagui.templates.principalnew")
+ @identity.require(identity.in_group("admins"))
+ def new(self, tg_errors=None):
+ """Displays the new service principal form"""
+ if tg_errors:
+ turbogears.flash("There were validation errors.<br/>" +
+ "Please see the messages below for details.")
+
+ client = self.get_ipaclient()
+
+ return dict(form=principal_new_form, principal={})
+
+ @expose()
+ @identity.require(identity.in_group("admins"))
+ def create(self, **kw):
+ """Creates a service principal group"""
+ self.restrict_post()
+ client = self.get_ipaclient()
+
+ if kw.get('submit') == 'Cancel':
+ turbogears.flash("Add principal cancelled")
+ raise turbogears.redirect('/')
+
+ tg_errors, kw = self.principalcreatevalidate(**kw)
+ if tg_errors:
+ turbogears.flash("There were validation errors.<br/>" +
+ "Please see the messages below for details.")
+ return dict(form=principal_new_form, principal=kw,
+ tg_template='ipagui.templates.principalnew')
+
+ principal_name = ""
+ hostname = kw.get('hostname')
+ #
+ # Create the principal itself
+ #
+ try:
+ if kw.get('service') == "other":
+ service = kw.get('other')
+ if not service:
+ turbogears.flash("Service type must be provided")
+ return dict(form=principal_new_form, principal=kw,
+ tg_template='ipagui.templates.principalnew')
+ else:
+ service = kw.get('service')
+
+ # The realm is added by add_service_principal
+ principal_name = utf8_encode_values(service + "/" + kw.get('hostname'))
+
+ rv = client.add_service_principal(principal_name, 0)
+ except ipaerror.exception_for(ipaerror.LDAP_DUPLICATE):
+ turbogears.flash("Service principal '%s' already exists" %
+ principal_name)
+ return dict(form=principal_new_form, principal=kw,
+ tg_template='ipagui.templates.principalnew')
+ except ipaerror.IPAError, e:
+ turbogears.flash("Service principal add failed: " + str(e) + "<br/>" + e.detail[0].get('desc','') + ". " + e.detail[0].get('info',''))
+ return dict(form=principal_new_form, principal=kw,
+ tg_template='ipagui.templates.principalnew')
+
+ turbogears.flash("%s added!" % principal_name)
+ raise turbogears.redirect('/principal/list', hostname=hostname)
+
+ @expose("ipagui.templates.principallist")
+ @identity.require(identity.not_anonymous())
+ def list(self, **kw):
+ """Searches for service principals and displays list of results"""
+ client = self.get_ipaclient()
+
+ principals = None
+ counter = 0
+ hostname = kw.get('hostname')
+ if hostname != None and len(hostname) > 0:
+ try:
+ principals = client.find_service_principal(hostname.encode('utf-8'), principal_fields, 0, 2)
+ counter = principals[0]
+ principals = principals[1:]
+
+ if counter == -1:
+ turbogears.flash("These results are truncated.<br />" +
+ "Please refine your search and try again.")
+
+ # For each entry break out service type and hostname
+ for i in range(len(principals)):
+ (service,host) = principals[i].krbprincipalname.split('/')
+ h = host.split('@')
+ principals[i].setValue('service', service)
+ principals[i].setValue('hostname', h[0])
+
+ except ipaerror.IPAError, e:
+ turbogears.flash("principal list failed: " + str(e) + "<br/>" + e.detail[0].get('desc','') + ". " + e.detail[0].get('info',''))
+ raise turbogears.redirect("/principal/list")
+
+ return dict(principals=principals, hostname=hostname, fields=ipagui.forms.principal.PrincipalFields())
+
+ @expose("ipagui.templates.principalshow")
+ @identity.require(identity.not_anonymous())
+ def show(self, **kw):
+ """Display a single service principal"""
+
+ try:
+ princ = kw['principal']
+ princ_dn = kw['principal_dn']
+ except KeyError, e:
+ turbogears.flash("Principal show failed. Unable to find key %s" % e)
+ raise turbogears.redirect("/principal/list")
+
+ principal = {}
+
+ try:
+ # The principal info is passed in. Not going to both to re-query this.
+ (service,host) = princ.split('/')
+ h = host.split('@')
+ principal['service'] = service
+ principal['hostname'] = h[0]
+ principal['principal_dn'] = princ_dn
+
+ return dict(principal=principal)
+ except:
+ turbogears.flash("Principal show failed %s" % princ)
+ raise turbogears.redirect("/")
+
+ @expose()
+ @identity.require(identity.in_group("admins"))
+ def delete(self, principal):
+ """Delete a service principal"""
+ self.restrict_post()
+ client = self.get_ipaclient()
+
+ print "Deleting %s" % principal
+
+ try:
+ client.delete_service_principal(principal)
+
+ turbogears.flash("Service principal deleted")
+ raise turbogears.redirect('/principal/list')
+ except (SyntaxError, ipaerror.IPAError), e:
+ turbogears.flash("Service principal deletion failed: " + str(e) + "<br/>" + e.detail[0].get('desc','') + ". " + e.detail[0].get('info',''))
+ raise turbogears.redirect('/principal/list')
+
+ @validate(form=principal_new_form)
+ @identity.require(identity.not_anonymous())
+ def principalcreatevalidate(self, tg_errors=None, **kw):
+ return tg_errors, kw
diff --git a/ipa-server/ipa-gui/ipagui/subcontrollers/user.py b/ipa-server/ipa-gui/ipagui/subcontrollers/user.py
new file mode 100644
index 00000000..d8fabb6b
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/subcontrollers/user.py
@@ -0,0 +1,854 @@
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+import re
+import random
+from pickle import dumps, loads
+from base64 import b64encode, b64decode
+import logging
+
+import cherrypy
+import turbogears
+from turbogears import controllers, expose, flash
+from turbogears import validators, validate
+from turbogears import widgets, paginate
+from turbogears import error_handler
+from turbogears import identity
+
+from ipacontroller import IPAController
+import ipa.user
+from ipa.entity import utf8_encode_values
+from ipa import ipaerror
+import ipagui.forms.user
+import ipa.config
+from ipagui.helpers import ipahelper
+
+log = logging.getLogger(__name__)
+
+password_chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
+
+user_new_form = ipagui.forms.user.UserNewForm()
+user_edit_form = ipagui.forms.user.UserEditForm()
+
+user_fields = ['*', 'nsAccountLock']
+
+class UserController(IPAController):
+
+ def __init__(self, *args, **kw):
+ super(UserController,self).__init__(*args, **kw)
+# self.load_custom_fields()
+
+ def get_email_domain(self):
+ client = self.get_ipaclient()
+
+ conf = client.get_ipa_config()
+ email_domain = conf.ipadefaultemaildomain
+
+ return email_domain
+
+ def load_custom_fields(self):
+
+ client = self.get_ipaclient()
+ schema = client.get_custom_fields()
+
+ # FIXME: Don't load from LDAP every single time it is called
+
+ # FIXME: Is removing the attributes on the fly thread-safe? Do we
+ # need to lock here?
+ for s in schema:
+ required=False
+ if (s['required'].lower() == "true"):
+ required=True
+ field = widgets.TextField(name=s['field'],label=s['label'])
+ validator = validators.String(not_empty=required)
+
+ # Don't allow dupes on the new form
+ try:
+ for i in range(len(user_new_form.custom_fields)):
+ if user_new_form.custom_fields[i].name == s['field']:
+ user_new_form.custom_fields.pop(i)
+ except:
+ pass
+
+ # Don't allow dupes on the edit form
+ try:
+ for i in range(len(user_edit_form.custom_fields)):
+ if user_edit_form.custom_fields[i].name == s['field']:
+ user_edit_form.custom_fields.pop(i)
+ except:
+ pass
+
+ # Don't allow dupes in the list of user fields
+ try:
+ for i in range(len(ipagui.forms.user.UserFields.custom_fields)):
+ if ipagui.forms.user.UserFields.custom_fields[i].name == s['field']:
+ ipagui.forms.user.UserFields.custom_fields.pop(i)
+ except:
+ pass
+
+ ipagui.forms.user.UserFields.custom_fields.append(field)
+ user_new_form.custom_fields.append(field)
+ user_edit_form.custom_fields.append(field)
+
+ user_new_form.validator.add_field(s['field'], validator)
+ user_edit_form.validator.add_field(s['field'], validator)
+
+ def initialize_mv_fields(self, user_dict):
+ """We use a separate attribute to store multi-values while on
+ the edit page. It is important that this be at least []. If
+ it is None it will cause an error to be thrown."""
+
+ # Load potential multi-valued fields
+ if isinstance(user_dict['cn'], basestring):
+ user_dict['cn'] = [user_dict['cn']]
+ user_dict['cns'] = ipahelper.setup_mv_fields(user_dict['cn'], 'cn')
+
+ if isinstance(user_dict.get('telephonenumber',''), basestring):
+ user_dict['telephonenumber'] = [user_dict.get('telephonenumber')]
+ user_dict['telephonenumbers'] = ipahelper.setup_mv_fields(user_dict.get('telephonenumber'), 'telephonenumber')
+
+ if isinstance(user_dict.get('facsimiletelephonenumber',''), basestring):
+ user_dict['facsimiletelephonenumber'] = [user_dict.get('facsimiletelephonenumber')]
+ user_dict['facsimiletelephonenumbers'] = ipahelper.setup_mv_fields(user_dict.get('facsimiletelephonenumber'), 'facsimiletelephonenumber')
+
+ if isinstance(user_dict.get('mobile',''), basestring):
+ user_dict['mobile'] = [user_dict.get('mobile')]
+ user_dict['mobiles'] = ipahelper.setup_mv_fields(user_dict.get('mobile'), 'mobile')
+
+ if isinstance(user_dict.get('pager',''), basestring):
+ user_dict['pager'] = [user_dict.get('pager')]
+ user_dict['pagers'] = ipahelper.setup_mv_fields(user_dict.get('pager'), 'pager')
+
+ if isinstance(user_dict.get('homephone',''), basestring):
+ user_dict['homephone'] = [user_dict.get('homephone')]
+ user_dict['homephones'] = ipahelper.setup_mv_fields(user_dict.get('homephone'), 'homephone')
+
+ return user_dict
+
+ @expose()
+ def index(self):
+ raise turbogears.redirect("/user/list")
+
+ @expose("ipagui.templates.usernew")
+ @identity.require(identity.in_any_group("admins","editors"))
+ def new(self, tg_errors=None):
+ """Displays the new user form"""
+ self.load_custom_fields()
+ if tg_errors:
+ turbogears.flash("There were validation errors.<br/>" +
+ "Please see the messages below for details.")
+
+ return dict(form=user_new_form, user={})
+
+ @expose()
+ @identity.require(identity.in_any_group("admins","editors"))
+ def create(self, **kw):
+ """Creates a new user"""
+ self.restrict_post()
+ client = self.get_ipaclient()
+
+ if kw.get('submit') == 'Cancel':
+ turbogears.flash("Add user cancelled")
+ raise turbogears.redirect('/user/list')
+
+ # Fix incoming multi-valued fields we created for the form
+ kw = ipahelper.fix_incoming_fields(kw, 'cn', 'cns')
+ kw = ipahelper.fix_incoming_fields(kw, 'telephonenumber', 'telephonenumbers')
+ kw = ipahelper.fix_incoming_fields(kw, 'facsimiletelephonenumber', 'facsimiletelephonenumbers')
+ kw = ipahelper.fix_incoming_fields(kw, 'mobile', 'mobiles')
+ kw = ipahelper.fix_incoming_fields(kw, 'pager', 'pagers')
+ kw = ipahelper.fix_incoming_fields(kw, 'homephone', 'homephones')
+
+ tg_errors, kw = self.usercreatevalidate(**kw)
+
+ if tg_errors:
+ turbogears.flash("There were validation errors.<br/>" +
+ "Please see the messages below for details.")
+ return dict(form=user_new_form, user=kw,
+ tg_template='ipagui.templates.usernew')
+
+ #
+ # Create the user itself
+ #
+ try:
+ new_user = ipa.user.User()
+ new_user.setValueNotEmpty('title', kw.get('title'))
+ new_user.setValueNotEmpty('givenname', kw.get('givenname'))
+ new_user.setValueNotEmpty('sn', kw.get('sn'))
+ new_user.setValueNotEmpty('cn', kw.get('cn'))
+ new_user.setValueNotEmpty('displayname', kw.get('displayname'))
+ new_user.setValueNotEmpty('initials', kw.get('initials'))
+
+ new_user.setValueNotEmpty('uid', kw.get('uid'))
+ new_user.setValueNotEmpty('loginshell', kw.get('loginshell'))
+ new_user.setValueNotEmpty('gecos', kw.get('gecos'))
+
+ new_user.setValueNotEmpty('mail', kw.get('mail'))
+ new_user.setValueNotEmpty('telephonenumber', kw.get('telephonenumber'))
+ new_user.setValueNotEmpty('facsimiletelephonenumber',
+ kw.get('facsimiletelephonenumber'))
+ new_user.setValueNotEmpty('mobile', kw.get('mobile'))
+ new_user.setValueNotEmpty('pager', kw.get('pager'))
+ new_user.setValueNotEmpty('homephone', kw.get('homephone'))
+
+ new_user.setValueNotEmpty('street', kw.get('street'))
+ new_user.setValueNotEmpty('l', kw.get('l'))
+ new_user.setValueNotEmpty('st', kw.get('st'))
+ new_user.setValueNotEmpty('postalcode', kw.get('postalcode'))
+
+ new_user.setValueNotEmpty('ou', kw.get('ou'))
+ new_user.setValueNotEmpty('businesscategory', kw.get('businesscategory'))
+ new_user.setValueNotEmpty('description', kw.get('description'))
+ new_user.setValueNotEmpty('employeetype', kw.get('employeetype'))
+ if kw.get('manager'):
+ new_user.setValueNotEmpty('manager', kw.get('manager'))
+ new_user.setValueNotEmpty('roomnumber', kw.get('roomnumber'))
+ if kw.get('secretary'):
+ new_user.setValueNotEmpty('secretary', kw.get('secretary'))
+
+ new_user.setValueNotEmpty('carlicense', kw.get('carlicense'))
+ new_user.setValueNotEmpty('labeleduri', kw.get('labeleduri'))
+
+ for custom_field in user_new_form.custom_fields:
+ new_user.setValueNotEmpty(custom_field.name,
+ kw.get(custom_field.name, ''))
+
+ rv = client.add_user(new_user)
+
+ if kw.get('nsAccountLock'):
+ client.mark_user_inactive(kw.get('uid'))
+ except ipaerror.exception_for(ipaerror.LDAP_DUPLICATE):
+ turbogears.flash("User with login '%s' already exists" %
+ kw.get('uid'))
+ return dict(form=user_new_form, user=kw,
+ tg_template='ipagui.templates.usernew')
+ except ipaerror.IPAError, e:
+ turbogears.flash("User add failed: " + str(e) + "<br/>" + e.detail[0].get('desc','') + ". " + e.detail[0].get('info',''))
+ return dict(form=user_new_form, user=kw,
+ tg_template='ipagui.templates.usernew')
+
+ #
+ # NOTE: from here on, the user account now exists.
+ # on any error, we redirect to the _edit_ user page.
+ # this code does data setup, similar to useredit()
+ #
+ user = client.get_user_by_uid(kw['uid'], user_fields)
+ user_dict = user.toDict()
+
+ user_groups_dicts = []
+ user_groups_data = b64encode(dumps(user_groups_dicts))
+
+ # store a copy of the original user for the update later
+ user_data = b64encode(dumps(user_dict))
+ user_dict['user_orig'] = user_data
+ user_dict['user_groups_data'] = user_groups_data
+
+ # preserve group add info in case of errors
+ user_dict['dnadd'] = kw.get('dnadd')
+ user_dict['dn_to_info_json'] = kw.get('dn_to_info_json')
+
+ #
+ # Set the Password
+ #
+ if kw.get('krbprincipalkey'):
+ try:
+ client.modifyPassword(user_dict['krbprincipalname'], "", kw.get('krbprincipalkey'))
+ except ipaerror.IPAError, e:
+ message = "User successfully created.<br />"
+ message += "There was an error setting the password.<br />"
+ turbogears.flash(message)
+ return dict(form=user_edit_form, user=user_dict,
+ user_groups=user_groups_dicts,
+ tg_template='ipagui.templates.useredit')
+
+ #
+ # Add groups
+ #
+ failed_adds = []
+ try:
+ dnadds = kw.get('dnadd')
+ cherrypy.session['uid'] = user_dict.get('uid')
+
+ # remove the default group from failed add
+ if dnadds != None:
+ try:
+ conf=client.get_ipa_config()
+ default_cn="cn=%s" % conf.getValue('ipadefaultprimarygroup')
+
+ if not(isinstance(dnadds,list) or isinstance(dnadds,tuple)):
+ dnadds = [dnadds]
+
+ for d in dnadds:
+ e = d.find(default_cn)
+ if e >= 0:
+ dnadds.remove(d)
+ except:
+ pass
+
+ if len(dnadds) > 0:
+ failed_adds = client.add_groups_to_user(
+ utf8_encode_values(dnadds), user.dn)
+ kw['dnadd'] = failed_adds
+ except ipaerror.IPAError, e:
+ failed_adds = dnadds
+
+ if len(failed_adds) > 0:
+ message = "User successfully created.<br />"
+ message += "There was an error adding groups.<br />"
+ message += "Failures have been preserved in the add/remove lists."
+ turbogears.flash(message)
+
+ # Setup any multi-value fields, otherwise you'll get:
+ # 'NoneType' object is not iterable
+ user_dict = self.initialize_mv_fields(user_dict)
+ return dict(form=user_edit_form, user=user_dict,
+ user_groups=user_groups_dicts,
+ tg_template='ipagui.templates.useredit')
+
+ turbogears.flash("%s added!" % kw['uid'])
+ print "Succeeded "
+ raise turbogears.redirect('/user/show', uid=kw['uid'])
+
+ @expose("ipagui.templates.dynamiceditsearch")
+ @identity.require(identity.not_anonymous())
+ def edit_search(self, **kw):
+ """Searches for groups and displays list of results in a table.
+ This method is used for the ajax search on the user edit page."""
+ client = self.get_ipaclient()
+
+ groups = []
+ groups_counter = 0
+ searchlimit = 100
+ criteria = kw.get('criteria')
+ if criteria != None and len(criteria) > 0:
+ try:
+ groups = client.find_groups(criteria.encode('utf-8'), None,
+ searchlimit)
+ groups_counter = groups[0]
+ groups = groups[1:]
+ except ipaerror.IPAError, e:
+ turbogears.flash("search failed: " + str(e))
+
+ return dict(users=None, groups=groups, criteria=criteria,
+ counter=groups_counter)
+
+
+ @expose("ipagui.templates.useredit")
+ @identity.require(identity.not_anonymous())
+ def edit(self, uid=None, principal=None, tg_errors=None):
+ """Displays the edit user form"""
+ self.load_custom_fields()
+ if tg_errors:
+ turbogears.flash("There were validation errors.<br/>" +
+ "Please see the messages below for details.")
+
+ client = self.get_ipaclient()
+
+ try:
+ if uid is not None:
+ user = client.get_user_by_uid(uid, user_fields)
+ elif principal is not None:
+ principal = principal + "@" + ipa.config.config.default_realm
+ user = client.get_user_by_principal(principal, user_fields)
+ else:
+ turbogears.flash("User edit failed: No uid or principal provided")
+ raise turbogears.redirect('/')
+ user_dict = user.toDict()
+
+ user_dict = self.initialize_mv_fields(user_dict)
+
+ # Edit shouldn't fill in the password field.
+ if user_dict.has_key('krbprincipalkey'):
+ del(user_dict['krbprincipalkey'])
+
+ user_dict['uid_hidden'] = user_dict.get('uid')
+
+ user_groups = client.get_groups_by_member(user.dn, ['dn', 'cn'])
+ user_groups.sort(self.sort_by_cn)
+ user_groups_dicts = map(lambda group: group.toDict(), user_groups)
+ user_groups_data = b64encode(dumps(user_groups_dicts))
+
+ # store a copy of the original user for the update later
+ user_data = b64encode(dumps(user_dict))
+ user_dict['user_orig'] = user_data
+ user_dict['user_groups_data'] = user_groups_data
+
+ # grab manager and secretary names
+ if user.manager:
+ try:
+ user_manager = client.get_entry_by_dn(user.manager,
+ ['givenname', 'sn', 'uid'])
+ user_dict['manager_cn'] = "%s %s" % (
+ user_manager.getValue('givenname', ''),
+ user_manager.getValue('sn', ''))
+ except (ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND),
+ ipaerror.exception_for(ipaerror.LDAP_DATABASE_ERROR)):
+ pass
+ if user.secretary:
+ try:
+ user_secretary = client.get_entry_by_dn(user.secretary,
+ ['givenname', 'sn', 'uid'])
+ user_dict['secretary_cn'] = "%s %s" % (
+ user_secretary.getValue('givenname', ''),
+ user_secretary.getValue('sn', ''))
+ except (ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND),
+ ipaerror.exception_for(ipaerror.LDAP_DATABASE_ERROR)):
+ pass
+
+ # Set the uid we're editing in the session. If it doesn't match
+ # later the update will not be processed
+ cherrypy.session['uid'] = user_dict.get('uid')
+
+ # Hack. The admin user doesn't have inetorgperson as an
+ # objectclass so don't require the givenName attribute if
+ # this objectclass doesn't exist in the record.
+ oc = [x.lower() for x in user_dict.get('objectclass')]
+ try:
+ p = oc.index('inetorgperson')
+ except ValueError:
+ # This entry doesn't have inetorgperson so don't require gn
+ user_edit_form.validator.fields.get('givenname').not_empty=False
+
+ return dict(form=user_edit_form, user=user_dict,
+ user_groups=user_groups_dicts)
+ except ipaerror.IPAError, e:
+ if uid is None:
+ uid = principal
+ turbogears.flash("User edit failed: " + str(e) + "<br/>" + e.detail[0].get('desc','') + ". " + e.detail[0].get('info',''))
+ raise turbogears.redirect('/user/show', uid=uid)
+
+ @expose()
+ @identity.require(identity.not_anonymous())
+ def update(self, **kw):
+ """Updates an existing user"""
+ self.restrict_post()
+ client = self.get_ipaclient()
+
+ if not kw.get('uid'):
+ kw['uid'] = kw.get('uid_hidden')
+
+ if kw.get('submit') == 'Cancel Edit':
+ turbogears.flash("Edit user cancelled")
+ raise turbogears.redirect('/user/show', uid=kw.get('uid'))
+
+ edituid = cherrypy.session.get('uid')
+ if edituid and edituid != kw.get('uid') and edituid != kw.get('uid_hidden'):
+ turbogears.flash("Something went wrong. You last viewed %s but are trying to update %s" % (kw.get('uid'), edituid))
+ raise turbogears.redirect('/user/show', uid=kw.get('uid'))
+
+ # Fix incoming multi-valued fields we created for the form
+ kw = ipahelper.fix_incoming_fields(kw, 'cn', 'cns')
+ kw = ipahelper.fix_incoming_fields(kw, 'telephonenumber', 'telephonenumbers')
+ kw = ipahelper.fix_incoming_fields(kw, 'facsimiletelephonenumber', 'facsimiletelephonenumbers')
+ kw = ipahelper.fix_incoming_fields(kw, 'mobile', 'mobiles')
+ kw = ipahelper.fix_incoming_fields(kw, 'pager', 'pagers')
+ kw = ipahelper.fix_incoming_fields(kw, 'homephone', 'homephones')
+
+ # admins and editors can update anybody. A user can only update
+ # themselves. We need this check because it is very easy to guess
+ # the edit URI.
+ if ((not 'admins' in turbogears.identity.current.groups and
+ not 'editors' in turbogears.identity.current.groups) and
+ (kw.get('uid_hidden') != turbogears.identity.current.display_name)):
+ turbogears.flash("You do not have permission to update this user.")
+ raise turbogears.redirect('/user/show', uid=kw.get('uid'))
+
+ if (kw.get('uid_hidden') == turbogears.identity.current.display_name and
+ kw.get('uid') != kw.get('uid_hidden')):
+ turbogears.flash("You cannot change your own login name.")
+ raise turbogears.redirect('/user/show', uid=kw.get('uid_hidden'))
+
+ # Decode the group data, in case we need to round trip
+ user_groups_dicts = loads(b64decode(kw.get('user_groups_data')))
+
+ tg_errors, kw = self.userupdatevalidate(**kw)
+ if tg_errors:
+ turbogears.flash("There were validation errors.<br/>" +
+ "Please see the messages below for details.")
+ return dict(form=user_edit_form, user=kw,
+ user_groups=user_groups_dicts,
+ tg_template='ipagui.templates.useredit')
+
+ # We don't want to inadvertantly add this to a record
+ try:
+ del kw['uid_hidden']
+ except KeyError:
+ pass
+
+ password_change = False
+ user_modified = False
+
+ #
+ # Update the user itself
+ #
+ try:
+ orig_user_dict = loads(b64decode(kw.get('user_orig')))
+
+ # remove multi-valued fields we created for the form
+ del(orig_user_dict['cns'])
+ del(orig_user_dict['telephonenumbers'])
+ del(orig_user_dict['facsimiletelephonenumbers'])
+ del(orig_user_dict['mobiles'])
+ del(orig_user_dict['pagers'])
+ del(orig_user_dict['homephones'])
+
+ new_user = ipa.user.User(orig_user_dict)
+ new_user.setValueNotEmpty('title', kw.get('title'))
+ new_user.setValueNotEmpty('givenname', kw.get('givenname'))
+ new_user.setValueNotEmpty('sn', kw.get('sn'))
+ new_user.setValueNotEmpty('cn', kw.get('cn'))
+ new_user.setValueNotEmpty('displayname', kw.get('displayname'))
+ new_user.setValueNotEmpty('initials', kw.get('initials'))
+
+ new_user.setValueNotEmpty('loginshell', kw.get('loginshell'))
+ new_user.setValueNotEmpty('gecos', kw.get('gecos'))
+
+ new_user.setValueNotEmpty('mail', kw.get('mail'))
+ new_user.setValueNotEmpty('telephonenumber', kw.get('telephonenumber'))
+ new_user.setValueNotEmpty('facsimiletelephonenumber',
+ kw.get('facsimiletelephonenumber'))
+ new_user.setValueNotEmpty('mobile', kw.get('mobile'))
+ new_user.setValueNotEmpty('pager', kw.get('pager'))
+ new_user.setValueNotEmpty('homephone', kw.get('homephone'))
+
+ new_user.setValueNotEmpty('street', kw.get('street'))
+ new_user.setValueNotEmpty('l', kw.get('l'))
+ new_user.setValueNotEmpty('st', kw.get('st'))
+ new_user.setValueNotEmpty('postalcode', kw.get('postalcode'))
+
+ new_user.setValueNotEmpty('ou', kw.get('ou'))
+ new_user.setValueNotEmpty('businesscategory', kw.get('businesscategory'))
+ new_user.setValueNotEmpty('description', kw.get('description'))
+ new_user.setValueNotEmpty('employeetype', kw.get('employeetype'))
+ new_user.setValueNotEmpty('manager', kw.get('manager'))
+ new_user.setValueNotEmpty('roomnumber', kw.get('roomnumber'))
+ new_user.setValueNotEmpty('secretary', kw.get('secretary'))
+
+ new_user.setValueNotEmpty('carlicense', kw.get('carlicense'))
+ new_user.setValueNotEmpty('labeleduri', kw.get('labeleduri'))
+
+ if kw.get('editprotected') == 'true':
+ if kw.get('krbprincipalkey'):
+ password_change = True
+ new_user.setValueNotEmpty('uidnumber', str(kw.get('uidnumber')))
+ new_user.setValueNotEmpty('gidnumber', str(kw.get('gidnumber')))
+ new_user.setValueNotEmpty('homedirectory', str(kw.get('homedirectory')))
+ new_user.setValueNotEmpty('uid', str(kw.get('uid')))
+
+ for custom_field in user_edit_form.custom_fields:
+ new_user.setValueNotEmpty(custom_field.name,
+ kw.get(custom_field.name, ''))
+
+ rv = client.update_user(new_user)
+ #
+ # If the user update succeeds, but below operations fail, we
+ # need to make sure a subsequent submit doesn't try to update
+ # the user again.
+ #
+ user_modified = True
+ kw['user_orig'] = b64encode(dumps(new_user.toDict()))
+ except ipaerror.exception_for(ipaerror.LDAP_EMPTY_MODLIST), e:
+ # could be a password change
+ # could be groups change
+ # too much work to figure out unless someone really screams
+ pass
+ except ipaerror.IPAError, e:
+ turbogears.flash("User update failed: " + str(e) + "<br/>" + e.detail[0].get('desc','') + ". " + e.detail[0].get('info',''))
+ return dict(form=user_edit_form, user=kw,
+ user_groups=user_groups_dicts,
+ tg_template='ipagui.templates.useredit')
+
+ #
+ # Password change
+ #
+ try:
+ if password_change:
+ rv = client.modifyPassword(orig_user_dict['krbprincipalname'], "", kw.get('krbprincipalkey'))
+ except ipaerror.IPAError, e:
+ turbogears.flash("User password change failed: " + str(e) + "<br/>" + e.detail[0].get('desc','') + ". " + e.detail[0].get('info',''))
+ return dict(form=user_edit_form, user=kw,
+ user_groups=user_groups_dicts,
+ tg_template='ipagui.templates.useredit')
+ except Exception, e:
+ turbogears.flash("User password change failed: " + str(e))
+ return dict(form=user_edit_form, user=kw,
+ user_groups=user_groups_dicts,
+ tg_template='ipagui.templates.useredit')
+
+ #
+ # Add groups
+ #
+ failed_adds = []
+ try:
+ dnadds = kw.get('dnadd')
+ if dnadds != None:
+ if not(isinstance(dnadds,list) or isinstance(dnadds,tuple)):
+ dnadds = [dnadds]
+ failed_adds = client.add_groups_to_user(
+ utf8_encode_values(dnadds), new_user.dn)
+ kw['dnadd'] = failed_adds
+ except ipaerror.IPAError, e:
+ failed_adds = dnadds
+
+ #
+ # Remove groups
+ #
+ failed_dels = []
+ try:
+ dndels = kw.get('dndel')
+ if dndels != None:
+ if not(isinstance(dndels,list) or isinstance(dndels,tuple)):
+ dndels = [dndels]
+ failed_dels = client.remove_groups_from_user(
+ utf8_encode_values(dndels), new_user.dn)
+ kw['dndel'] = failed_dels
+ except ipaerror.IPAError, e:
+ failed_dels = dndels
+
+ if (len(failed_adds) > 0) or (len(failed_dels) > 0):
+ message = "There was an error updating groups.<br />"
+ message += "Failures have been preserved in the add/remove lists."
+ if user_modified:
+ message = "User Details successfully updated.<br />" + message
+ if password_change:
+ message = "User password successfully updated.<br />" + message
+ turbogears.flash(message)
+ return dict(form=user_edit_form, user=kw,
+ user_groups=user_groups_dicts,
+ tg_template='ipagui.templates.useredit')
+
+ if kw.get('nsAccountLock') == '':
+ kw['nsAccountLock'] = "false"
+
+ try:
+ if kw.get('nsAccountLock') == "false" and new_user.getValues('nsaccountlock') == "true":
+ client.mark_user_active(kw.get('uid'))
+ elif kw.get('nsAccountLock') == "true" and new_user.nsaccountlock != "true":
+ client.mark_user_inactive(kw.get('uid'))
+ except ipaerror.IPAError, e:
+ turbogears.flash("User status change failed: " + str(e) + "<br/>" + e.detail[0].get('desc','') + ". " + e.detail[0].get('info',''))
+ return dict(form=user_edit_form, user=kw,
+ user_groups=user_groups_dicts,
+ tg_template='ipagui.templates.useredit')
+
+ turbogears.flash("%s updated!" % kw['uid'])
+ raise turbogears.redirect('/user/show', uid=kw['uid'])
+
+
+ @expose("ipagui.templates.userlist")
+ @identity.require(identity.not_anonymous())
+ def list(self, **kw):
+ """Searches for users and displays list of results"""
+ client = self.get_ipaclient()
+
+ users = None
+ counter = 0
+ uid = kw.get('uid')
+ if uid != None and len(uid) > 0:
+ try:
+ users = client.find_users(uid.encode('utf-8'), user_fields)
+ counter = users[0]
+ users = users[1:]
+ if counter == -1:
+ turbogears.flash("These results are truncated.<br />" +
+ "Please refine your search and try again.")
+ except ipaerror.IPAError, e:
+ turbogears.flash("User list failed: " + str(e) + "<br/>" + e.detail[0].get('desc','') + ". " + e.detail[0].get('info',''))
+ raise turbogears.redirect("/user/list")
+
+ return dict(users=users, uid=uid, fields=ipagui.forms.user.UserFields())
+
+
+ @expose("ipagui.templates.usershow")
+ @identity.require(identity.not_anonymous())
+ def show(self, uid):
+ """Retrieve a single user for display"""
+ client = self.get_ipaclient()
+ self.load_custom_fields()
+
+ try:
+ user = client.get_user_by_uid(uid, user_fields)
+ user_groups = client.get_groups_by_member(user.dn, ['cn'])
+ user_groups.sort(self.sort_by_cn)
+ user_reports = client.get_users_by_manager(user.dn,
+ ['givenname', 'sn', 'uid'])
+ user_reports.sort(self.sort_group_member)
+
+ user_manager = None
+ user_secretary = None
+ try:
+ if user.manager:
+ user_manager = client.get_entry_by_dn(user.manager,
+ ['givenname', 'sn', 'uid'])
+ except (ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND),
+ ipaerror.exception_for(ipaerror.LDAP_DATABASE_ERROR)):
+ pass
+
+ try:
+ if user.secretary:
+ user_secretary = client.get_entry_by_dn(user.secretary,
+ ['givenname', 'sn', 'uid'])
+ except (ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND),
+ ipaerror.exception_for(ipaerror.LDAP_DATABASE_ERROR)):
+ pass
+
+ return dict(user=user.toDict(), fields=ipagui.forms.user.UserFields(),
+ user_groups=user_groups, user_reports=user_reports,
+ user_manager=user_manager, user_secretary=user_secretary)
+ except ipaerror.IPAError, e:
+ turbogears.flash("User show failed: " + str(e) + "<br/>" + e.detail[0].get('desc','') + ". " + e.detail[0].get('info',''))
+ raise turbogears.redirect("/")
+
+ @expose()
+ @identity.require(identity.not_anonymous())
+ def delete(self, uid):
+ """Delete user."""
+ self.restrict_post()
+ client = self.get_ipaclient()
+
+ try:
+ client.delete_user(uid)
+
+ turbogears.flash("user deleted")
+ raise turbogears.redirect('/user/list')
+ except (SyntaxError, ipaerror.IPAError), e:
+ turbogears.flash("User deletion failed: " + str(e) + "<br/>" + e.detail[0].get('desc','') + ". " + e.detail[0].get('info',''))
+ raise turbogears.redirect('/user/list')
+
+ @validate(form=user_new_form)
+ @identity.require(identity.not_anonymous())
+ def usercreatevalidate(self, tg_errors=None, **kw):
+ return tg_errors, kw
+
+ @validate(form=user_edit_form)
+ @identity.require(identity.not_anonymous())
+ def userupdatevalidate(self, tg_errors=None, **kw):
+ return tg_errors, kw
+
+ # @expose()
+ def generate_password(self):
+ password = ""
+ generator = random.SystemRandom()
+ for char in range(8):
+ password += generator.choice(password_chars)
+
+ return password
+
+ @expose()
+ @identity.require(identity.not_anonymous())
+ def suggest_uid(self, givenname, sn):
+ # filter illegal uid characters out
+ givenname = re.sub(r'[^a-zA-Z_\-0-9]', "", givenname)
+ sn = re.sub(r'[^a-zA-Z_\-0-9]', "", sn)
+
+ if (len(givenname) == 0) or (len(sn) == 0):
+ return ""
+
+ client = self.get_ipaclient()
+
+ givenname = givenname.lower()
+ sn = sn.lower()
+
+ uid = givenname[0] + sn[:7]
+ try:
+ client.get_user_by_uid(uid)
+ except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND):
+ return uid
+
+ uid = givenname[:7] + sn[0]
+ try:
+ client.get_user_by_uid(uid)
+ except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND):
+ return uid
+
+ uid = (givenname + sn)[:8]
+ try:
+ client.get_user_by_uid(uid)
+ except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND):
+ return uid
+
+ uid = sn[:8]
+ try:
+ client.get_user_by_uid(uid)
+ except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND):
+ return uid
+
+ suffix = 2
+ template = givenname[0] + sn[:7]
+ while suffix < 20:
+ uid = template[:8 - len(str(suffix))] + str(suffix)
+ try:
+ client.get_user_by_uid(uid)
+ except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND):
+ return uid
+ suffix += 1
+
+ return ""
+
+ @expose()
+ @identity.require(identity.not_anonymous())
+ def suggest_email(self, givenname, sn):
+ # remove illegal email characters
+ givenname = re.sub(r'[^a-zA-Z0-9!#\$%\*/?\|\^\{\}`~&\'\+\-=_]', "", givenname)
+ sn = re.sub(r'[^a-zA-Z0-9!#\$%\*/?\|\^\{\}`~&\'\+\-=_]', "", sn)
+
+ if (len(givenname) == 0) or (len(sn) == 0):
+ return ""
+
+ client = self.get_ipaclient()
+
+ givenname = givenname.lower()
+ sn = sn.lower()
+
+ email = "%s.%s@%s" % (givenname, sn, self.get_email_domain())
+ try:
+ client.get_user_by_email(email)
+ except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND):
+ return email
+
+ email = "%s@%s" % (self.suggest_uid(givenname, sn), self.get_email_domain())
+ try:
+ client.get_user_by_email(email)
+ except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND):
+ return email
+
+ return ""
+
+ @expose("ipagui.templates.userselectsearch")
+ @identity.require(identity.not_anonymous())
+ def user_select_search(self, **kw):
+ """Searches for users and displays list of results in a table.
+ This method is used for the ajax search for managers
+ and secrectary on the user pages."""
+ client = self.get_ipaclient()
+
+ users = []
+ users_counter = 0
+ searchlimit = 100
+ criteria = kw.get('criteria')
+ if criteria != None and len(criteria) > 0:
+ try:
+ users = client.find_users(criteria.encode('utf-8'), None,
+ searchlimit)
+ users_counter = users[0]
+ users = users[1:]
+ except ipaerror.IPAError, e:
+ turbogears.flash("search failed: " + str(e) + "<br/>" + e.detail[0].get('desc','') + ". " + e.detail[0].get('info',''))
+
+ return dict(users=users, criteria=criteria,
+ which_select=kw.get('which_select'),
+ counter=users_counter)
diff --git a/ipa-server/ipa-gui/ipagui/templates/Makefile.am b/ipa-server/ipa-gui/ipagui/templates/Makefile.am
new file mode 100644
index 00000000..ddc8666c
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/templates/Makefile.am
@@ -0,0 +1,55 @@
+NULL =
+
+appdir = $(IPA_DATA_DIR)/ipagui/templates
+app_PYTHON = \
+ __init__.py \
+ $(NULL)
+
+app_DATA = \
+ delegateedit.kid \
+ delegateform.kid \
+ delegategroupsearch.kid \
+ delegatelayout.kid \
+ delegatelist.kid \
+ delegatenew.kid \
+ dynamiceditsearch.kid \
+ groupeditform.kid \
+ groupedit.kid \
+ grouplayout.kid \
+ grouplist.kid \
+ groupnewform.kid \
+ groupnew.kid \
+ groupshow.kid \
+ ipapolicyeditform.kid \
+ ipapolicyedit.kid \
+ ipapolicyshow.kid \
+ loginfailed.kid \
+ master.kid \
+ not_found.kid \
+ policyindex.kid \
+ policylayout.kid \
+ principallayout.kid \
+ principallist.kid \
+ principalshow.kid \
+ principalnewform.kid \
+ principalnew.kid \
+ usereditform.kid \
+ useredit.kid \
+ userlayout.kid \
+ userlist.kid \
+ usernewform.kid \
+ usernew.kid \
+ userselectsearch.kid \
+ usershow.kid \
+ welcome.kid \
+ unhandled_exception.kid \
+ $(NULL)
+
+EXTRA_DIST = \
+ $(app_DATA) \
+ $(NULL)
+
+MAINTAINERCLEANFILES = \
+ *~ \
+ *.pyc \
+ Makefile.in
diff --git a/ipa-server/ipa-gui/ipagui/templates/__init__.py b/ipa-server/ipa-gui/ipagui/templates/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/templates/__init__.py
diff --git a/ipa-server/ipa-gui/ipagui/templates/delegateedit.kid b/ipa-server/ipa-gui/ipagui/templates/delegateedit.kid
new file mode 100644
index 00000000..d9f6c3c4
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/templates/delegateedit.kid
@@ -0,0 +1,33 @@
+<!--
+ Copyright (C) 2007 Red Hat
+ see file 'COPYING' for use and warranty information
+
+ This program is free software; you can redistribute it and/or
+ modify it under the terms of the GNU General Public License as
+ published by the Free Software Foundation; version 2 only
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+-->
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:py="http://purl.org/kid/ns#"
+ py:extends="'delegatelayout.kid'">
+<head>
+<meta content="text/html; charset=utf-8" http-equiv="Content-Type" py:replace="''"/>
+<title>Edit Delegation</title>
+</head>
+<body>
+
+ <h1 class="accesscontrol">Edit Delegation</h1>
+
+ ${form.display(action=tg.url("/delegate/update"), value=delegate,
+ actionname='Update')}
+
+</body>
+</html>
diff --git a/ipa-server/ipa-gui/ipagui/templates/delegateform.kid b/ipa-server/ipa-gui/ipagui/templates/delegateform.kid
new file mode 100644
index 00000000..71d08a4f
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/templates/delegateform.kid
@@ -0,0 +1,213 @@
+<!--
+ Copyright (C) 2007 Red Hat
+ see file 'COPYING' for use and warranty information
+
+ This program is free software; you can redistribute it and/or
+ modify it under the terms of the GNU General Public License as
+ published by the Free Software Foundation; version 2 only
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+-->
+<div xmlns:py="http://purl.org/kid/ns#"
+ class="simpleroster">
+
+ <?python searchurl = tg.url('/delegate/group_search') ?>
+
+ <script type="text/javascript">
+ function lostFocus(which_group) {
+ /* The user has left the field, save what they put in there in case
+ * they don't do a Find. */
+ group_cn_field = $('form_' + which_group + '_group_cn');
+ group_criteria_field = $(which_group + '_criteria')
+ group_cn_field.value = group_criteria_field.value
+ }
+
+ function enterDoSearch(e, which_group) {
+ var keyPressed;
+ if (window.event) {
+ keyPressed = window.event.keyCode;
+ } else {
+ keyPressed = e.which;
+ }
+
+ if (keyPressed == 13) {
+ return doSearch(which_group);
+ } else {
+ return true;
+ }
+ }
+
+ function doSearch(which_group) {
+ $(which_group + '_searchresults').update("Searching...");
+ new Ajax.Updater(which_group + '_searchresults',
+ '${searchurl}',
+ { asynchronous:true,
+ parameters: { criteria: $(which_group + '_criteria').value,
+ which_group: which_group},
+ evalScripts: true });
+ return false;
+ }
+
+ function selectGroup(which_group, group_dn, group_cn) {
+ group_dn_field = $('form_' + which_group + '_group_dn');
+ group_cn_field = $('form_' + which_group + '_group_cn');
+ group_cn_span = $(which_group + '_group_cn');
+
+ group_dn_field.value = group_dn;
+ group_cn_field.value = group_cn;
+ group_cn_span.update(group_cn);
+
+ new Effect.Fade($(which_group + '_searcharea'), {duration: 0.25});
+ new Effect.Appear($(which_group + '_change_link'), {duration: 0.25});
+ }
+
+ function confirmDelete() {
+ if (confirm("Are you sure you want to delete this delegation?")) {
+ $('deleteform').submit();
+ }
+ return false;
+ }
+ </script>
+
+ <form style="display:none" id='deleteform'
+ method="post" action="${tg.url('/delegate/delete')}">
+ <input type="hidden" name="acistr" value="${value.get('orig_acistr')}" />
+ </form>
+
+ <form action="${action}" name="${name}" method="${method}" class="tableform">
+
+ <input type="submit" class="submitbutton" name="submit"
+ value="${actionname} Delegation"/>
+ <input type="submit" class="submitbutton" name="submit"
+ value="Cancel ${actionname}"/>
+ <span py:if='actionname == "Update"'>
+ <input type="button" class="deletebutton"
+ value="Delete Delegation"
+ onclick="return confirmDelete();"
+ />
+ </span>
+
+ <div py:for="field in hidden_fields"
+ py:replace="field.display(value_for(field), **params_for(field))"
+ />
+
+<h2>Delegation Details</h2>
+
+ <table class="formtable" cellpadding="2" cellspacing="0" border="0">
+ <tr>
+ <th valign="top">
+ <label class="fieldlabel" for="${delegate_fields.name.field_id}"
+ py:content="delegate_fields.name.label" />:
+ </th>
+ <td>
+ <span py:replace="delegate_fields.name.display(value_for(delegate_fields.name))" />
+ <span py:if="tg.errors.get('name')" class="fielderror"
+ py:content="tg.errors.get('name')" />
+ </td>
+ </tr>
+ <tr>
+ <th valign="top">
+ <label class="fieldlabel" for="${delegate_fields.source_group_cn.field_id}"
+ py:content="delegate_fields.source_group_cn.label" />:
+ </th>
+ <td>
+ <div>
+ <span id='source_group_cn'>${value_for(delegate_fields.source_group_cn)}</span>
+ <a href="#" id='source_change_link'
+ onclick="new Effect.Appear($('source_searcharea'), {duration: 0.25});
+ new Effect.Fade(this, {duration: 0.25});
+ return false;">change</a>
+ <span py:if="tg.errors.get('source_group_dn')" class="fielderror"
+ py:content="tg.errors.get('source_group_dn')" />
+ </div>
+ <div id="source_searcharea" style="display:none">
+ <input class="requiredfield" id="source_criteria" type="text"
+ onkeypress="return enterDoSearch(event, 'source');" onblur="return lostFocus('source');"/>
+ <input class="searchbutton" type="button" value="Find"
+ onclick="return doSearch('source');"
+ />
+ <div id="source_searchresults">
+ </div>
+ </div>
+ </td>
+ </tr>
+ <tr>
+ <th valign="top">
+ <label class="fieldlabel" for="${delegate_fields.attrs.field_id}"
+ py:content="delegate_fields.attrs.label" />:
+ </th>
+ <td valign="top">
+ <span py:if="tg.errors.get('attrs')" class="fielderror"
+ py:content="tg.errors.get('attrs')" />
+ <span py:replace="delegate_fields.attrs.display(value_for(delegate_fields.attrs))" />
+ </td>
+ </tr>
+ <tr>
+ <th valign="top">
+ <label class="fieldlabel" for="${delegate_fields.dest_group_cn.field_id}"
+ py:content="delegate_fields.dest_group_cn.label" />:
+ </th>
+ <td>
+ <div>
+ <span id='dest_group_cn'>${value_for(delegate_fields.dest_group_cn)}</span>
+ <a href="#" id='dest_change_link'
+ onclick="new Effect.Appear($('dest_searcharea'), {duration: 0.25});
+ new Effect.Fade(this, {duration: 0.25});
+ return false;">change</a>
+ <span py:if="tg.errors.get('dest_group_dn')" class="fielderror"
+ py:content="tg.errors.get('dest_group_dn')" />
+ </div>
+ <div id="dest_searcharea" style="display:none">
+ <div>
+ <input class="requiredfield" id="dest_criteria" type="text"
+ onkeypress="return enterDoSearch(event, 'dest');" onblur="return lostFocus('dest');"/>
+ <input class="searchbutton" type="button" value="Find"
+ onclick="return doSearch('dest');"
+ />
+ </div>
+ <div id="dest_searchresults">
+ </div>
+ </div>
+ </td>
+ </tr>
+ </table>
+
+<hr />
+
+ <input type="submit" class="submitbutton" name="submit"
+ value="${actionname} Delegation"/>
+ <input type="submit" class="submitbutton" name="submit"
+ value="Cancel ${actionname}"/>
+ <span py:if='actionname == "Update"'>
+ <input type="button" class="deletebutton"
+ value="Delete Delegation"
+ onclick="return confirmDelete();"
+ />
+ </span>
+
+ <script py:if="not value.get('source_group_dn')"
+ type="text/javascript">
+ new Effect.Appear($('source_searcharea'), {duration: 0.25});
+ new Effect.Fade($('source_change_link'), {duration: 0.25});
+ </script>
+ <script py:if="not value.get('dest_group_dn')"
+ type="text/javascript">
+ new Effect.Appear($('dest_searcharea'), {duration: 0.25});
+ new Effect.Fade($('dest_change_link'), {duration: 0.25});
+ </script>
+
+ </form>
+
+
+ <script type="text/javascript">
+ document.getElementById("form_name").focus();
+ </script>
+
+</div>
diff --git a/ipa-server/ipa-gui/ipagui/templates/delegategroupsearch.kid b/ipa-server/ipa-gui/ipagui/templates/delegategroupsearch.kid
new file mode 100644
index 00000000..f8f8b5c6
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/templates/delegategroupsearch.kid
@@ -0,0 +1,48 @@
+<!--
+ Copyright (C) 2007 Red Hat
+ see file 'COPYING' for use and warranty information
+
+ This program is free software; you can redistribute it and/or
+ modify it under the terms of the GNU General Public License as
+ published by the Free Software Foundation; version 2 only
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+-->
+<div xmlns:py="http://purl.org/kid/ns#">
+
+<?python
+from ipagui.helpers import ipahelper
+?>
+ <div py:if='(groups != None) and (len(groups) > 0)'>
+ <div id="search-results-count">
+ ${len(groups)} results returned:
+ <span py:if="counter &lt; 0">
+ (truncated)
+ </span>
+ </div>
+
+ <div py:for="group in groups">
+ <?python
+ group_dn_esc = ipahelper.javascript_string_escape(group.dn)
+ group_cn_esc = ipahelper.javascript_string_escape(group.cn)
+ which_group_esc = ipahelper.javascript_string_escape(which_group)
+ ?>
+
+ ${group.cn}
+ <a href=""
+ onclick="selectGroup('${which_group_esc}', '${group_dn_esc}', '${group_cn_esc}');
+ return false;"
+ >select</a>
+ </div>
+ </div>
+ <div py:if='(groups != None) and (len(groups) == 0)'>
+ No results found for "${criteria}"
+ </div>
+</div>
diff --git a/ipa-server/ipa-gui/ipagui/templates/delegatelayout.kid b/ipa-server/ipa-gui/ipagui/templates/delegatelayout.kid
new file mode 100644
index 00000000..7ece6cf9
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/templates/delegatelayout.kid
@@ -0,0 +1,34 @@
+<!--
+ Copyright (C) 2007 Red Hat
+ see file 'COPYING' for use and warranty information
+
+ This program is free software; you can redistribute it and/or
+ modify it under the terms of the GNU General Public License as
+ published by the Free Software Foundation; version 2 only
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+-->
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:py="http://purl.org/kid/ns#"
+ py:extends="'master.kid'">
+<head>
+</head>
+
+<body py:match="item.tag=='{http://www.w3.org/1999/xhtml}body'" py:attrs="item.items()">
+ <div id="main_content">
+<div id="details">
+ <div id="alertbox" py:if="value_of('tg_flash', None)"><p py:content="XML(tg_flash)"></p></div>
+
+ <div py:replace="[item.text]+item[:]"></div>
+ </div>
+</div>
+</body>
+
+</html>
diff --git a/ipa-server/ipa-gui/ipagui/templates/delegatelist.kid b/ipa-server/ipa-gui/ipagui/templates/delegatelist.kid
new file mode 100644
index 00000000..81ecfeb0
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/templates/delegatelist.kid
@@ -0,0 +1,93 @@
+<!--
+ Copyright (C) 2007 Red Hat
+ see file 'COPYING' for use and warranty information
+
+ This program is free software; you can redistribute it and/or
+ modify it under the terms of the GNU General Public License as
+ published by the Free Software Foundation; version 2 only
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+-->
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:py="http://purl.org/kid/ns#"
+ py:extends="'delegatelayout.kid'">
+<head>
+<meta content="text/html; charset=utf-8" http-equiv="Content-Type" py:replace="''"/>
+<title>Delegations</title>
+</head>
+<body>
+
+<?python
+from ipagui.helpers import ipahelper
+?>
+
+ <script type="text/javascript" charset="utf-8" src="${tg.url('/static/javascript/tablekit.js')}"></script>
+
+ <script type="text/javascript">
+ function editDelegation(acistr) {
+ $('edit_acistr').value = acistr;
+ $('editform').submit();
+ return false;
+ }
+ </script>
+
+ <form style="display:none" id='editform'
+ method="post" action="${tg.url('/delegate/edit')}">
+ <input type="hidden" id="edit_acistr" name="acistr" value="" />
+ </form>
+ <h1 class="accesscontrol">Delegations</h1>
+
+ <table id="resultstable" class="details sortable resizable">
+ <thead>
+ <tr>
+ <th>${fields.name.label}</th>
+ <th>${fields.source_group_cn.label}</th>
+ <th>${fields.attrs.label}</th>
+ <th>${fields.dest_group_cn.label}</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr py:for='aci in aci_list'>
+ <?python
+ source_cn = group_dn_to_cn.get(aci.source_group)
+ dest_cn = group_dn_to_cn.get(aci.dest_group)
+ acistr = aci.orig_acistr
+ acistr_esc = ipahelper.javascript_string_escape(acistr)
+ ?>
+ <td>
+ <a href="#" onclick="return editDelegation('${acistr_esc}');"
+ >${aci.name}</a>
+ </td>
+ <td>
+ <a href="${tg.url('/group/show', cn=source_cn)}"
+ >${source_cn}</a>
+ </td>
+ <td>
+ ${", ".join(aci.attrs)}
+ </td>
+ <td>
+ <a href="${tg.url('/group/show', cn=dest_cn)}"
+ >${dest_cn}</a>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+
+ <table border="0">
+ <tbody>
+ <tr>
+ <td>
+ <a href="${tg.url('/delegate/new')}">add new delegation</a><br />
+ </td>
+ </tr>
+ </tbody>
+ </table>
+</body>
+</html>
diff --git a/ipa-server/ipa-gui/ipagui/templates/delegatenew.kid b/ipa-server/ipa-gui/ipagui/templates/delegatenew.kid
new file mode 100644
index 00000000..12ba9e36
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/templates/delegatenew.kid
@@ -0,0 +1,31 @@
+<!--
+ Copyright (C) 2007 Red Hat
+ see file 'COPYING' for use and warranty information
+
+ This program is free software; you can redistribute it and/or
+ modify it under the terms of the GNU General Public License as
+ published by the Free Software Foundation; version 2 only
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+-->
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:py="http://purl.org/kid/ns#"
+ py:extends="'delegatelayout.kid'">
+<head>
+<meta content="text/html; charset=utf-8" http-equiv="Content-Type" py:replace="''"/>
+<title>Add Delegation</title>
+</head>
+<body>
+ <h1 class="accesscontrol">Add Delegation</h1>
+
+ ${form.display(action=tg.url("/delegate/create"), value=delegate,
+ actionname='Add')}
+</body>
+</html>
diff --git a/ipa-server/ipa-gui/ipagui/templates/dynamiceditsearch.kid b/ipa-server/ipa-gui/ipagui/templates/dynamiceditsearch.kid
new file mode 100644
index 00000000..2407f665
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/templates/dynamiceditsearch.kid
@@ -0,0 +1,97 @@
+<!--
+ Copyright (C) 2007 Red Hat
+ see file 'COPYING' for use and warranty information
+
+ This program is free software; you can redistribute it and/or
+ modify it under the terms of the GNU General Public License as
+ published by the Free Software Foundation; version 2 only
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+-->
+<div xmlns:py="http://purl.org/kid/ns#">
+
+<?python
+from ipagui.helpers import ipahelper
+#
+# This file is used to render the results from an AJAX search onto a page.
+# It has many expectations of the page being rendered into:
+# - Source page must have included dynamicedit.js and followed all of its
+# requirements
+#
+?>
+ <div id="search-results-count">
+ </div>
+ <?python
+ criteria_esc = ipahelper.javascript_string_escape(criteria)
+ ?>
+ <script type="text/javascript">
+ search_string = "${criteria_esc}";
+ results_counter = 0;
+ </script>
+ <?python search_div_counter = 1 ?>
+ <div py:for="entities in (users, groups)">
+ <div py:if='(entities != None) and (len(entities) > 0)'>
+ <div py:for="entity in entities" id="search-${search_div_counter}">
+ <?python
+ ent_dn_esc = ipahelper.javascript_string_escape(entity.dn)
+ ent_uid = entity.uid
+ if ent_uid:
+ ent_name = "%s %s" % (entity.getValue('givenName', ''),
+ entity.getValue('sn', ''))
+ ent_descr = "(%s)" % entity.uid
+ ent_type = "user"
+ else:
+ ent_name = entity.cn
+ ent_descr = "[group]"
+ ent_type = "group"
+ ent_name_esc = ipahelper.javascript_string_escape(ent_name)
+ ent_descr_esc = ipahelper.javascript_string_escape(ent_descr)
+ ent_type_esc = ipahelper.javascript_string_escape(ent_type)
+ ?>
+ <span id="search-info-${search_div_counter}"></span>
+ <script type="text/javascript">
+ if ((added_hash["${ent_dn_esc}"] == 1) ||
+ (member_hash["${ent_dn_esc}"] == 1)) {
+ $("search-${search_div_counter}").style.display = 'none';
+ } else {
+ results_counter = results_counter + 1;
+ }
+
+ renderMemberInfo($('search-info-${search_div_counter}'),
+ new MemberDisplayInfo('${ent_name_esc}',
+ '${ent_descr_esc}',
+ '${ent_type_esc}'));
+ </script>
+ <a href=""
+ onclick="addmemberHandler(this, '${ent_dn_esc}',
+ new MemberDisplayInfo('${ent_name_esc}',
+ '${ent_descr_esc}',
+ '${ent_type_esc}'));
+ return false;"
+ >add</a>
+ <?python
+ search_div_counter = search_div_counter + 1
+ ?>
+ </div>
+ </div>
+ </div>
+ <script type="text/javascript">
+ if (results_counter == 0) {
+ var message = "No results found for '" + search_string + "'";
+ } else {
+ var message = results_counter + " results found:";
+ }
+ $('search-results-count').appendChild(document.createTextNode(message));
+ </script>
+ <script py:if="counter &lt; 0">
+ $('search-results-count').appendChild(document.createTextNode(
+ " (truncated)"));
+ </script>
+</div>
diff --git a/ipa-server/ipa-gui/ipagui/templates/groupedit.kid b/ipa-server/ipa-gui/ipagui/templates/groupedit.kid
new file mode 100644
index 00000000..9614770f
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/templates/groupedit.kid
@@ -0,0 +1,36 @@
+<!--
+ Copyright (C) 2007 Red Hat
+ see file 'COPYING' for use and warranty information
+
+ This program is free software; you can redistribute it and/or
+ modify it under the terms of the GNU General Public License as
+ published by the Free Software Foundation; version 2 only
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+-->
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:py="http://purl.org/kid/ns#"
+ py:extends="'grouplayout.kid'">
+<head>
+ <meta content="text/html; charset=utf-8" http-equiv="Content-Type" py:replace="''"/>
+ <title>Edit Group</title>
+</head>
+<body>
+ <div id="details">
+ <h1 class="usergroup">Edit Group</h1>
+<input type="checkbox" id="toggleprotected_checkbox"
+ onclick="toggleProtectedFields(this);">
+ <span class="small">edit protected fields</span>
+ </input>
+
+ ${form.display(action=tg.url('/group/update'), value=group, members=members)}
+</div>
+</body>
+</html>
diff --git a/ipa-server/ipa-gui/ipagui/templates/groupeditform.kid b/ipa-server/ipa-gui/ipagui/templates/groupeditform.kid
new file mode 100644
index 00000000..78f76b0a
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/templates/groupeditform.kid
@@ -0,0 +1,289 @@
+<!--
+ Copyright (C) 2007 Red Hat
+ see file 'COPYING' for use and warranty information
+
+ This program is free software; you can redistribute it and/or
+ modify it under the terms of the GNU General Public License as
+ published by the Free Software Foundation; version 2 only
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+-->
+<div xmlns:py="http://purl.org/kid/ns#"
+ class="simpleroster">
+
+ <form style="display:none" id='deleteform'
+ method="post" action="${tg.url('/group/delete')}">
+ <input type="hidden" name="dn" value="${value.get('dn')}" />
+ </form>
+
+ <form action="${action}" name="${name}" method="${method}" class="tableform"
+ onsubmit="preSubmit()" >
+
+ <input type="submit" class="submitbutton" name="submit"
+ value="Update Group"/>
+ <input type="submit" class="submitbutton" name="submit"
+ value="Cancel Edit" />
+ <input type="button" class="deletebutton"
+ value="Delete Group"
+ onclick="return confirmDelete();"
+ />
+
+
+<?python
+from ipagui.helpers import ipahelper
+?>
+
+ <script type="text/javascript" charset="utf-8"
+ src="${tg.url('/static/javascript/dynamicedit.js')}"></script>
+ <script type="text/javascript" charset="utf-8"
+ src="${tg.url('/tg_widgets/tg_expanding_form_widget/javascript/expanding_form.js')}"></script>
+
+ <?python searchurl = tg.url('/group/edit_search') ?>
+
+ <script type="text/javascript">
+ function toggleProtectedFields(checkbox) {
+ var gidnumberField = $('form_gidnumber');
+ var cnField = $('form_cn');
+ if (checkbox.checked) {
+ gidnumberField.disabled = false;
+ cnField.disabled = false;
+ $('form_editprotected').value = 'true';
+ } else {
+ gidnumberField.disabled = true;
+ cnField.disabled = true;
+ $('form_editprotected').value = '';
+ }
+ }
+
+ function doSearch() {
+ $('searchresults').update("Searching...");
+ new Ajax.Updater('searchresults',
+ '${searchurl}',
+ { asynchronous:true,
+ parameters: { criteria: $('criteria').value },
+ evalScripts: true });
+ return false;
+ }
+
+ function confirmDelete() {
+ if (confirm("Are you sure you want to delete this group?")) {
+ $('deleteform').submit();
+ }
+ return false;
+ }
+ </script>
+
+ <div py:for="field in hidden_fields"
+ py:replace="field.display(value_for(field), **params_for(field))"
+ />
+
+ <h2 class="formsection">Group Details</h2>
+ <table class="formtable" cellpadding="2" cellspacing="0" border="0">
+ <tr>
+ <th>
+ <label class="fieldlabel" for="${group_fields.cn.field_id}"
+ py:content="group_fields.cn.label" />:
+ </th>
+ <td>
+ <span py:replace="group_fields.cn.display(value_for(group_fields.cn))" />
+ <span py:if="tg.errors.get('cn')" class="fielderror"
+ py:content="tg.errors.get('cn')" />
+ </td>
+ </tr>
+
+ <tr>
+ <th>
+ <label class="fieldlabel" for="${group_fields.description.field_id}"
+ py:content="group_fields.description.label" />:
+ </th>
+ <td>
+ <span py:replace="group_fields.description.display(value_for(group_fields.description))" />
+ <span py:if="tg.errors.get('description')" class="fielderror"
+ py:content="tg.errors.get('description')" />
+
+ <script type="text/javascript">
+ document.getElementById('form_cn').disabled = true;
+ </script>
+ </td>
+ </tr>
+
+ <tr>
+ <th>
+ <label class="fieldlabel" for="${group_fields.gidnumber.field_id}"
+ py:content="group_fields.gidnumber.label" />:
+ </th>
+ <td>
+ <span py:replace="group_fields.gidnumber.display(value_for(group_fields.gidnumber))" />
+ <span py:if="tg.errors.get('gidnumber')" class="fielderror"
+ py:content="tg.errors.get('gidnumber')" />
+
+ <script type="text/javascript">
+ document.getElementById('form_gidnumber').disabled = true;
+ </script>
+ </td>
+ </tr>
+ <tr>
+ <th>
+ <label class="fieldlabel" for="${group_fields.nsAccountLock.field_id}" py:content="group_fields.nsAccountLock.label" />:
+ </th>
+ <td>
+ <span py:replace="group_fields.nsAccountLock.display(value_for(group_fields.nsAccountLock))" />
+ <span py:if="tg.errors.get('nsAccountLock')" class="fielderror"
+ py:content="tg.errors.get('nsAccountLock')" />
+ </td>
+ </tr>
+ </table>
+
+ <div>
+ <h2 class="formsection">Group Members</h2>
+
+ <div class="floatlist">
+ <div class="floatheader">To Remove:</div>
+ <div id="delmembers">
+ </div>
+ </div>
+
+ <div>
+ <?python div_counter = 1 ?>
+ <div py:for="member in members" id="member-${div_counter}">
+ <?python
+ member_dn = member.get('dn')
+ member_dn_esc = ipahelper.javascript_string_escape(member_dn)
+
+ member_uid = member.get('uid')
+ member_inherited = member.get('inherited')
+ if member_uid:
+ member_name = "%s %s" % (member.get('givenName', ''),
+ member.get('sn', ''))
+ member_descr = "(%s)" % member.get('uid')
+ if member_inherited:
+ member_type = "iuser"
+ else:
+ member_type = "user"
+ else:
+ member_name = member.get('cn')
+ member_descr = "[group]"
+ if member_inherited:
+ member_type = "igroup"
+ else:
+ member_type = "group"
+ member_name_esc = ipahelper.javascript_string_escape(member_name)
+ member_descr_esc = ipahelper.javascript_string_escape(member_descr)
+ member_type_esc = ipahelper.javascript_string_escape(member_type)
+ ?>
+ <span id="member-info-${div_counter}"></span>
+ <script type="text/javascript">
+ renderMemberInfo($('member-info-${div_counter}'),
+ new MemberDisplayInfo('${member_name_esc}',
+ '${member_descr_esc}',
+ '${member_type_esc}'));
+ </script>
+ <a py:if="member_inherited != True" href="#"
+ onclick="removememberHandler(this, '${member_dn_esc}',
+ new MemberDisplayInfo('${member_name_esc}',
+ '${member_descr_esc}',
+ '${member_type_esc}'));
+ return false;"
+ >remove</a>
+ <script type="text/javascript">
+ dn_to_member_div_id['${member_dn_esc}'] = "member-${div_counter}";
+ member_hash["${member_dn_esc}"] = 1;
+ </script>
+ <?python
+ div_counter = div_counter + 1
+ ?>
+ </div>
+ &nbsp; <!-- a space here to prevent an empty div -->
+ </div>
+
+ </div>
+
+ <div style="clear:both">
+ <h2 class="formsection">Add Members</h2>
+
+ <div class="floatlist">
+ <div class="floatheader">To Add:</div>
+ <div id="newmembers">
+ </div>
+ </div>
+
+ <div>
+ <div id="search">
+ <input id="criteria" type="text" name="criteria"
+ onkeypress="return enterDoSearch(event);" />
+ <input class="searchbutton" type="button" value="Find"
+ onclick="return doSearch();"
+ />
+ </div>
+ <div id="searchresults">
+ </div>
+ </div>
+ </div>
+<hr />
+ <input type="submit" class="submitbutton" name="submit"
+ value="Update Group"/>
+ <input type="submit" class="submitbutton" name="submit"
+ value="Cancel Edit" />
+ <input type="button" class="deletebutton"
+ value="Delete Group"
+ onclick="return confirmDelete();"
+ />
+ </form>
+
+ <script type="text/javascript">
+ /*
+ * This section restores the contents of the add and remove lists
+ * dynamically if we have to refresh the page
+ */
+ if ($('form_dn_to_info_json').value != "") {
+ dn_to_info_hash = new Hash($('form_dn_to_info_json').value.evalJSON());
+ }
+
+ if ($('form_editprotected').value != "") {
+ $('toggleprotected_checkbox').checked = true;
+ toggleProtectedFields($('toggleprotected_checkbox'));
+ }
+ </script>
+
+ <?python
+ dnadds = value.get('dnadd', [])
+ if not(isinstance(dnadds,list) or isinstance(dnadds,tuple)):
+ dnadds = [dnadds]
+
+ dndels = value.get('dndel', [])
+ if not(isinstance(dndels,list) or isinstance(dndels,tuple)):
+ dndels = [dndels]
+ ?>
+
+ <script py:for="dnadd in dnadds">
+ <?python
+ dnadd_esc = ipahelper.javascript_string_escape(dnadd)
+ ?>
+ var dn = "${dnadd_esc}";
+ var info = dn_to_info_hash[dn];
+ var newdiv = addmember(dn, info);
+ if (newdiv != null) {
+ newdiv.style.display = 'block';
+ }
+ </script>
+
+ <script py:for="dndel in dndels">
+ <?python
+ dndel_esc = ipahelper.javascript_string_escape(dndel)
+ ?>
+ var dn = "${dndel_esc}";
+ var info = dn_to_info_hash[dn];
+ var newdiv = removemember(dn, info);
+ newdiv.style.display = 'block';
+ orig_div_id = dn_to_member_div_id[dn]
+ $(orig_div_id).style.display = 'none';
+ </script>
+
+</div>
diff --git a/ipa-server/ipa-gui/ipagui/templates/grouplayout.kid b/ipa-server/ipa-gui/ipagui/templates/grouplayout.kid
new file mode 100644
index 00000000..fe013ca0
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/templates/grouplayout.kid
@@ -0,0 +1,40 @@
+<!--
+ Copyright (C) 2007 Red Hat
+ see file 'COPYING' for use and warranty information
+
+ This program is free software; you can redistribute it and/or
+ modify it under the terms of the GNU General Public License as
+ published by the Free Software Foundation; version 2 only
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+-->
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:py="http://purl.org/kid/ns#"
+ py:extends="'master.kid'">
+<head>
+</head>
+
+<body py:match="item.tag=='{http://www.w3.org/1999/xhtml}body'" py:attrs="item.items()">
+ <div id="main_content">
+ <div id="alertbox" py:if="value_of('tg_flash', None)">
+ <p py:content="XML(tg_flash)"></p></div>
+
+ <div py:replace="[item.text]+item[:]"></div>
+ </div>
+
+<!-- <div id="sidebar">
+ <h2>Tools</h2>
+ <a href="${tg.url('/group/index')}">Add Group</a><br/>
+ <a href="${tg.url('/group/index')}">Find Group</a><br/>
+ <a href="${tg.url('/group/index')}">List Groups</a><br/>
+ </div> -->
+</body>
+
+</html>
diff --git a/ipa-server/ipa-gui/ipagui/templates/grouplist.kid b/ipa-server/ipa-gui/ipagui/templates/grouplist.kid
new file mode 100644
index 00000000..00fd03aa
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/templates/grouplist.kid
@@ -0,0 +1,93 @@
+<!--
+ Copyright (C) 2007 Red Hat
+ see file 'COPYING' for use and warranty information
+
+ This program is free software; you can redistribute it and/or
+ modify it under the terms of the GNU General Public License as
+ published by the Free Software Foundation; version 2 only
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+-->
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:py="http://purl.org/kid/ns#"
+ py:extends="'grouplayout.kid'">
+<head>
+<meta content="text/html; charset=utf-8" http-equiv="Content-Type" py:replace="''"/>
+<title>Find Groups</title>
+</head>
+<body>
+<div id="details">
+ <h1 class="usergroup">Find Groups</h1>
+ <script type="text/javascript" charset="utf-8" src="${tg.url('/static/javascript/tablekit.js')}"></script>
+ <div id="search">
+ <form action="${tg.url('/group/list')}" method="get">
+ <input id="criteria" type="text" name="criteria" value="${criteria}" />
+ <input type="submit" value="Find Groups"/>
+ </form>
+ <script type="text/javascript">
+ document.getElementById("criteria").focus();
+ </script>
+ </div>
+ <div py:if='(groups != None) and (len(groups) > 0)'>
+ <h2>${len(groups)} results returned:</h2>
+ <table id="resultstable" class="details sortable resizable" cellspacing="0">
+ <thead>
+ <tr>
+ <th>
+ ${fields.cn.label}
+ </th>
+ <th>
+ ${fields.description.label}
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr py:for="group in groups" py:if="group.nsAccountLock != 'true'">
+ <td>
+ <a href="${tg.url('/group/show',cn=group.cn)}">${group.cn}</a>
+ </td>
+ <td>
+ ${group.description}
+ </td>
+ </tr>
+ <tr id="inactive" py:for="group in groups" py:if="group.nsAccountLock == 'true'">
+ <td>
+ <a href="${tg.url('/group/show',cn=group.cn)}">${group.cn}</a>
+ </td>
+ <td>
+ ${group.description}
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <div py:if='(groups != None) and (len(groups) == 0)'>
+ <h2>No results found for "${criteria}"</h2>
+ </div>
+ <div class="instructions" py:if='groups == None'>
+ <p>
+ Search automatically looks across multiple fields. If you want to find
+ Joe in Finance, try typing "joe finance" into the search box.
+ </p>
+ <p>
+ Exact matches are listed first, followed by partial matches. If your search
+ is too broad, you will get a warning that the search returned too many
+ results. Try being more specific.
+ </p>
+ <p>
+ The results that come back are sortable. Simply click on a column
+ header to sort on that header. A triangle will indicate the sorted
+ column, along with its direction. Clicking and dragging between headers
+ will allow you to resize the header.
+ </p>
+ </div>
+</div>
+</body>
+</html>
diff --git a/ipa-server/ipa-gui/ipagui/templates/groupnew.kid b/ipa-server/ipa-gui/ipagui/templates/groupnew.kid
new file mode 100644
index 00000000..f5c83cae
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/templates/groupnew.kid
@@ -0,0 +1,32 @@
+<!--
+ Copyright (C) 2007 Red Hat
+ see file 'COPYING' for use and warranty information
+
+ This program is free software; you can redistribute it and/or
+ modify it under the terms of the GNU General Public License as
+ published by the Free Software Foundation; version 2 only
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+-->
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:py="http://purl.org/kid/ns#"
+ py:extends="'grouplayout.kid'">
+<head>
+ <meta content="text/html; charset=utf-8" http-equiv="Content-Type" py:replace="''"/>
+ <title>Add Group</title>
+</head>
+<body>
+<div id="details">
+ <h1 class="usergroup">Add Group</h1>
+
+ ${form.display(action=tg.url('/group/create'), value=group)}
+</div>
+</body>
+</html>
diff --git a/ipa-server/ipa-gui/ipagui/templates/groupnewform.kid b/ipa-server/ipa-gui/ipagui/templates/groupnewform.kid
new file mode 100644
index 00000000..ef321079
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/templates/groupnewform.kid
@@ -0,0 +1,149 @@
+<!--
+ Copyright (C) 2007 Red Hat
+ see file 'COPYING' for use and warranty information
+
+ This program is free software; you can redistribute it and/or
+ modify it under the terms of the GNU General Public License as
+ published by the Free Software Foundation; version 2 only
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+-->
+<div xmlns:py="http://purl.org/kid/ns#"
+ class="simpleroster">
+ <form action="${action}" name="${name}" method="${method}" class="tableform"
+ onsubmit="preSubmit()" >
+
+ <input type="submit" class="submitbutton" name="submit" value="Add Group"/>
+
+<?python
+from ipagui.helpers import ipahelper
+?>
+
+ <script type="text/javascript" charset="utf-8"
+ src="${tg.url('/static/javascript/dynamicedit.js')}"></script>
+
+ <?python searchurl = tg.url('/group/edit_search') ?>
+
+ <script type="text/javascript">
+ function doSearch() {
+ $('searchresults').update("Searching...");
+ new Ajax.Updater('searchresults',
+ '${searchurl}',
+ { asynchronous:true,
+ parameters: { criteria: $('criteria').value },
+ evalScripts: true });
+ return false;
+ }
+ </script>
+
+ <div py:for="field in hidden_fields"
+ py:replace="field.display(value_for(field), **params_for(field))"
+ />
+
+ <h2 class="formsection">Group Details</h2>
+ <table class="formtable" cellpadding="2" cellspacing="0" border="0">
+ <tr>
+ <th>
+ <label class="fieldlabel" for="${group_fields.cn.field_id}"
+ py:content="group_fields.cn.label" />:
+ </th>
+ <td>
+ <span py:replace="group_fields.cn.display(value_for(group_fields.cn))" />
+ <span py:if="tg.errors.get('cn')" class="fielderror"
+ py:content="tg.errors.get('cn')" />
+
+ </td>
+ </tr>
+
+ <tr>
+ <th>
+ <label class="fieldlabel" for="${group_fields.description.field_id}"
+ py:content="group_fields.description.label" />:
+ </th>
+ <td>
+ <span py:replace="group_fields.description.display(value_for(group_fields.description))" />
+ <span py:if="tg.errors.get('description')" class="fielderror"
+ py:content="tg.errors.get('description')" />
+
+ </td>
+ </tr>
+
+ <tr>
+ <th>
+ <label class="fieldlabel" for="${group_fields.gidnumber.field_id}"
+ py:content="group_fields.gidnumber.label" />:
+ </th>
+ <td>
+ Generated by server
+ </td>
+ </tr>
+ </table>
+
+ <div style="clear:both">
+ <h2 class="formsection">Add Members</h2>
+
+ <div class="floatlist">
+ <div class="floatheader">To Add:</div>
+ <div id="newmembers">
+ </div>
+ </div>
+
+ <div>
+ <div id="search">
+ <input id="criteria" type="text" name="criteria"
+ onkeypress="return enterDoSearch(event);" />
+ <input type="button" value="Find"
+ onclick="return doSearch();"
+ />
+ </div>
+ <div id="searchresults">
+ </div>
+ </div>
+ </div>
+
+<hr />
+
+ <input type="submit" class="submitbutton" name="submit" value="Add Group"/>
+
+ </form>
+
+ <script type="text/javascript">
+ document.getElementById("form_cn").focus();
+ </script>
+
+ <script type="text/javascript">
+ /*
+ * This section restores the contents of the add and remove lists
+ * dynamically if we have to refresh the page
+ */
+ if ($('form_dn_to_info_json').value != "") {
+ dn_to_info_hash = new Hash($('form_dn_to_info_json').value.evalJSON());
+ }
+ </script>
+
+ <?python
+ dnadds = value.get('dnadd', [])
+ if not(isinstance(dnadds,list) or isinstance(dnadds,tuple)):
+ dnadds = [dnadds]
+ ?>
+
+ <script py:for="dnadd in dnadds">
+ <?python
+ dnadd_esc = ipahelper.javascript_string_escape(dnadd)
+ ?>
+ var dn = "${dnadd_esc}";
+ var info = dn_to_info_hash[dn];
+ var newdiv = addmember(dn, info);
+ if (newdiv != null) {
+ newdiv.style.display = 'block';
+ }
+ </script>
+
+</div>
diff --git a/ipa-server/ipa-gui/ipagui/templates/groupshow.kid b/ipa-server/ipa-gui/ipagui/templates/groupshow.kid
new file mode 100644
index 00000000..d0ca6982
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/templates/groupshow.kid
@@ -0,0 +1,131 @@
+<!--
+ Copyright (C) 2007 Red Hat
+ see file 'COPYING' for use and warranty information
+
+ This program is free software; you can redistribute it and/or
+ modify it under the terms of the GNU General Public License as
+ published by the Free Software Foundation; version 2 only
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+-->
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:py="http://purl.org/kid/ns#"
+ py:extends="'grouplayout.kid'">
+<head>
+ <meta content="text/html; charset=utf-8" http-equiv="Content-Type" py:replace="''"/>
+ <title>View Group</title>
+</head>
+<body>
+<?python
+cn = group.get('cn')
+if isinstance(cn, list):
+ cn = cn[0]
+edit_url = tg.url('/group/edit', cn=cn)
+from ipagui.helpers import userhelper
+?>
+<div id="details">
+ <h1 class="usergroup">View Group</h1>
+
+ <input py:if="'editors' in tg.identity.groups or 'admins' in tg.identity.groups"
+ class="submitbutton" type="button"
+ onclick="document.location.href='${edit_url}'"
+ value="Edit Group" />
+
+ <h2 class="formsection">Group Details</h2>
+ <table class="formtable" cellpadding="2" cellspacing="0" border="0">
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="fields.cn.label" />:
+ </th>
+ <td>${group.get("cn")}</td>
+ </tr>
+
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="fields.description.label" />:
+ </th>
+ <td>${group.get("description")}</td>
+ </tr>
+
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="fields.gidnumber.label" />:
+ </th>
+ <td>${group.get("gidnumber")}</td>
+ </tr>
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="fields.nsAccountLock.label" />:
+ </th>
+ <td>${userhelper.account_status_display(group.get("nsAccountLock"))}</td>
+ </tr>
+ </table>
+
+ <h2 class="formsection">Group Members</h2>
+ <div py:for="member in members">
+ <?python
+
+ member_uid = member.get('uid')
+ member_inherited = member.get('inherited')
+ if member_uid:
+ member_cn = "%s %s" % (member.get('givenName', ''), member.get('sn', ''))
+ member_desc = "(%s)" % member_uid
+ if member_inherited:
+ member_type = "iuser"
+ else:
+ member_type = "user"
+ view_url = tg.url('/user/show', uid=member_uid)
+ else:
+ mem = member.get('cn')
+ if isinstance(mem, list):
+ mem = mem[0]
+ member_cn = "%s" % mem
+ member_desc = "[group]"
+ if member_inherited:
+ member_type = "igroup"
+ else:
+ member_type = "group"
+ view_url = tg.url('/group/show', cn=member_cn)
+ ?>
+ <span py:if='member_type == "user"'>
+ <b>
+ <a href="${view_url}"
+ >${member_cn}</a> ${member_desc}
+ </b>
+ </span>
+ <span py:if='member_type == "iuser"'>
+ <a href="${view_url}"
+ >${member_cn}</a> ${member_desc}
+ </span>
+ <span py:if='member_type == "group"'>
+ <b>
+ <i>
+ <a href="${view_url}"
+ >${member_cn}</a> ${member_desc}
+ </i>
+ </b>
+ </span>
+ <span py:if='member_type == "igroup"'>
+ <i>
+ <a href="${view_url}"
+ >${member_cn}</a> ${member_desc}
+ </i>
+ </span>
+ </div>
+
+ <br/>
+<hr />
+ <input py:if="'editors' in tg.identity.groups or 'admins' in tg.identity.groups"
+ class="submitbutton" type="button"
+ onclick="document.location.href='${edit_url}'"
+ value="Edit Group" />
+</div>
+</body>
+</html>
diff --git a/ipa-server/ipa-gui/ipagui/templates/ipapolicyedit.kid b/ipa-server/ipa-gui/ipagui/templates/ipapolicyedit.kid
new file mode 100644
index 00000000..6b071d2d
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/templates/ipapolicyedit.kid
@@ -0,0 +1,32 @@
+<!--
+ Copyright (C) 2007 Red Hat
+ see file 'COPYING' for use and warranty information
+
+ This program is free software; you can redistribute it and/or
+ modify it under the terms of the GNU General Public License as
+ published by the Free Software Foundation; version 2 only
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+-->
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:py="http://purl.org/kid/ns#"
+ py:extends="'policylayout.kid'">
+<head>
+ <meta content="text/html; charset=utf-8" http-equiv="Content-Type" py:replace="''"/>
+ <title>Edit IPA Policy</title>
+</head>
+<body>
+ <div>
+ <h1 class="policy">Edit IPA Policy</h1>
+
+ ${form.display(action=tg.url('/ipapolicy/update'), value=ipapolicy)}
+</div>
+</body>
+</html>
diff --git a/ipa-server/ipa-gui/ipagui/templates/ipapolicyeditform.kid b/ipa-server/ipa-gui/ipagui/templates/ipapolicyeditform.kid
new file mode 100644
index 00000000..a608829c
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/templates/ipapolicyeditform.kid
@@ -0,0 +1,280 @@
+<!--
+ Copyright (C) 2007 Red Hat
+ see file 'COPYING' for use and warranty information
+
+ This program is free software; you can redistribute it and/or
+ modify it under the terms of the GNU General Public License as
+ published by the Free Software Foundation; version 2 only
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+-->
+<div xmlns:py="http://purl.org/kid/ns#"
+ class="simpleroster">
+
+ <form action="${action}" name="${name}" method="${method}" class="tableform"
+ onsubmit="preSubmit()" >
+
+ <input type="submit" class="submitbutton" name="submit"
+ value="Update Policy"/>
+ <input type="submit" class="submitbutton" name="submit"
+ value="Cancel Edit" />
+
+<?python
+from ipagui.helpers import ipahelper
+?>
+
+ <script type="text/javascript" charset="utf-8"
+ src="${tg.url('/static/javascript/dynamicedit.js')}"></script>
+ <script type="text/javascript" charset="utf-8"
+ src="${tg.url('/tg_widgets/tg_expanding_form_widget/javascript/expanding_form.js')}"></script>
+
+ <div py:for="field in hidden_fields"
+ py:replace="field.display(value_for(field), **params_for(field))"
+ />
+
+ <h2 class="formsection">Search</h2>
+ <table class="formtable" cellpadding="2" cellspacing="0" border="0">
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="ipapolicy_fields.ipasearchtimelimit.label" />:
+ </th>
+ <td>
+ <span py:replace="ipapolicy_fields.ipasearchtimelimit.display(value_for(ipapolicy_fields.ipasearchtimelimit))" />
+ <span py:if="tg.errors.get('ipasearchtimelimit')" class="fielderror"
+ py:content="tg.errors.get('ipasearchtimelimit')" />
+ </td>
+ </tr>
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="ipapolicy_fields.ipasearchrecordslimit.label" />:
+ </th>
+ <td>
+ <span py:replace="ipapolicy_fields.ipasearchrecordslimit.display(value_for(ipapolicy_fields.ipasearchrecordslimit))" />
+ <span py:if="tg.errors.get('ipasearchrecordslimit')" class="fielderror"
+ py:content="tg.errors.get('ipasearchrecordslimit')" />
+ </td>
+ </tr>
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="ipapolicy_fields.ipausersearchfields.label" />:
+ </th>
+ <td>
+ <span py:replace="ipapolicy_fields.ipausersearchfields.display(value_for(ipapolicy_fields.ipausersearchfields))" />
+ <span py:if="tg.errors.get('ipausersearchfields')" class="fielderror"
+ py:content="tg.errors.get('ipausersearchfields')" />
+ </td>
+ </tr>
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="ipapolicy_fields.ipagroupsearchfields.label" />:
+ </th>
+ <td>
+ <span py:replace="ipapolicy_fields.ipagroupsearchfields.display(value_for(ipapolicy_fields.ipagroupsearchfields))" />
+ <span py:if="tg.errors.get('ipagroupsearchfields')" class="fielderror"
+ py:content="tg.errors.get('ipagroupsearchfields')" />
+ </td>
+ </tr>
+ </table>
+
+ <h2 class="formsection">Password Policy</h2>
+ <table class="formtable" cellpadding="2" cellspacing="0" border="0">
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="ipapolicy_fields.ipapwdexpadvnotify.label" />:
+ </th>
+ <td>
+ <span py:replace="ipapolicy_fields.ipapwdexpadvnotify.display(value_for(ipapolicy_fields.ipapwdexpadvnotify))" />
+ <span py:if="tg.errors.get('ipapwdexpadvnotify')" class="fielderror"
+ py:content="tg.errors.get('ipapwdexpadvnotify')" />
+ </td>
+ </tr>
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="ipapolicy_fields.krbminpwdlife.label" />:
+ </th>
+ <td>
+ <span py:replace="ipapolicy_fields.krbminpwdlife.display(value_for(ipapolicy_fields.krbminpwdlife))" />
+ <span py:if="tg.errors.get('krbminpwdlife')" class="fielderror"
+ py:content="tg.errors.get('krbminpwdlife')" />
+ </td>
+ </tr>
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="ipapolicy_fields.krbmaxpwdlife.label" />:
+ </th>
+ <td>
+ <span py:replace="ipapolicy_fields.krbmaxpwdlife.display(value_for(ipapolicy_fields.krbmaxpwdlife))" />
+ <span py:if="tg.errors.get('krbmaxpwdlife')" class="fielderror"
+ py:content="tg.errors.get('krbmaxpwdlife')" />
+ </td>
+ </tr>
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="ipapolicy_fields.krbpwdmindiffchars.label" />:
+ </th>
+ <td>
+ <span py:replace="ipapolicy_fields.krbpwdmindiffchars.display(value_for(ipapolicy_fields.krbpwdmindiffchars))" />
+ <span py:if="tg.errors.get('krbpwdmindiffchars')" class="fielderror"
+ py:content="tg.errors.get('krbpwdmindiffchars')" />
+ </td>
+ </tr>
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="ipapolicy_fields.krbpwdminlength.label" />:
+ </th>
+ <td>
+ <span py:replace="ipapolicy_fields.krbpwdminlength.display(value_for(ipapolicy_fields.krbpwdminlength))" />
+ <span py:if="tg.errors.get('krbpwdminlength')" class="fielderror"
+ py:content="tg.errors.get('krbpwdminlength')" />
+ </td>
+ </tr>
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="ipapolicy_fields.krbpwdhistorylength.label" />:
+ </th>
+ <td>
+ <span py:replace="ipapolicy_fields.krbpwdhistorylength.display(value_for(ipapolicy_fields.krbpwdhistorylength))" />
+ <span py:if="tg.errors.get('krbpwdhistorylength')" class="fielderror"
+ py:content="tg.errors.get('krbpwdhistorylength')" />
+ </td>
+ </tr>
+ </table>
+
+ <h2 class="formsection">User Settings</h2>
+ <table class="formtable" cellpadding="2" cellspacing="0" border="0">
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="ipapolicy_fields.ipamaxusernamelength.label" />:
+ </th>
+ <td>
+ <span py:replace="ipapolicy_fields.ipamaxusernamelength.display(value_for(ipapolicy_fields.ipamaxusernamelength))" />
+ <span py:if="tg.errors.get('ipamaxusernamelength')" class="fielderror"
+ py:content="tg.errors.get('ipamaxusernamelength')" />
+ </td>
+ </tr>
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="ipapolicy_fields.ipahomesrootdir.label" />:
+ </th>
+ <td>
+ <span py:replace="ipapolicy_fields.ipahomesrootdir.display(value_for(ipapolicy_fields.ipahomesrootdir))" />
+ <span py:if="tg.errors.get('ipahomesrootdir')" class="fielderror"
+ py:content="tg.errors.get('ipahomesrootdir')" />
+ </td>
+ </tr>
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="ipapolicy_fields.ipadefaultloginshell.label" />:
+ </th>
+ <td>
+ <span py:replace="ipapolicy_fields.ipadefaultloginshell.display(value_for(ipapolicy_fields.ipadefaultloginshell))" />
+ <span py:if="tg.errors.get('ipadefaultloginshell')" class="fielderror"
+ py:content="tg.errors.get('ipadefaultloginshell')" />
+ </td>
+ </tr>
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="ipapolicy_fields.ipadefaultprimarygroup.label" />:
+ </th>
+ <td>
+ <span py:replace="ipapolicy_fields.ipadefaultprimarygroup.display(value_for(ipapolicy_fields.ipadefaultprimarygroup))" />
+ <span py:if="tg.errors.get('ipadefaultprimarygroup')" class="fielderror"
+ py:content="tg.errors.get('ipadefaultprimarygroup')" />
+ </td>
+ </tr>
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="ipapolicy_fields.ipadefaultemaildomain.label" />:
+ </th>
+ <td>
+ <span py:replace="ipapolicy_fields.ipadefaultemaildomain.display(value_for(ipapolicy_fields.ipadefaultemaildomain))" />
+ <span py:if="tg.errors.get('ipadefaultemaildomain')" class="fielderror"
+ py:content="tg.errors.get('ipadefaultemaildomain')" />
+ </td>
+ </tr>
+ <!--
+ <tr>
+ <th>
+ <label class="fieldlabel" for="${ipapolicy_fields.userobjectclasses.field_id}"
+ py:content="ipapolicy_fields.userobjectclasses.label" />:
+ </th>
+ <td colspan="3">
+ <table class="formtable" cellpadding="2" cellspacing="0" border="0" id="${ipapolicy_fields.userobjectclasses.field_id}">
+ <tbody>
+ <?python repetition = 0
+ fld_index = 0
+ fld_error = tg.errors.get('ipauserobjectclasses')
+ ?>
+ <tr py:for="fld in value_for(ipapolicy_fields.ipauserobjectclasses)"
+ id="${ipapolicy_fields.userobjectclasses.field_id}_${repetition}"
+ class="${ipapolicy_fields.userobjectclasses.field_class}">
+
+ <td py:for="field in ipapolicy_fields.userobjectclasses.fields">
+ <span><input class="textfield" type="text" id="${ipapolicy_fields.userobjectclasses.field_id}_${repetition}_ipauserobjectclasses" name="userobjectclasses-${repetition}.ipauserobjectclasses" value="${fld}"/></span>
+ <span py:if="fld_error and fld_error[fld_index]" class="fielderror"
+ py:content="tg.errors.get('ipauserobjectclasses')" />
+ </td>
+ <?python fld_index = fld_index + 1 ?>
+ <td>
+ <a
+ href="javascript:ExpandingForm.removeItem('${ipapolicy_fields.userobjectclasses.field_id}_${repetition}')">Remove</a>
+ </td>
+ <?python repetition = repetition + 1?>
+ </tr>
+ </tbody>
+ </table>
+ <a id="${ipapolicy_fields.userobjectclasses.field_id}_doclink" href="javascript:ExpandingForm.addItem('${ipapolicy_fields.userobjectclasses.field_id}');">Add User Object Class</a>
+ </td>
+ </tr>
+ <tr>
+ <th>
+ <label class="fieldlabel" for="${ipapolicy_fields.groupobjectclasses.field_id}"
+ py:content="ipapolicy_fields.groupobjectclasses.label" />:
+ </th>
+ <td colspan="3">
+ <table class="formtable" cellpadding="2" cellspacing="0" border="0" id="${ipapolicy_fields.groupobjectclasses.field_id}">
+ <tbody>
+ <?python repetition = 0
+ fld_index = 0
+ fld_error = tg.errors.get('ipagroupobjectclasses')
+ ?>
+ <tr py:for="fld in value_for(ipapolicy_fields.ipagroupobjectclasses)"
+ id="${ipapolicy_fields.groupobjectclasses.field_id}_${repetition}"
+ class="${ipapolicy_fields.groupobjectclasses.field_class}">
+
+ <td py:for="field in ipapolicy_fields.groupobjectclasses.fields">
+ <span><input class="textfield" type="text" id="${ipapolicy_fields.groupobjectclasses.field_id}_${repetition}_ipagroupobjectclasses" name="groupobjectclasses-${repetition}.ipagroupobjectclasses" value="${fld}"/></span>
+ <span py:if="fld_error and fld_error[fld_index]" class="fielderror"
+ py:content="tg.errors.get('ipagroupobjectclasses')" />
+ </td>
+ <?python fld_index = fld_index + 1 ?>
+ <td>
+ <a
+ href="javascript:ExpandingForm.removeItem('${ipapolicy_fields.groupobjectclasses.field_id}_${repetition}')">Remove</a>
+ </td>
+ <?python repetition = repetition + 1?>
+ </tr>
+ </tbody>
+ </table>
+ <a id="${ipapolicy_fields.groupobjectclasses.field_id}_doclink" href="javascript:ExpandingForm.addItem('${ipapolicy_fields.groupobjectclasses.field_id}');">Add Group Object Class</a>
+ </td>
+ </tr>
+ -->
+ </table>
+
+ <hr/>
+
+ <input type="submit" class="submitbutton" name="submit"
+ value="Update Policy"/>
+ <input type="submit" class="submitbutton" name="submit"
+ value="Cancel Edit" />
+ </form>
+
+</div>
diff --git a/ipa-server/ipa-gui/ipagui/templates/ipapolicyshow.kid b/ipa-server/ipa-gui/ipagui/templates/ipapolicyshow.kid
new file mode 100644
index 00000000..3549a9f1
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/templates/ipapolicyshow.kid
@@ -0,0 +1,188 @@
+<!--
+ Copyright (C) 2007 Red Hat
+ see file 'COPYING' for use and warranty information
+
+ This program is free software; you can redistribute it and/or
+ modify it under the terms of the GNU General Public License as
+ published by the Free Software Foundation; version 2 only
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+-->
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:py="http://purl.org/kid/ns#"
+ py:extends="'policylayout.kid'">
+<head>
+<meta content="text/html; charset=utf-8" http-equiv="Content-Type" py:replace="''"/>
+<title>Manage IPA Policy</title>
+</head>
+<body>
+
+<?python
+from ipagui.helpers import ipahelper
+edit_url = tg.url('/ipapolicy/edit')
+?>
+
+ <script type="text/javascript" charset="utf-8" src="${tg.url('/static/javascript/tablekit.js')}"></script>
+
+ <h1 class="policy">Manage IPA Policy</h1>
+ <input class="submitbutton" type="button"
+ onclick="document.location.href='${edit_url}'"
+ value="Edit Policy" />
+
+ <h2 class="formsection">Search</h2>
+ <table class="formtable" cellpadding="2" cellspacing="0" border="0">
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="fields.ipasearchtimelimit.label" />:
+ </th>
+ <td>${ipapolicy.get("ipasearchtimelimit")}</td>
+ </tr>
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="fields.ipasearchrecordslimit.label" />:
+ </th>
+ <td>${ipapolicy.get("ipasearchrecordslimit")}</td>
+ </tr>
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="fields.ipausersearchfields.label" />:
+ </th>
+ <td>${ipapolicy.get("ipausersearchfields")}</td>
+ </tr>
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="fields.ipagroupsearchfields.label" />:
+ </th>
+ <td>${ipapolicy.get("ipagroupsearchfields")}</td>
+ </tr>
+ </table>
+
+ <h2 class="formsection">Password Policy</h2>
+ <table class="formtable" cellpadding="2" cellspacing="0" border="0">
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="fields.ipapwdexpadvnotify.label" />:
+ </th>
+ <td>${ipapolicy.get("ipapwdexpadvnotify")}</td>
+ </tr>
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="fields.krbminpwdlife.label" />:
+ </th>
+ <td>${password.get("krbminpwdlife")}</td>
+ </tr>
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="fields.krbmaxpwdlife.label" />:
+ </th>
+ <td>${password.get("krbmaxpwdlife")}</td>
+ </tr>
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="fields.krbpwdmindiffchars.label" />:
+ </th>
+ <td>${password.get("krbpwdmindiffchars")}</td>
+ </tr>
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="fields.krbpwdminlength.label" />:
+ </th>
+ <td>${password.get("krbpwdminlength")}</td>
+ </tr>
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="fields.krbpwdhistorylength.label" />:
+ </th>
+ <td>${password.get("krbpwdhistorylength")}</td>
+ </tr>
+ </table>
+ <h2 class="formsection">User Settings</h2>
+ <table class="formtable" cellpadding="2" cellspacing="0" border="0">
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="fields.ipamaxusernamelength.label" />:
+ </th>
+ <td>${ipapolicy.get("ipamaxusernamelength")}</td>
+ </tr>
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="fields.ipahomesrootdir.label" />:
+ </th>
+ <td>${ipapolicy.get("ipahomesrootdir")}</td>
+ </tr>
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="fields.ipadefaultloginshell.label" />:
+ </th>
+ <td>${ipapolicy.get("ipadefaultloginshell")}</td>
+ </tr>
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="fields.ipadefaultprimarygroup.label" />:
+ </th>
+ <td>${ipapolicy.get("ipadefaultprimarygroup")}</td>
+ </tr>
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="fields.ipadefaultemaildomain.label" />:
+ </th>
+ <td>${ipapolicy.get("ipadefaultemaildomain")}</td>
+ </tr>
+ <!--
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="fields.ipauserobjectclasses.label" />:
+ </th>
+ <td>
+ <table cellpadding="2" cellspacing="0" border="0">
+ <tbody>
+ <?python
+ index = 0
+ values = ipapolicy.get("ipauserobjectclasses", '')
+ if isinstance(values, str):
+ values = [values]
+ ?>
+ <tr py:for="index in range(len(values))">
+ <td>${values[index]}</td>
+ </tr>
+ </tbody>
+ </table>
+ </td>
+ </tr>
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="fields.ipagroupobjectclasses.label" />:
+ </th>
+ <td>
+ <table cellpadding="2" cellspacing="0" border="0">
+ <tbody>
+ <?python
+ index = 0
+ values = ipapolicy.get("ipagroupobjectclasses", '')
+ if isinstance(values, str):
+ values = [values]
+ ?>
+ <tr py:for="index in range(len(values))">
+ <td>${values[index]}</td>
+ </tr>
+ </tbody>
+ </table>
+ </td>
+ </tr>
+ -->
+ </table>
+<hr />
+ <input class="submitbutton" type="button"
+ onclick="document.location.href='${edit_url}'"
+ value="Edit Policy" />
+
+
+</body>
+</html>
diff --git a/ipa-server/ipa-gui/ipagui/templates/loginfailed.kid b/ipa-server/ipa-gui/ipagui/templates/loginfailed.kid
new file mode 100644
index 00000000..064cebbb
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/templates/loginfailed.kid
@@ -0,0 +1,41 @@
+<!--
+ Copyright (C) 2007 Red Hat
+ see file 'COPYING' for use and warranty information
+
+ This program is free software; you can redistribute it and/or
+ modify it under the terms of the GNU General Public License as
+ published by the Free Software Foundation; version 2 only
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+-->
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:py="http://purl.org/kid/ns#"
+ py:extends="'master.kid'">
+<head>
+<meta content="text/html; charset=utf-8" http-equiv="Content-Type" py:replace="''"/>
+<title>Permission Denied</title>
+</head>
+
+<body>
+ <div id="main_content">
+ <div id="details">
+ <div id="alertbox" py:if="value_of('tg_flash', None)">
+ <p py:content="XML(tg_flash)"></p></div>
+ <h1>Permission Denied</h1>
+ <div class="instructions">
+ <p>
+ You do not have permission to access this page.
+ </p>
+ </div>
+ </div>
+ </div>
+</body>
+
+</html>
diff --git a/ipa-server/ipa-gui/ipagui/templates/master.kid b/ipa-server/ipa-gui/ipagui/templates/master.kid
new file mode 100644
index 00000000..d8b34142
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/templates/master.kid
@@ -0,0 +1,121 @@
+<!--
+ Copyright (C) 2007 Red Hat
+ see file 'COPYING' for use and warranty information
+
+ This program is free software; you can redistribute it and/or
+ modify it under the terms of the GNU General Public License as
+ published by the Free Software Foundation; version 2 only
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+-->
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<?python import sitetemplate ?>
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:py="http://purl.org/kid/ns#" py:extends="sitetemplate">
+
+<head py:match="item.tag=='{http://www.w3.org/1999/xhtml}head'" py:attrs="item.items()">
+ <meta content="text/html; charset=UTF-8" http-equiv="content-type" py:replace="''"/>
+ <title py:replace="''">Your title goes here</title>
+ <meta py:replace="item[:]"/>
+ <style type="text/css" media="all">
+ @import "${tg.url('/static/css/style_platform.css')}";
+ @import "${tg.url('/static/css/style_platform-objects.css')}";
+ @import "${tg.url('/static/css/style_freeipa.css')}";
+ </style>
+ <script type="text/javascript" charset="utf-8" src="${tg.url('/static/javascript/prototype.js')}"></script>
+ <script type="text/javascript" charset="utf-8" src="${tg.url('/static/javascript/scriptaculous.js?load=effects')}"></script>
+ <script type="text/javascript" charset="utf-8" src="${tg.url('/static/javascript/ipautil.js')}"></script>
+</head>
+
+<body py:match="item.tag=='{http://www.w3.org/1999/xhtml}body'" py:attrs="item.items()">
+
+ <div id="head">
+ <h1><a href="${tg.url('/')}">Free IPA</a></h1>
+ <div id="headerinfo">
+ <div id="searchbar">
+ <form action="${tg.url('/topsearch')}" method="post">
+ <select name="searchtype">
+ <option>Users</option>
+ <option>Groups</option>
+ </select>
+ <input class="searchtext" id="topsearchbox" type="text"
+ name="searchvalue"
+ value="Type search terms here."
+ onfocus="clearsearch()" />
+ <input type="submit" value="Search"/>
+ </form>
+ <script type="text/javascript">
+ function clearsearch() {
+ topsearchbox = document.getElementById('topsearchbox');
+ topsearchbox.onfocus = null;
+ topsearchbox.value = "";
+ }
+ </script>
+ </div>
+ </div>
+</div>
+ <div id="navbar">
+<!-- hiding the tabs
+ <ul>
+ <li><a href="#">Overview</a></li>
+ <li class="active"><a href="#">Users</a></li>
+ <li><a href="#">Groups</a></li>
+ <li><a href="#">Resources</a></li>
+ <li><a href="#">Policy</a></li>
+ <li><a href="#">Search</a></li>
+ </ul>
+-->
+ <div id="login">
+ <div py:if="tg.config('identity.on') and not defined('logging_in')" id="pageLogin">
+ <span py:if="tg.identity.anonymous">
+ Kerberos login failed.
+ </span>
+ <span py:if="not tg.identity.anonymous">
+ Logged in as: ${tg.identity.user.display_name}
+ </span>
+ </div>
+ </div>
+
+
+ </div>
+
+ <div id="content">
+ <div id="sidebar">
+ <h2>Tasks</h2>
+ <ul>
+ <li py:if="'admins' in tg.identity.groups"><a href="${tg.url('/user/new')}">Add User</a></li>
+ <li><a href="${tg.url('/user/list')}">Find Users</a></li>
+ </ul>
+ <ul>
+ <li py:if="'admins' in tg.identity.groups"><a href="${tg.url('/group/new')}">Add Group</a></li>
+ <li><a href="${tg.url('/group/list')}">Find Groups</a></li>
+ </ul>
+ <ul py:if="'admins' in tg.identity.groups">
+ <li><a href="${tg.url('/principal/new')}">Add Service Principal</a></li>
+ <li><a href="${tg.url('/principal/list')}">Find Service Principal</a></li>
+ </ul>
+ <ul py:if="'admins' in tg.identity.groups">
+ <li><a href="${tg.url('/policy/index')}">Manage Policy</a></li>
+ </ul>
+ <ul>
+ <li py:if="not tg.identity.anonymous"><a href="${tg.url('/user/edit/', principal=tg.identity.user.display_name)}">Self Service</a></li>
+ </ul>
+ <ul py:if="'admins' in tg.identity.groups">
+ <li><a href="${tg.url('/delegate/list')}">Delegations</a></li>
+ </ul>
+ </div>
+
+ <div py:replace="[item.text]+item[:]"></div>
+
+
+ </div>
+
+</body>
+
+</html>
diff --git a/ipa-server/ipa-gui/ipagui/templates/not_found.kid b/ipa-server/ipa-gui/ipagui/templates/not_found.kid
new file mode 100644
index 00000000..0bc4c05c
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/templates/not_found.kid
@@ -0,0 +1,37 @@
+<!--
+ Copyright (C) 2007-2008 Red Hat
+ see file 'COPYING' for use and warranty information
+
+ This program is free software; you can redistribute it and/or
+ modify it under the terms of the GNU General Public License as
+ published by the Free Software Foundation; version 2 only
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+-->
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:py="http://purl.org/kid/ns#"
+ py:extends="'master.kid'">
+<head>
+<meta content="text/html; charset=utf-8" http-equiv="Content-Type" py:replace="''"/>
+<title>Page Not Found</title>
+</head>
+
+<body>
+ <div id="main_content">
+ <h1>Page Not Found</h1>
+ <div py:if='message'>
+ ${message}
+ </div>
+
+ </div>
+
+</body>
+</html>
diff --git a/ipa-server/ipa-gui/ipagui/templates/policyindex.kid b/ipa-server/ipa-gui/ipagui/templates/policyindex.kid
new file mode 100644
index 00000000..081b942d
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/templates/policyindex.kid
@@ -0,0 +1,48 @@
+<!--
+ Copyright (C) 2007 Red Hat
+ see file 'COPYING' for use and warranty information
+
+ This program is free software; you can redistribute it and/or
+ modify it under the terms of the GNU General Public License as
+ published by the Free Software Foundation; version 2 only
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+-->
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:py="http://purl.org/kid/ns#"
+ py:extends="'policylayout.kid'">
+<head>
+<meta content="text/html; charset=utf-8" http-equiv="Content-Type" py:replace="''"/>
+<title>Manage Policy</title>
+</head>
+<body>
+
+<?python
+from ipagui.helpers import ipahelper
+?>
+
+ <script type="text/javascript" charset="utf-8" src="${tg.url('/static/javascript/tablekit.js')}"></script>
+
+ <h1 class="policy">Manage Policy</h1>
+
+ <table>
+ <tbody>
+ <tr>
+ <td>
+ <a href="${tg.url('/ipapolicy/show')}"
+ >IPA Policy</a>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+
+
+</body>
+</html>
diff --git a/ipa-server/ipa-gui/ipagui/templates/policylayout.kid b/ipa-server/ipa-gui/ipagui/templates/policylayout.kid
new file mode 100644
index 00000000..7ece6cf9
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/templates/policylayout.kid
@@ -0,0 +1,34 @@
+<!--
+ Copyright (C) 2007 Red Hat
+ see file 'COPYING' for use and warranty information
+
+ This program is free software; you can redistribute it and/or
+ modify it under the terms of the GNU General Public License as
+ published by the Free Software Foundation; version 2 only
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+-->
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:py="http://purl.org/kid/ns#"
+ py:extends="'master.kid'">
+<head>
+</head>
+
+<body py:match="item.tag=='{http://www.w3.org/1999/xhtml}body'" py:attrs="item.items()">
+ <div id="main_content">
+<div id="details">
+ <div id="alertbox" py:if="value_of('tg_flash', None)"><p py:content="XML(tg_flash)"></p></div>
+
+ <div py:replace="[item.text]+item[:]"></div>
+ </div>
+</div>
+</body>
+
+</html>
diff --git a/ipa-server/ipa-gui/ipagui/templates/principallayout.kid b/ipa-server/ipa-gui/ipagui/templates/principallayout.kid
new file mode 100644
index 00000000..62ec92bc
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/templates/principallayout.kid
@@ -0,0 +1,36 @@
+<!--
+ Copyright (C) 2007 Red Hat
+ see file 'COPYING' for use and warranty information
+
+ This program is free software; you can redistribute it and/or
+ modify it under the terms of the GNU General Public License as
+ published by the Free Software Foundation; version 2 only
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+-->
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:py="http://purl.org/kid/ns#"
+ py:extends="'master.kid'">
+<head>
+</head>
+
+<body py:match="item.tag=='{http://www.w3.org/1999/xhtml}body'" py:attrs="item.items()">
+ <div id="main_content">
+ <div id="details">
+ <div id="alertbox" py:if="value_of('tg_flash', None)">
+ <p py:content="XML(tg_flash)"></p></div>
+
+ <div py:replace="[item.text]+item[:]"></div>
+ </div>
+
+ </div>
+</body>
+
+</html>
diff --git a/ipa-server/ipa-gui/ipagui/templates/principallist.kid b/ipa-server/ipa-gui/ipagui/templates/principallist.kid
new file mode 100644
index 00000000..9dc627ea
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/templates/principallist.kid
@@ -0,0 +1,82 @@
+<!--
+ Copyright (C) 2007 Red Hat
+ see file 'COPYING' for use and warranty information
+
+ This program is free software; you can redistribute it and/or
+ modify it under the terms of the GNU General Public License as
+ published by the Free Software Foundation; version 2 only
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+-->
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:py="http://purl.org/kid/ns#"
+ py:extends="'principallayout.kid'">
+<head>
+<meta content="text/html; charset=utf-8" http-equiv="Content-Type" py:replace="''"/>
+<title>Find Service Principals</title>
+</head>
+<body>
+ <h1 class="system">Find Service Principals</h1>
+ <script type="text/javascript" charset="utf-8" src="${tg.url('/static/javascript/tablekit.js')}"></script>
+ <div id="search">
+ <form action="${tg.url('/principal/list')}" method="get">
+ <input id="hostname" type="text" name="hostname" value="${hostname}" />
+ <input class="searchbutton" type="submit" value="Find Service Principals"/>
+ </form>
+ <script type="text/javascript">
+ document.getElementById("hostname").focus();
+ </script>
+ </div>
+ <div py:if='(principals != None) and (len(principals) > 0)'>
+ <h2>${len(principals)} results returned:</h2>
+ <table id="resultstable" class="details sortable resizable" cellspacing="0">
+ <thead>
+ <tr>
+ <th>
+ Hostname
+ </th>
+ <th>
+ Service
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr py:for="principal in principals">
+ <td>
+ <a href="${tg.url('/principal/show',principal=principal.krbprincipalname,principal_dn=principal.dn)}"
+ >${principal.hostname}</a>
+ </td>
+ <td>
+ <a href="${tg.url('/principal/show',principal=principal.krbprincipalname,principal_dn=principal.dn)}"
+ >${principal.service}</a>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <div id="alertbox" py:if='(principals != None) and (len(principals) == 0)'>
+ <p>No results found for "${hostname}"</p>
+ </div>
+
+ <div class="instructions" py:if='principals == None'>
+ <p>
+ Exact matches are listed first, followed by partial matches. If your search
+ is too broad, you will get a warning that the search returned too many
+ results. Try being more specific.
+ </p>
+ <p>
+ The results that come back are sortable. Simply click on a column
+ header to sort on that header. A triangle will indicate the sorted
+ column, along with its direction. Clicking and dragging between headers
+ will allow you to resize the header.
+ </p>
+ </div>
+</body>
+</html>
diff --git a/ipa-server/ipa-gui/ipagui/templates/principalnew.kid b/ipa-server/ipa-gui/ipagui/templates/principalnew.kid
new file mode 100644
index 00000000..b7e02891
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/templates/principalnew.kid
@@ -0,0 +1,30 @@
+<!--
+ Copyright (C) 2007 Red Hat
+ see file 'COPYING' for use and warranty information
+
+ This program is free software; you can redistribute it and/or
+ modify it under the terms of the GNU General Public License as
+ published by the Free Software Foundation; version 2 only
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+-->
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:py="http://purl.org/kid/ns#"
+ py:extends="'principallayout.kid'">
+<head>
+ <meta content="text/html; charset=utf-8" http-equiv="Content-Type" py:replace="''"/>
+ <title>Add Service Principal</title>
+</head>
+<body>
+ <h1 class="system">Add Service Principal</h1>
+
+ ${form.display(action=tg.url('/principal/create'), value=principal)}
+</body>
+</html>
diff --git a/ipa-server/ipa-gui/ipagui/templates/principalnewform.kid b/ipa-server/ipa-gui/ipagui/templates/principalnewform.kid
new file mode 100644
index 00000000..fe865b52
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/templates/principalnewform.kid
@@ -0,0 +1,119 @@
+<!--
+ Copyright (C) 2007 Red Hat
+ see file 'COPYING' for use and warranty information
+
+ This program is free software; you can redistribute it and/or
+ modify it under the terms of the GNU General Public License as
+ published by the Free Software Foundation; version 2 only
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+-->
+<div xmlns:py="http://purl.org/kid/ns#"
+ class="simpleroster">
+ <form action="${action}" name="${name}" method="${method}" class="tableform"
+ onsubmit="preSubmit()" >
+
+ <input type="submit" class="submitbutton" name="submit" value="Add Principal"/>
+
+<?python
+from ipagui.helpers import ipahelper
+?>
+
+ <script type="text/javascript" charset="utf-8"
+ src="${tg.url('/static/javascript/dynamicedit.js')}"></script>
+
+ <?python searchurl = tg.url('/principal/edit_search') ?>
+
+ <script type="text/javascript">
+ function toggleOther(field) {
+ otherField = document.getElementById('form_other');
+ var e=document.getElementById(field).value;
+ if ( e == "other") {
+ otherField.disabled = false;
+ } else {
+ otherField.disabled =true;
+ }
+ }
+
+ function doSearch() {
+ $('searchresults').update("Searching...");
+ new Ajax.Updater('searchresults',
+ '${searchurl}',
+ { asynchronous:true,
+ parameters: { criteria: $('criteria').value },
+ evalScripts: true });
+ return false;
+ }
+ </script>
+
+ <div py:for="field in hidden_fields"
+ py:replace="field.display(value_for(field), **params_for(field))"
+ />
+
+ <h2 class="formsection">Service Principal Details</h2>
+ <table class="formtable" cellpadding="2" cellspacing="0" border="0">
+ <tr>
+ <th>
+ <label class="fieldlabel" for="${principal_fields.hostname.field_id}"
+ py:content="principal_fields.hostname.label" />:
+ </th>
+ <td>
+ <span py:replace="principal_fields.hostname.display(value_for(principal_fields.hostname))" />
+ <span py:if="tg.errors.get('hostname')" class="fielderror"
+ py:content="tg.errors.get('hostname')" />
+
+ </td>
+ </tr>
+
+ <tr>
+ <th>
+ <label class="fieldlabel" for="${principal_fields.service.field_id}"
+ py:content="principal_fields.service.label" />:
+ </th>
+ <td>
+ <span py:replace="principal_fields.service.display(value_for(principal_fields.service))" />
+ <span py:if="tg.errors.get('service')" class="fielderror"
+ py:content="tg.errors.get('service')" />
+
+ </td>
+ </tr>
+
+ <tr>
+ <th>
+ <label class="fieldlabel" for="${principal_fields.other.field_id}"
+ py:content="principal_fields.other.label" />:
+ </th>
+ <td>
+ <span py:replace="principal_fields.other.display(value_for(principal_fields.other))" />
+ <span py:if="tg.errors.get('other')" class="fielderror"
+ py:content="tg.errors.get('other')" />
+ <script type="text/javascript">
+ var e=document.getElementById('form_service').value;
+ if ( e != "other") {
+ document.getElementById('form_other').disabled = true;
+ }
+ </script>
+
+ </td>
+ </tr>
+
+ </table>
+
+<hr />
+
+ <input type="submit" class="submitbutton" name="submit" value="Add Principal"/>
+
+ </form>
+
+ <script type="text/javascript">
+ document.getElementById("form_hostname").focus();
+ </script>
+
+</div>
diff --git a/ipa-server/ipa-gui/ipagui/templates/principalshow.kid b/ipa-server/ipa-gui/ipagui/templates/principalshow.kid
new file mode 100644
index 00000000..5904f034
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/templates/principalshow.kid
@@ -0,0 +1,70 @@
+<!--
+ Copyright (C) 2007 Red Hat
+ see file 'COPYING' for use and warranty information
+
+ This program is free software; you can redistribute it and/or
+ modify it under the terms of the GNU General Public License as
+ published by the Free Software Foundation; version 2 only
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+-->
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:py="http://purl.org/kid/ns#"
+ py:extends="'policylayout.kid'">
+<head>
+<meta content="text/html; charset=utf-8" http-equiv="Content-Type" py:replace="''"/>
+<title>View Service Principal</title>
+</head>
+<body>
+
+ <script type="text/javascript" charset="utf-8" src="${tg.url('/static/javascript/tablekit.js')}"></script>
+
+ <script type="text/javascript" charset="utf-8">
+ function confirmDelete() {
+ if (confirm("Are you sure you want to delete this service principal?")) {
+ $('deleteform').submit();
+ }
+ return false;
+ }
+ </script>
+
+ <form id='deleteform'
+ method="post" action="${tg.url('/principal/delete')}">
+
+ <input type="hidden" name="principal" value="${principal.get('principal_dn')}" />
+
+ <input type="submit" class="submitbutton"
+ value="Delete Principal"
+ onclick="return confirmDelete();"
+ />
+
+ <h1 class="system">View Service Principal</h1>
+
+ <h2 class="formsection">Principal</h2>
+ <table class="formtable" cellpadding="2" cellspacing="0" border="0">
+ <tr>
+ <th>
+ <label class="fieldlabel">Host</label>:
+ </th>
+ <td>${principal.get("hostname")}</td>
+ </tr>
+ <tr>
+ <th>
+ <label class="fieldlabel">Service</label>:
+ </th>
+ <td>${principal.get("service")}</td>
+ </tr>
+ </table>
+ </form>
+
+<hr />
+
+</body>
+</html>
diff --git a/ipa-server/ipa-gui/ipagui/templates/unhandled_exception.kid b/ipa-server/ipa-gui/ipagui/templates/unhandled_exception.kid
new file mode 100644
index 00000000..89aececc
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/templates/unhandled_exception.kid
@@ -0,0 +1,48 @@
+<!--
+ Copyright (C) 2007 Red Hat
+ see file 'COPYING' for use and warranty information
+
+ This program is free software; you can redistribute it and/or
+ modify it under the terms of the GNU General Public License as
+ published by the Free Software Foundation; version 2 only
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+-->
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:py="http://purl.org/kid/ns#"
+ py:extends="'master.kid'">
+<head>
+<meta content="text/html; charset=utf-8" http-equiv="Content-Type" py:replace="''"/>
+<title>Error</title>
+</head>
+
+<body>
+ <div id="main_content">
+ <h1>An unexpected error occured</h1>
+
+ <div py:if='message'>
+ <b>Message:</b>
+ <pre>${message}</pre>
+ </div>
+
+ <div py:if='error_msg'>
+ <b>HTTP Error Message:</b>
+ <pre>${error_msg}</pre>
+ </div>
+
+ <div py:if='details'>
+ <b>Stack Trace:</b>
+ <pre>${details}</pre>
+ </div>
+ </div>
+
+</body>
+</html>
diff --git a/ipa-server/ipa-gui/ipagui/templates/useredit.kid b/ipa-server/ipa-gui/ipagui/templates/useredit.kid
new file mode 100644
index 00000000..9633b53a
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/templates/useredit.kid
@@ -0,0 +1,57 @@
+<!--
+ Copyright (C) 2007 Red Hat
+ see file 'COPYING' for use and warranty information
+
+ This program is free software; you can redistribute it and/or
+ modify it under the terms of the GNU General Public License as
+ published by the Free Software Foundation; version 2 only
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+-->
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:py="http://purl.org/kid/ns#"
+ py:extends="'userlayout.kid'">
+<head>
+<meta content="text/html; charset=utf-8" http-equiv="Content-Type" py:replace="''"/>
+<title>Edit User</title>
+</head>
+<body>
+
+ <div>
+ <div style="float:right">
+ <input type="checkbox" id="toggleprotected_checkbox"
+ onclick="toggleProtectedFields(this);">
+ <span class="small">edit protected fields</span>
+ </input>
+ </div>
+ <h1 class="user">Edit User</h1>
+ </div>
+
+<?python
+from ipagui.helpers import userhelper
+pw_expires_days = userhelper.password_expires_in(user.get("krbPasswordExpiration"))
+pw_expires_soon = userhelper.password_expires_soon(pw_expires_days)
+pw_is_expired = userhelper.password_is_expired(pw_expires_days)
+if pw_expires_days != 1:
+ days_suffix = "s"
+else:
+ days_suffix = ""
+?>
+
+ <div py:if='pw_expires_soon' class="warning_message">
+ ${user.get("uid")}'s password will expire in ${pw_expires_days} day${days_suffix}
+ </div>
+ <div py:if='pw_is_expired' class="warning_message">
+ ${user.get("uid")}'s password has expired
+ </div>
+
+ ${form.display(action=tg.url('/user/update'), value=user, user_groups=user_groups)}
+</body>
+</html>
diff --git a/ipa-server/ipa-gui/ipagui/templates/usereditform.kid b/ipa-server/ipa-gui/ipagui/templates/usereditform.kid
new file mode 100644
index 00000000..b2a3c39c
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/templates/usereditform.kid
@@ -0,0 +1,949 @@
+<!--
+ Copyright (C) 2007 Red Hat
+ see file 'COPYING' for use and warranty information
+
+ This program is free software; you can redistribute it and/or
+ modify it under the terms of the GNU General Public License as
+ published by the Free Software Foundation; version 2 only
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+-->
+<div xmlns:py="http://purl.org/kid/ns#"
+ class="simpleroster">
+
+ <form style="display:none" id='deleteform'
+ method="post" action="${tg.url('/user/delete')}">
+ <input type="hidden" name="uid" value="${value.get('uid')}" />
+ </form>
+
+ <form action="${action}" name="${name}" method="${method}" class="tableform"
+ onsubmit="preSubmit()">
+
+ <input type="submit" class="submitbutton" name="submit"
+ value="Update User"/>
+ <input type="submit" class="submitbutton" name="submit"
+ value="Cancel Edit" />
+ <input py:if="'editors' in tg.identity.groups or 'admins' in tg.identity.groups and tg.identity.display_name != value.get('uid')"
+ type="button" class="submitbutton"
+ value="Delete User"
+ onclick="return confirmDelete();"
+ />
+
+<?python
+from ipagui.helpers import ipahelper
+?>
+
+ <script type="text/javascript" charset="utf-8"
+ src="${tg.url('/static/javascript/dynamicedit.js')}"></script>
+ <script type="text/javascript" charset="utf-8"
+ src="${tg.url('/static/javascript/dynamicselect.js')}"></script>
+ <script type="text/javascript" charset="utf-8"
+ src="${tg.url('/tg_widgets/tg_expanding_form_widget/javascript/expanding_form.js')}"></script>
+
+ <?python
+ searchurl = tg.url('/user/edit_search')
+ selectSearchurl = tg.url('/user/user_select_search')
+ ?>
+
+ <script type="text/javascript">
+ function toggleProtectedFields(checkbox) {
+ passwordField = document.getElementById('form_krbprincipalkey');
+ passwordConfirmField = document.getElementById('form_krbprincipalkey_confirm');
+ uidField = document.getElementById('form_uid');
+ uidnumberField = document.getElementById('form_uidnumber');
+ gidnumberField = document.getElementById('form_gidnumber');
+ homedirectoryField = document.getElementById('form_homedirectory');
+ if (checkbox.checked) {
+ passwordField.disabled = false;
+ passwordConfirmField.disabled = false;
+ uidField.disabled = false;
+ uidnumberField.disabled = false;
+ gidnumberField.disabled = false;
+ homedirectoryField.disabled = false;
+ $('form_editprotected').value = 'true';
+ } else {
+ passwordField.disabled = true;
+ passwordConfirmField.disabled = true;
+ uidField.disabled = true;
+ uidnumberField.disabled = true;
+ gidnumberField.disabled = true;
+ homedirectoryField.disabled = true;
+ $('form_editprotected').value = '';
+ }
+ }
+
+ function warnRDN() {
+ if (confirm("Are you sure you want to change the login name?\nThis can have unexpected results. Additionally, a password change will be required.")) {
+ return true;
+ }
+ return false;
+ }
+
+ function doSearch() {
+ $('searchresults').update("Searching...");
+ new Ajax.Updater('searchresults',
+ '${searchurl}',
+ { asynchronous:true,
+ parameters: { criteria: $('criteria').value },
+ evalScripts: true });
+ return false;
+ }
+
+ // override dynamicedit.js version
+ // we don't need to show [group] nor italize groups
+ function renderMemberInfo(newdiv, info) {
+ if (info.type == "group") {
+ newdiv.appendChild(document.createTextNode(
+ info.name + " "));
+ }
+ }
+
+ function doSelectSearch(which_select) {
+ $(which_select + '_searchresults').update("Searching...");
+ new Ajax.Updater(which_select + '_searchresults',
+ '${selectSearchurl}',
+ { asynchronous:true,
+ parameters: { criteria: $(which_select + '_criteria').value,
+ which_select: which_select},
+ evalScripts: true });
+ return false;
+ }
+
+ function confirmDelete() {
+ if (confirm("Are you sure you want to delete this person?")) {
+ $('deleteform').submit();
+ }
+ return false;
+ }
+ </script>
+
+
+ <div py:for="field in hidden_fields"
+ py:replace="field.display(value_for(field), **params_for(field))"
+ />
+
+ <h2 class="formsection">Identity Details</h2>
+ <table class="formtable" cellpadding="2" cellspacing="0" border="0">
+ <tr>
+ <th>
+ <label class="fieldlabel" for="${user_fields.title.field_id}"
+ py:content="user_fields.title.label" />:
+ </th>
+ <td>
+ <span py:replace="user_fields.title.display(value_for(user_fields.title))" />
+ <span py:if="tg.errors.get('title')" class="fielderror"
+ py:content="tg.errors.get('title')" />
+
+ </td>
+ </tr>
+
+ <tr>
+ <th>
+ <label class="fieldlabel" for="${user_fields.givenname.field_id}"
+ py:content="user_fields.givenname.label" />:
+ </th>
+ <td>
+ <span py:replace="user_fields.givenname.display(value_for(user_fields.givenname))" />
+ <span py:if="tg.errors.get('givenname')" class="fielderror"
+ py:content="tg.errors.get('givenname')" />
+
+ </td>
+ </tr>
+
+ <tr>
+ <th>
+ <label class="fieldlabel" for="${user_fields.sn.field_id}"
+ py:content="user_fields.sn.label" />:
+ </th>
+ <td>
+ <span py:replace="user_fields.sn.display(value_for(user_fields.sn))" />
+ <span py:if="tg.errors.get('sn')" class="fielderror"
+ py:content="tg.errors.get('sn')" />
+ </td>
+ </tr>
+
+ <tr>
+ <th>
+ <label class="fieldlabel" for="${user_fields.cns.field_id}"
+ py:content="user_fields.cns.label" />:
+ </th>
+ <td colspan="3">
+ <table class="formtable" cellpadding="2" cellspacing="0" border="0" id="${user_fields.cns.field_id}">
+ <tbody>
+ <?python repetition = 0
+ cn_index = 0
+ cn_error = tg.errors.get('cn')
+ ?>
+ <tr py:for="cn in value_for(user_fields.cn)"
+ id="${user_fields.cns.field_id}_${repetition}"
+ class="${user_fields.cns.field_class}">
+
+ <td py:for="field in user_fields.cns.fields">
+ <span><input class="textfield" type="text" id="${user_fields.cns.field_id}_${repetition}_cn" name="cns-${repetition}.cn" value="${cn}"/></span>
+ <span py:if="cn_error and cn_error[cn_index]" class="fielderror"
+ py:content="tg.errors.get('cn')" />
+ </td>
+ <?python cn_index = cn_index + 1 ?>
+ <td>
+ <a
+ href="javascript:ExpandingForm.removeItem('${user_fields.cns.field_id}_${repetition}')">Remove</a>
+ </td>
+ <?python repetition = repetition + 1?>
+ </tr>
+ </tbody>
+ </table>
+ <a id="${user_fields.cns.field_id}_doclink" href="javascript:ExpandingForm.addItem('${user_fields.cns.field_id}');">Add Full Name</a>
+ </td>
+ </tr>
+
+ <tr>
+ <th>
+ <label class="fieldlabel" for="${user_fields.displayname.field_id}"
+ py:content="user_fields.displayname.label" />:
+ </th>
+ <td>
+ <span py:replace="user_fields.displayname.display(value_for(user_fields.displayname))" />
+ <span py:if="tg.errors.get('displayname')" class="fielderror"
+ py:content="tg.errors.get('displayname')" />
+
+ </td>
+ </tr>
+
+ <tr>
+ <th>
+ <label class="fieldlabel" for="${user_fields.initials.field_id}"
+ py:content="user_fields.initials.label" />:
+ </th>
+ <td>
+ <span py:replace="user_fields.initials.display(value_for(user_fields.initials))" />
+ <span py:if="tg.errors.get('initials')" class="fielderror"
+ py:content="tg.errors.get('initials')" />
+
+ </td>
+ </tr>
+ </table>
+
+ <h2 class="formsection">Account Details</h2>
+ <table class="formtable" cellpadding="2" cellspacing="0" border="0">
+ <tr>
+ <th>
+ <label class="fieldlabel" for="${user_fields.nsAccountLock.field_id}"
+ py:content="user_fields.nsAccountLock.label" />:
+ </th>
+ <td>
+ <span py:replace="user_fields.nsAccountLock.display(value_for(user_fields.nsAccountLock))" />
+ <span py:if="tg.errors.get('nsAccountLock')" class="fielderror"
+ py:content="tg.errors.get('nsAccountLock')" />
+ <script py:if="tg.identity.display_name == value.get('uid')" type="text/javascript">
+ document.getElementById('form_nsAccountLock').disabled = true;
+ </script>
+ </td>
+ </tr>
+
+ <tr>
+ <th>
+ <label class="fieldlabel" for="${user_fields.uid.field_id}"
+ py:content="user_fields.uid.label" />:
+ </th>
+ <td>
+ <span py:replace="user_fields.uid.display(
+ value_for(user_fields.uid))" />
+ <span py:if="tg.errors.get('uid')" class="fielderror"
+ py:content="tg.errors.get('uid')" />
+
+ <script type="text/javascript">
+ document.getElementById('form_uid').disabled = true;
+ </script>
+ </td>
+ </tr>
+
+ <tr>
+ <th valign="top">
+ <label class="fieldlabel" for="${user_fields.krbprincipalkey.field_id}"
+ py:content="user_fields.krbprincipalkey.label" />:
+ </th>
+ <td valign="top">
+ <span py:replace="user_fields.krbprincipalkey.display(value_for(user_fields.krbprincipalkey))" />
+ <span py:if="tg.errors.get('krbprincipalkey')" class="fielderror"
+ py:content="tg.errors.get('krbprincipalkey')" />
+
+ <script type="text/javascript">
+ document.getElementById('form_krbprincipalkey').disabled = true;
+ </script>
+
+ <!--
+ <span id="password_text">********</span>
+ <input id="genpassword_button" type="button" value="Generate Password"
+ disabled="true"
+ onclick="new Ajax.Request('${tg.url('/user/generate_password')}',
+ {
+ method: 'get',
+ onSuccess: function(transport) {
+ document.getElementById('form_krbprincipalkey').value =
+ transport.responseText;
+ }
+ });" />
+ <br />
+ <input type="checkbox"
+ onclick="togglePassword(this);"><span class="xsmall">edit</span></input>
+ <script type="text/javascript">
+ document.getElementById('form_krbprincipalkey').style.display='none';
+
+ function togglePassword(checkbox) {
+ passwordField = document.getElementById('form_krbprincipalkey');
+ passwordText = document.getElementById('password_text');
+ passwordButton = document.getElementById('genpassword_button');
+ if (checkbox.checked) {
+ passwordField.style.display='inline';
+ passwordText.style.display='none';
+ passwordButton.disabled=false;
+ } else {
+ passwordField.style.display='none';
+ passwordText.style.display='inline';
+ passwordButton.disabled=true;
+ }
+ }
+ </script>
+ -->
+ </td>
+ </tr>
+
+ <tr>
+ <th valign="top">
+ <label class="fieldlabel" for="${user_fields.krbprincipalkey_confirm.field_id}"
+ py:content="user_fields.krbprincipalkey_confirm.label" />:
+ </th>
+ <td valign="top">
+ <span py:replace="user_fields.krbprincipalkey_confirm.display(
+ value_for(user_fields.krbprincipalkey_confirm))" />
+ <span py:if="tg.errors.get('krbprincipalkey_confirm')" class="fielderror"
+ py:content="tg.errors.get('krbprincipalkey_confirm')" />
+
+ <script type="text/javascript">
+ document.getElementById('form_krbprincipalkey_confirm').disabled = true;
+ </script>
+ </td>
+ </tr>
+
+ <tr>
+ <th>
+ <label class="fieldlabel" for="${user_fields.uidnumber.field_id}"
+ py:content="user_fields.uidnumber.label" />:
+ </th>
+ <td>
+ <span py:replace="user_fields.uidnumber.display(
+ value_for(user_fields.uidnumber))" />
+ <span py:if="tg.errors.get('uidnumber')" class="fielderror"
+ py:content="tg.errors.get('uidnumber')" />
+
+ <script type="text/javascript">
+ document.getElementById('form_uidnumber').disabled = true;
+ </script>
+ </td>
+ </tr>
+
+ <tr>
+ <th>
+ <label class="fieldlabel" for="${user_fields.gidnumber.field_id}"
+ py:content="user_fields.gidnumber.label" />:
+ </th>
+ <td>
+ <span py:replace="user_fields.gidnumber.display(
+ value_for(user_fields.gidnumber))" />
+ <span py:if="tg.errors.get('gidnumber')" class="fielderror"
+ py:content="tg.errors.get('gidnumber')" />
+
+ <script type="text/javascript">
+ document.getElementById('form_gidnumber').disabled = true;
+ </script>
+ </td>
+ </tr>
+
+ <tr>
+ <th>
+ <label class="fieldlabel" for="${user_fields.homedirectory.field_id}"
+ py:content="user_fields.homedirectory.label" />:
+ </th>
+ <td>
+ <span py:replace="user_fields.homedirectory.display(
+ value_for(user_fields.homedirectory))" />
+ <span py:if="tg.errors.get('homedirectory')" class="fielderror"
+ py:content="tg.errors.get('homedirectory')" />
+
+ <script type="text/javascript">
+ document.getElementById('form_homedirectory').disabled = true;
+ </script>
+ </td>
+ </tr>
+
+ <tr>
+ <th>
+ <label class="fieldlabel" for="${user_fields.loginshell.field_id}"
+ py:content="user_fields.loginshell.label" />:
+ </th>
+ <td>
+ <span py:replace="user_fields.loginshell.display(
+ value_for(user_fields.loginshell))" />
+ <span py:if="tg.errors.get('loginshell')" class="fielderror"
+ py:content="tg.errors.get('loginshell')" />
+ </td>
+ </tr>
+
+ <tr>
+ <th>
+ <label class="fieldlabel" for="${user_fields.gecos.field_id}"
+ py:content="user_fields.gecos.label" />:
+ </th>
+ <td>
+ <span py:replace="user_fields.gecos.display(
+ value_for(user_fields.gecos))" />
+ <span py:if="tg.errors.get('gecos')" class="fielderror"
+ py:content="tg.errors.get('gecos')" />
+ </td>
+ </tr>
+ </table>
+
+ <h2 class="formsection">Contact Details</h2>
+ <table class="formtable" cellpadding="2" cellspacing="0" border="0">
+ <tr>
+ <th>
+ <label class="fieldlabel" for="${user_fields.mail.field_id}"
+ py:content="user_fields.mail.label" />:
+ </th>
+ <td>
+ <span py:replace="user_fields.mail.display(value_for(user_fields.mail))" />
+ <span py:if="tg.errors.get('mail')" class="fielderror"
+ py:content="tg.errors.get('mail')" />
+ </td>
+ </tr>
+
+ <tr>
+ <th>
+ <label class="fieldlabel" for="${user_fields.telephonenumbers.field_id}"
+ py:content="user_fields.telephonenumbers.label" />:
+ </th>
+ <td colspan="3">
+ <table class="formtable" cellpadding="2" cellspacing="0" border="0" id="${user_fields.telephonenumbers.field_id}">
+ <tbody>
+ <?python repetition = 0
+ tele_index = 0
+ tele_error = tg.errors.get('telephonenumber')
+ ?>
+ <tr py:for="tele in value_for(user_fields.telephonenumber)"
+ id="${user_fields.telephonenumbers.field_id}_${repetition}"
+ class="${user_fields.telephonenumbers.field_class}">
+
+ <td py:for="field in user_fields.telephonenumbers.fields">
+ <span><input class="textfield" type="text" id="${user_fields.telephonenumbers.field_id}_${repetition}_telephonenumber" name="telephonenumbers-${repetition}.telephonenumber" value="${tele}"/></span>
+ <span py:if="tele_error and tele_error[tele_index]" class="fielderror"
+ py:content="tg.errors.get('telephonenumber')" />
+ </td>
+ <?python tele_index = tele_index + 1 ?>
+ <td>
+ <a
+ href="javascript:ExpandingForm.removeItem('${user_fields.telephonenumbers.field_id}_${repetition}')">Remove</a>
+ </td>
+ <?python repetition = repetition + 1?>
+ </tr>
+ </tbody>
+ </table>
+ <a id="${user_fields.telephonenumbers.field_id}_doclink" href="javascript:ExpandingForm.addItem('${user_fields.telephonenumbers.field_id}');">Add Work Number</a>
+ </td>
+ </tr>
+ <tr>
+ <th>
+ <label class="fieldlabel" for="${user_fields.facsimiletelephonenumbers.field_id}"
+ py:content="user_fields.facsimiletelephonenumbers.label" />:
+ </th>
+ <td colspan="3">
+ <table class="formtable" cellpadding="2" cellspacing="0" border="0" id="${user_fields.facsimiletelephonenumbers.field_id}">
+ <tbody>
+ <?python repetition = 0
+ fax_index = 0
+ fax_error = tg.errors.get('facsimiletelephonenumber')
+ ?>
+ <tr py:for="fax in value_for(user_fields.facsimiletelephonenumber)"
+ id="${user_fields.facsimiletelephonenumbers.field_id}_${repetition}"
+ class="${user_fields.facsimiletelephonenumbers.field_class}">
+
+ <td py:for="field in user_fields.facsimiletelephonenumbers.fields">
+ <span><input class="textfield" type="text" id="${user_fields.facsimiletelephonenumbers.field_id}_${repetition}_facsimiletelephonenumber" name="facsimiletelephonenumbers-${repetition}.facsimiletelephonenumber" value="${fax}"/></span>
+ <span py:if="fax_error and fax_error[fax_index]" class="fielderror"
+ py:content="tg.errors.get('facsimiletelephonenumber')" />
+ </td>
+ <?python fax_index = fax_index + 1 ?>
+ <td>
+ <a
+ href="javascript:ExpandingForm.removeItem('${user_fields.facsimiletelephonenumbers.field_id}_${repetition}')">Remove</a>
+ </td>
+ <?python repetition = repetition + 1?>
+ </tr>
+ </tbody>
+ </table>
+ <a id="${user_fields.facsimiletelephonenumbers.field_id}_doclink" href="javascript:ExpandingForm.addItem('${user_fields.facsimiletelephonenumbers.field_id}');">Add Fax Number</a>
+ </td>
+ </tr>
+
+ <tr>
+ <th>
+ <label class="fieldlabel" for="${user_fields.mobiles.field_id}"
+ py:content="user_fields.mobiles.label" />:
+ </th>
+ <td colspan="3">
+ <table class="formtable" cellpadding="2" cellspacing="0" border="0" id="${user_fields.mobiles.field_id}">
+ <tbody>
+ <?python repetition = 0
+ mobile_index = 0
+ mobile_error = tg.errors.get('mobile')
+ ?>
+ <tr py:for="mobile in value_for(user_fields.mobile)"
+ id="${user_fields.mobiles.field_id}_${repetition}"
+ class="${user_fields.mobiles.field_class}">
+
+ <td py:for="field in user_fields.mobiles.fields">
+ <span><input class="textfield" type="text" id="${user_fields.mobiles.field_id}_${repetition}_mobile" name="mobiles-${repetition}.mobile" value="${mobile}"/></span>
+ <span py:if="mobile_error and mobile_error[mobile_index]" class="fielderror"
+ py:content="tg.errors.get('mobile')" />
+ </td>
+ <?python mobile_index = mobile_index + 1 ?>
+ <td>
+ <a
+ href="javascript:ExpandingForm.removeItem('${user_fields.mobiles.field_id}_${repetition}')">Remove</a>
+ </td>
+ <?python repetition = repetition + 1?>
+ </tr>
+ </tbody>
+ </table>
+ <a id="${user_fields.mobiles.field_id}_doclink" href="javascript:ExpandingForm.addItem('${user_fields.mobiles.field_id}');">Add Cell Number</a>
+ </td>
+ </tr>
+
+ <tr>
+ <th>
+ <label class="fieldlabel" for="${user_fields.pagers.field_id}"
+ py:content="user_fields.pagers.label" />:
+ </th>
+ <td colspan="3">
+ <table class="formtable" cellpadding="2" cellspacing="0" border="0" id="${user_fields.pagers.field_id}">
+ <tbody>
+ <?python repetition = 0
+ pager_index = 0
+ pager_error = tg.errors.get('pager')
+ ?>
+ <tr py:for="pager in value_for(user_fields.pager)"
+ id="${user_fields.pagers.field_id}_${repetition}"
+ class="${user_fields.pagers.field_class}">
+
+ <td py:for="field in user_fields.pagers.fields">
+ <span><input class="textfield" type="text" id="${user_fields.pagers.field_id}_${repetition}_pager" name="pagers-${repetition}.pager" value="${pager}"/></span>
+ <span py:if="pager_error and pager_error[pager_index]" class="fielderror"
+ py:content="tg.errors.get('pager')" />
+ </td>
+ <?python pager_index = pager_index + 1 ?>
+ <td>
+ <a
+ href="javascript:ExpandingForm.removeItem('${user_fields.pagers.field_id}_${repetition}')">Remove</a>
+ </td>
+ <?python repetition = repetition + 1?>
+ </tr>
+ </tbody>
+ </table>
+ <a id="${user_fields.pagers.field_id}_doclink" href="javascript:ExpandingForm.addItem('${user_fields.pagers.field_id}');">Add Pager Number</a>
+ </td>
+ </tr>
+
+ <tr>
+ <th>
+ <label class="fieldlabel" for="${user_fields.homephones.field_id}"
+ py:content="user_fields.homephones.label" />:
+ </th>
+ <td colspan="3">
+ <table class="formtable" cellpadding="2" cellspacing="0" border="0" id="${user_fields.homephones.field_id}">
+ <tbody>
+ <?python repetition = 0
+ homephone_index = 0
+ homephone_error = tg.errors.get('homephone')
+ ?>
+ <tr py:for="homephone in value_for(user_fields.homephone)"
+ id="${user_fields.homephones.field_id}_${repetition}"
+ class="${user_fields.homephones.field_class}">
+
+ <td py:for="field in user_fields.homephones.fields">
+ <span><input class="textfield" type="text" id="${user_fields.homephones.field_id}_${repetition}_homephone" name="homephones-${repetition}.homephone" value="${homephone}"/></span>
+ <span py:if="homephone_error and homephone_error[homephone_index]" class="fielderror"
+ py:content="tg.errors.get('homephone')" />
+ </td>
+ <?python homephone_index = homephone_index + 1 ?>
+ <td>
+ <a
+ href="javascript:ExpandingForm.removeItem('${user_fields.homephones.field_id}_${repetition}')">Remove</a>
+ </td>
+ <?python repetition = repetition + 1?>
+ </tr>
+ </tbody>
+ </table>
+ <a id="${user_fields.homephones.field_id}_doclink" href="javascript:ExpandingForm.addItem('${user_fields.homephones.field_id}');">Add Home Phone</a>
+ </td>
+ </tr>
+ </table>
+
+ <h2 class="formsection">Mailing Address</h2>
+ <table class="formtable" cellpadding="2" cellspacing="0" border="0">
+ <tr>
+ <th>
+ <label class="fieldlabel" for="${user_fields.street.field_id}"
+ py:content="user_fields.street.label" />:
+ </th>
+ <td>
+ <span py:replace="user_fields.street.display(value_for(user_fields.street))" />
+ <span py:if="tg.errors.get('street')" class="fielderror"
+ py:content="tg.errors.get('street')" />
+ </td>
+ </tr>
+
+ <tr>
+ <th>
+ <label class="fieldlabel" for="${user_fields.roomnumber.field_id}"
+ py:content="user_fields.roomnumber.label" />:
+ </th>
+ <td>
+ <span py:replace="user_fields.roomnumber.display(value_for(user_fields.roomnumber))" />
+ <span py:if="tg.errors.get('roomnumber')" class="fielderror"
+ py:content="tg.errors.get('roomnumber')" />
+ </td>
+ </tr>
+
+ <tr>
+ <th>
+ <label class="fieldlabel" for="${user_fields.l.field_id}"
+ py:content="user_fields.l.label" />:
+ </th>
+ <td>
+ <span py:replace="user_fields.l.display(value_for(user_fields.l))" />
+ <span py:if="tg.errors.get('l')" class="fielderror"
+ py:content="tg.errors.get('l')" />
+ </td>
+ </tr>
+
+ <tr>
+ <th>
+ <label class="fieldlabel" for="${user_fields.st.field_id}"
+ py:content="user_fields.st.label" />:
+ </th>
+ <td>
+ <span py:replace="user_fields.st.display(value_for(user_fields.st))" />
+ <span py:if="tg.errors.get('st')" class="fielderror"
+ py:content="tg.errors.get('st')" />
+ </td>
+ </tr>
+
+ <tr>
+ <th>
+ <label class="fieldlabel" for="${user_fields.postalcode.field_id}"
+ py:content="user_fields.postalcode.label" />:
+ </th>
+ <td>
+ <span py:replace="user_fields.postalcode.display(value_for(user_fields.postalcode))" />
+ <span py:if="tg.errors.get('postalcode')" class="fielderror"
+ py:content="tg.errors.get('postalcode')" />
+ </td>
+ </tr>
+ </table>
+
+ <h2 class="formsection">Employee Information</h2>
+ <table class="formtable" cellpadding="2" cellspacing="0" border="0">
+ <tr>
+ <th>
+ <label class="fieldlabel" for="${user_fields.ou.field_id}"
+ py:content="user_fields.ou.label" />:
+ </th>
+ <td>
+ <span py:replace="user_fields.ou.display(value_for(user_fields.ou))" />
+ <span py:if="tg.errors.get('ou')" class="fielderror"
+ py:content="tg.errors.get('ou')" />
+ </td>
+ </tr>
+
+ <tr>
+ <th>
+ <label class="fieldlabel" for="${user_fields.businesscategory.field_id}"
+ py:content="user_fields.businesscategory.label" />:
+ </th>
+ <td>
+ <span py:replace="user_fields.businesscategory.display(value_for(user_fields.businesscategory))" />
+ <span py:if="tg.errors.get('businesscategory')" class="fielderror"
+ py:content="tg.errors.get('businesscategory')" />
+ </td>
+ </tr>
+
+ <tr>
+ <th>
+ <label class="fieldlabel" for="${user_fields.description.field_id}"
+ py:content="user_fields.description.label" />:
+ </th>
+ <td>
+ <span py:replace="user_fields.description.display(value_for(user_fields.description))" />
+ <span py:if="tg.errors.get('description')" class="fielderror"
+ py:content="tg.errors.get('description')" />
+ </td>
+ </tr>
+
+ <tr>
+ <th>
+ <label class="fieldlabel" for="${user_fields.employeetype.field_id}"
+ py:content="user_fields.employeetype.label" />:
+ </th>
+ <td>
+ <span py:replace="user_fields.employeetype.display(value_for(user_fields.employeetype))" />
+ <span py:if="tg.errors.get('employeetype')" class="fielderror"
+ py:content="tg.errors.get('employeetype')" />
+ </td>
+ </tr>
+
+ <tr>
+ <th valign="top">
+ <label class="fieldlabel" for="${user_fields.manager.field_id}"
+ py:content="user_fields.manager.label" />:
+ </th>
+ <td valign="top">
+ <div>
+ <span id='manager_select_cn'>${value_for(user_fields.manager_cn)}</span>
+ <span py:if="'editors' in tg.identity.groups or 'admins' in tg.identity.groups or tg.identity.display_name != value.get('uid')" id='manager_links'>
+ <a href="#" onclick="return clearSelect('manager');">clear</a>
+ <a href="#" onclick="return startSelect('manager');">change</a>
+ </span>
+ <span py:if="tg.errors.get('manager')" class="fielderror"
+ py:content="tg.errors.get('manager')" />
+ </div>
+ <div id="manager_searcharea" style="display:none">
+ <div>
+ <input id="manager_criteria" type="text"
+ onkeypress="return enterDoSelectSearch(event, 'manager');" />
+ <input type="button" value="Find"
+ onclick="return doSelectSearch('manager');"
+ />
+ </div>
+ <div id="manager_searchresults">
+ </div>
+ </div>
+ </td>
+ </tr>
+
+ <tr>
+ <th valign="top">
+ <label class="fieldlabel" for="${user_fields.secretary.field_id}"
+ py:content="user_fields.secretary.label" />:
+ </th>
+ <td valign="top">
+ <div>
+ <span id='secretary_select_cn'>${value_for(user_fields.secretary_cn)}</span>
+ <span id='secretary_links'>
+ <a href="#" onclick="return clearSelect('secretary');">clear</a>
+ <a href="#" onclick="return startSelect('secretary');">change</a>
+ </span>
+ <span py:if="tg.errors.get('secretary')" class="fielderror"
+ py:content="tg.errors.get('secretary')" />
+ </div>
+ <div id="secretary_searcharea" style="display:none">
+ <div>
+ <input id="secretary_criteria" type="text"
+ onkeypress="return enterDoSelectSearch(event, 'secretary');" />
+ <input type="button" value="Find"
+ onclick="return doSelectSearch('secretary');"
+ />
+ </div>
+ <div id="secretary_searchresults">
+ </div>
+ </div>
+ </td>
+ </tr>
+ </table>
+
+ <h2 class="formsection">Misc Information</h2>
+ <table class="formtable" cellpadding="2" cellspacing="0" border="0">
+ <tr>
+ <th>
+ <label class="fieldlabel" for="${user_fields.carlicense.field_id}"
+ py:content="user_fields.carlicense.label" />:
+ </th>
+ <td>
+ <span py:replace="user_fields.carlicense.display(value_for(user_fields.carlicense))" />
+ <span py:if="tg.errors.get('carlicense')" class="fielderror"
+ py:content="tg.errors.get('carlicense')" />
+ </td>
+ </tr>
+ <tr>
+ <th>
+ <label class="fieldlabel" for="${user_fields.labeleduri.field_id}"
+ py:content="user_fields.labeleduri.label" />:
+ </th>
+ <td>
+ <span py:replace="user_fields.labeleduri.display(value_for(user_fields.labeleduri))" />
+ <span py:if="tg.errors.get('labeleduri')" class="fielderror"
+ py:content="tg.errors.get('labeleduri')" />
+ </td>
+ </tr>
+ </table>
+
+ <div py:if='len(custom_fields) &gt; 0'>
+ <h2 class="formsection" >Custom Fields</h2>
+ <table class="formtable" cellpadding="2" cellspacing="0" border="0">
+ <tr py:for='custom_field in custom_fields'>
+ <th>
+ <label class="fieldlabel" for="${custom_field.field_id}"
+ py:content="custom_field.label" />:
+ </th>
+ <td>
+ <span py:replace="custom_field.display(value_for(custom_field))" />
+ <span py:if="tg.errors.get(custom_field.name)" class="fielderror"
+ py:content="tg.errors.get(custom_field.name)" />
+ </td>
+ </tr>
+ </table>
+ </div>
+
+
+ <div>
+ <h2 class="formsection">Groups</h2>
+
+ <div class="floatlist">
+ <div py:if="'editors' in tg.identity.groups or 'admins' in tg.identity.groups or tg.identity.display_name != value.get('uid')" class="floatheader">To Remove:</div>
+ <div id="delmembers">
+ </div>
+ </div>
+
+ <div>
+ <?python div_counter = 1 ?>
+ <div py:for="group in user_groups" id="member-${div_counter}">
+ <?python
+ group_dn = group.get('dn')
+ group_dn_esc = ipahelper.javascript_string_escape(group_dn)
+
+ group_name = group.get('cn')
+ group_descr = "[group]"
+ group_type = "group"
+
+ group_name_esc = ipahelper.javascript_string_escape(group_name)
+ group_descr_esc = ipahelper.javascript_string_escape(group_descr)
+ group_type_esc = ipahelper.javascript_string_escape(group_type)
+ ?>
+ <span id="member-info-${div_counter}"></span>
+ <script type="text/javascript">
+ renderMemberInfo($('member-info-${div_counter}'),
+ new MemberDisplayInfo('${group_name_esc}',
+ '${group_descr_esc}',
+ '${group_type_esc}'));
+ </script>
+ <a py:if="'editors' in tg.identity.groups or 'admins' in tg.identity.groups or tg.identity.display_name != value.get('uid')" href="#"
+ onclick="removememberHandler(this, '${group_dn_esc}',
+ new MemberDisplayInfo('${group_name_esc}',
+ '${group_descr_esc}',
+ '${group_type_esc}'));
+ return false;"
+ >remove</a>
+ <script type="text/javascript">
+ dn_to_member_div_id['${group_dn_esc}'] = "member-${div_counter}";
+ member_hash["${group_dn_esc}"] = 1;
+ </script>
+ <?python
+ div_counter = div_counter + 1
+ ?>
+ </div>
+ &nbsp; <!-- a space here to prevent an empty div -->
+ </div>
+
+ </div>
+
+ <div py:if="'editors' in tg.identity.groups or 'admins' in tg.identity.groups or tg.identity.display_name != value.get('uid')" style="clear:both">
+ <h2 class="formsection">Add Groups</h2>
+
+ <div class="floatlist">
+ <div class="floatheader">To Add:</div>
+ <div id="newmembers">
+ </div>
+ </div>
+
+ <div>
+ <div id="search">
+ <input id="criteria" type="text" name="criteria"
+ onkeypress="return enterDoSearch(event);" />
+ <input type="button" value="Find"
+ onclick="return doSearch();"
+ />
+ </div>
+ <div id="searchresults">
+ </div>
+ </div>
+ </div>
+
+ <hr/>
+
+ <input type="submit" class="submitbutton" name="submit"
+ value="Update User"/>
+ <input type="submit" class="submitbutton" name="submit"
+ value="Cancel Edit" />
+ <input py:if="'editors' in tg.identity.groups or 'admins' in tg.identity.groups and tg.identity.display_name != value.get('uid')"
+ type="button" class="submitbutton"
+ value="Delete User"
+ onclick="return confirmDelete();"
+ />
+
+ </form>
+
+ <script type="text/javascript">
+ /*
+ * This section restores the contents of the add and remove lists
+ * dynamically if we have to refresh the page
+ */
+ if ($('form_dn_to_info_json').value != "") {
+ dn_to_info_hash = new Hash($('form_dn_to_info_json').value.evalJSON());
+ }
+
+ if ($('form_editprotected').value != "") {
+ $('toggleprotected_checkbox').checked = true;
+ toggleProtectedFields($('toggleprotected_checkbox'));
+ }
+ </script>
+
+ <?python
+ dnadds = value.get('dnadd', [])
+ if not(isinstance(dnadds,list) or isinstance(dnadds,tuple)):
+ dnadds = [dnadds]
+
+ dndels = value.get('dndel', [])
+ if not(isinstance(dndels,list) or isinstance(dndels,tuple)):
+ dndels = [dndels]
+ ?>
+
+ <script py:for="dnadd in dnadds">
+ <?python
+ dnadd_esc = ipahelper.javascript_string_escape(dnadd)
+ ?>
+ var dn = "${dnadd_esc}";
+ var info = dn_to_info_hash[dn];
+ var newdiv = addmember(dn, info);
+ if (newdiv != null) {
+ newdiv.style.display = 'block';
+ }
+ </script>
+
+ <script py:for="dndel in dndels">
+ <?python
+ dndel_esc = ipahelper.javascript_string_escape(dndel)
+ ?>
+ var dn = "${dndel_esc}";
+ var info = dn_to_info_hash[dn];
+ var newdiv = removemember(dn, info);
+ newdiv.style.display = 'block';
+ orig_div_id = dn_to_member_div_id[dn]
+ $(orig_div_id).style.display = 'none';
+ </script>
+
+</div>
diff --git a/ipa-server/ipa-gui/ipagui/templates/userlayout.kid b/ipa-server/ipa-gui/ipagui/templates/userlayout.kid
new file mode 100644
index 00000000..7625ffdd
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/templates/userlayout.kid
@@ -0,0 +1,41 @@
+<!--
+ Copyright (C) 2007 Red Hat
+ see file 'COPYING' for use and warranty information
+
+ This program is free software; you can redistribute it and/or
+ modify it under the terms of the GNU General Public License as
+ published by the Free Software Foundation; version 2 only
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+-->
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:py="http://purl.org/kid/ns#"
+ py:extends="'master.kid'">
+<head>
+</head>
+
+<body py:match="item.tag=='{http://www.w3.org/1999/xhtml}body'" py:attrs="item.items()">
+ <div id="main_content">
+ <div id="details">
+ <div id="alertbox" py:if="value_of('tg_flash', None)">
+ <p py:content="XML(tg_flash)"></p></div>
+
+ <div py:replace="[item.text]+item[:]"></div>
+ </div>
+
+<!-- <div id="sidebar">
+ <h2>Tools</h2>
+ <a href="${tg.url('/user/new')}">Add User</a><br/>
+ <a href="${tg.url('/user/list')}">Find Users</a><br/>
+ </div> -->
+ </div>
+</body>
+
+</html>
diff --git a/ipa-server/ipa-gui/ipagui/templates/userlist.kid b/ipa-server/ipa-gui/ipagui/templates/userlist.kid
new file mode 100644
index 00000000..5dfe2bde
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/templates/userlist.kid
@@ -0,0 +1,118 @@
+<!--
+ Copyright (C) 2007 Red Hat
+ see file 'COPYING' for use and warranty information
+
+ This program is free software; you can redistribute it and/or
+ modify it under the terms of the GNU General Public License as
+ published by the Free Software Foundation; version 2 only
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+-->
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:py="http://purl.org/kid/ns#"
+ py:extends="'userlayout.kid'">
+<head>
+<meta content="text/html; charset=utf-8" http-equiv="Content-Type" py:replace="''"/>
+<title>Find Users</title>
+</head>
+<body>
+ <h1 class="user">Find Users</h1>
+ <script type="text/javascript" charset="utf-8" src="${tg.url('/static/javascript/tablekit.js')}"></script>
+ <div id="search">
+ <form action="${tg.url('/user/list')}" method="get">
+ <input id="uid" type="text" name="uid" value="${uid}" />
+ <input class="searchbutton" type="submit" value="Find Users"/>
+ </form>
+ <script type="text/javascript">
+ document.getElementById("uid").focus();
+ </script>
+ </div>
+ <div py:if='(users != None) and (len(users) > 0)'>
+ <h2>${len(users)} results returned:</h2>
+ <table id="resultstable" class="details sortable resizable" cellspacing="0">
+ <thead>
+ <tr>
+ <th>
+ User
+ </th>
+ <th>
+ Phone
+ </th>
+ <th>
+ Unit
+ </th>
+ <th>
+ Job Title
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr py:for="user in users" py:if="user.nsAccountLock != 'true'">
+ <td>
+ <a
+ href="${tg.url('/user/show',uid=user.uid)}"
+ py:content="u'%s %s (%s)' % (user.givenName, user.sn, user.uid)"
+ />
+ </td>
+ <td>
+ ${user.telephoneNumber}
+ </td>
+ <td>
+ ${user.ou}
+ </td>
+ <td>
+ ${user.title}
+ </td>
+ </tr>
+ </tbody>
+ <tbody>
+ <tr id="inactive" py:for="user in users" py:if="user.nsAccountLock == 'true'">
+ <td>
+ <a
+ href="${tg.url('/user/show',uid=user.uid)}"
+ py:content="u'%s %s (%s)' % (user.givenName, user.sn, user.uid)"
+ />
+ </td>
+ <td>
+ ${user.telephoneNumber}
+ </td>
+ <td>
+ ${user.ou}
+ </td>
+ <td>
+ ${user.title}
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <div id="alertbox" py:if='(users != None) and (len(users) == 0)'>
+ <p>No results found for "${uid}"</p>
+ </div>
+
+ <div class="instructions" py:if='users == None'>
+ <p>
+ Search automatically looks across multiple fields. If you want to find
+ Joe in Finance, try typing "joe finance" into the search box.
+ </p>
+ <p>
+ Exact matches are listed first, followed by partial matches. If your search
+ is too broad, you will get a warning that the search returned too many
+ results. Try being more specific.
+ </p>
+ <p>
+ The results that come back are sortable. Simply click on a column
+ header to sort on that header. A triangle will indicate the sorted
+ column, along with its direction. Clicking and dragging between headers
+ will allow you to resize the header.
+ </p>
+ </div>
+</body>
+</html>
diff --git a/ipa-server/ipa-gui/ipagui/templates/usernew.kid b/ipa-server/ipa-gui/ipagui/templates/usernew.kid
new file mode 100644
index 00000000..bbb74324
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/templates/usernew.kid
@@ -0,0 +1,30 @@
+<!--
+ Copyright (C) 2007 Red Hat
+ see file 'COPYING' for use and warranty information
+
+ This program is free software; you can redistribute it and/or
+ modify it under the terms of the GNU General Public License as
+ published by the Free Software Foundation; version 2 only
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+-->
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:py="http://purl.org/kid/ns#"
+ py:extends="'userlayout.kid'">
+<head>
+ <meta content="text/html; charset=utf-8" http-equiv="Content-Type" py:replace="''"/>
+ <title>Add User</title>
+</head>
+<body>
+ <h1 class="user">Add User</h1>
+
+ ${form.display(action=tg.url("/user/create"), value=user)}
+</body>
+</html>
diff --git a/ipa-server/ipa-gui/ipagui/templates/usernewform.kid b/ipa-server/ipa-gui/ipagui/templates/usernewform.kid
new file mode 100644
index 00000000..cd924a72
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/templates/usernewform.kid
@@ -0,0 +1,842 @@
+<!--
+ Copyright (C) 2007 Red Hat
+ see file 'COPYING' for use and warranty information
+
+ This program is free software; you can redistribute it and/or
+ modify it under the terms of the GNU General Public License as
+ published by the Free Software Foundation; version 2 only
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+-->
+<div xmlns:py="http://purl.org/kid/ns#"
+ class="simpleroster">
+ <form action="${action}" name="${name}" method="${method}" class="tableform"
+ onsubmit="preSubmit()">
+
+<input type="submit" class="submitbutton" name="submit" value="Add User"/>
+
+<?python
+from ipagui.helpers import ipahelper
+?>
+
+ <script type="text/javascript" charset="utf-8"
+ src="${tg.url('/static/javascript/dynamicedit.js')}"></script>
+ <script type="text/javascript" charset="utf-8"
+ src="${tg.url('/static/javascript/dynamicselect.js')}"></script>
+ <script type="text/javascript" charset="utf-8"
+ src="${tg.url('/tg_widgets/tg_expanding_form_widget/javascript/expanding_form.js')}"></script>
+
+ <?python
+ searchurl = tg.url('/user/edit_search')
+ selectSearchurl = tg.url('/user/user_select_search')
+ ?>
+
+ <script type="text/javascript">
+ function doSearch() {
+ $('searchresults').update("Searching...");
+ new Ajax.Updater('searchresults',
+ '${searchurl}',
+ { asynchronous:true,
+ parameters: { criteria: $('criteria').value },
+ evalScripts: true });
+ return false;
+ }
+
+ // override dynamicedit.js version
+ // we don't need to show [group] nor italize groups
+ function renderMemberInfo(newdiv, info) {
+ if (info.type == "group") {
+ newdiv.appendChild(document.createTextNode(
+ info.name + " "));
+ }
+ }
+ function doSelectSearch(which_select) {
+ $(which_select + '_searchresults').update("Searching...");
+ new Ajax.Updater(which_select + '_searchresults',
+ '${selectSearchurl}',
+ { asynchronous:true,
+ parameters: { criteria: $(which_select + '_criteria').value,
+ which_select: which_select},
+ evalScripts: true });
+ return false;
+ }
+ </script>
+
+ <div py:for="field in hidden_fields"
+ py:replace="field.display(value_for(field), **params_for(field))"
+ />
+
+ <h2 class="formsection">Identity Details</h2>
+ <table class="formtable" cellpadding="2" cellspacing="0" border="0">
+ <tr>
+ <th>
+ <label class="fieldlabel" for="${user_fields.title.field_id}"
+ py:content="user_fields.title.label" />:
+ </th>
+ <td>
+ <span py:replace="user_fields.title.display(value_for(user_fields.title))" />
+ <span py:if="tg.errors.get('title')" class="fielderror"
+ py:content="tg.errors.get('title')" />
+
+ </td>
+ </tr>
+
+ <tr>
+ <th>
+ <label class="fieldlabel" for="${user_fields.givenname.field_id}"
+ py:content="user_fields.givenname.label" />:
+ </th>
+ <td>
+ <span py:replace="user_fields.givenname.display(value_for(user_fields.givenname))" />
+ <span py:if="tg.errors.get('givenname')" class="fielderror"
+ py:content="tg.errors.get('givenname')" />
+
+ </td>
+ </tr>
+
+ <tr>
+ <th>
+ <label class="fieldlabel" for="${user_fields.sn.field_id}"
+ py:content="user_fields.sn.label" />:
+ </th>
+ <td>
+ <span py:replace="user_fields.sn.display(value_for(user_fields.sn))" />
+ <span py:if="tg.errors.get('sn')" class="fielderror"
+ py:content="tg.errors.get('sn')" />
+ <script type="text/javascript">
+ var uid_suggest = "";
+ var mail_suggest = "";
+ var cn_suggest = "";
+ var displayname_suggest = "";
+ var initials_suggest = "";
+
+ function autofill(self) {
+ var givenname = $('form_givenname');
+ var sn = $('form_sn');
+ if ((givenname.value == "") || (sn.value == "")) {
+ return;
+ }
+
+ var uid = $('form_uid');
+ var mail = $('form_mail');
+ var cn = $('form_cns_0_cn');
+ var displayname = $('form_displayname');
+ var initials = $('form_initials');
+
+ if ((cn.value == "") || (cn.value == cn_suggest)) {
+ cn.value = givenname.value + " " + sn.value;
+ cn_suggest = cn.value;
+ new Effect.Highlight(cn);
+ }
+
+ if ((displayname.value == "") ||
+ (displayname.value == displayname_suggest)) {
+ displayname.value = givenname.value + " " + sn.value;
+ displayname_suggest = displayname.value;
+ new Effect.Highlight(displayname);
+ }
+
+ if ((initials.value == "") ||
+ (initials.value == initials_suggest)) {
+ initials.value = givenname.value[0] + sn.value[0];
+ initials_suggest = initials.value;
+ new Effect.Highlight(initials);
+ }
+
+ if ((uid.value == "") || (uid.value == uid_suggest)) {
+ new Ajax.Request('${tg.url('/user/suggest_uid')}', {
+ method: 'get',
+ parameters: {'givenname': givenname.value, 'sn': sn.value},
+ onSuccess: function(transport) {
+ uid.value = transport.responseText;
+ uid_suggest = uid.value;
+ new Effect.Highlight(uid);
+ }
+ });
+ }
+
+ if ((mail.value == "") || (mail.value == mail_suggest)) {
+ new Ajax.Request('${tg.url('/user/suggest_email')}', {
+ method: 'get',
+ parameters: {'givenname': givenname.value, 'sn': sn.value},
+ onSuccess: function(transport) {
+ mail.value = transport.responseText;
+ mail_suggest = mail.value;
+ new Effect.Highlight(mail);
+ }
+ });
+ }
+ }
+
+ document.getElementById('form_givenname').onchange = autofill;
+ document.getElementById('form_sn').onchange = autofill;
+ </script>
+ </td>
+ </tr>
+
+ <tr>
+ <th>
+ <label class="fieldlabel" for="${user_fields.cns.field_id}"
+ py:content="user_fields.cns.label" />:
+ </th>
+ <td colspan="3">
+ <table class="formtable" cellpadding="2" cellspacing="0" border="0" id="${user_fields.cns.field_id}">
+ <tbody>
+ <?python repetition = 0
+ cn_index = 0
+ cn_error = tg.errors.get('cn')
+ values = value_for(user_fields.cn)
+ if values is None:
+ values=['']
+ ?>
+ <tr py:for="cn in values"
+ id="${user_fields.cns.field_id}_${repetition}"
+ class="${user_fields.cns.field_class}">
+
+ <td py:for="field in user_fields.cns.fields">
+ <span><input class="textfield" type="text" id="${user_fields.cns.field_id}_${repetition}_cn" name="cns-${repetition}.cn" value="${cn}"/></span>
+ <span py:if="cn_error and cn_error[cn_index]" class="fielderror"
+ py:content="tg.errors.get('cn')" />
+ </td>
+ <?python cn_index = cn_index + 1 ?>
+ <td>
+ <a
+ href="javascript:ExpandingForm.removeItem('${user_fields.cns.field_id}_${repetition}')">Remove</a>
+ </td>
+ <?python repetition = repetition + 1?>
+ </tr>
+ </tbody>
+ </table>
+ <a id="${user_fields.cns.field_id}_doclink" href="javascript:ExpandingForm.addItem('${user_fields.cns.field_id}');">Add Full Name</a>
+ </td>
+ </tr>
+
+ <tr>
+ <th>
+ <label class="fieldlabel" for="${user_fields.displayname.field_id}"
+ py:content="user_fields.displayname.label" />:
+ </th>
+ <td>
+ <span py:replace="user_fields.displayname.display(value_for(user_fields.displayname))" />
+ <span py:if="tg.errors.get('displayname')" class="fielderror"
+ py:content="tg.errors.get('displayname')" />
+
+ </td>
+ </tr>
+
+ <tr>
+ <th>
+ <label class="fieldlabel" for="${user_fields.initials.field_id}"
+ py:content="user_fields.initials.label" />:
+ </th>
+ <td>
+ <span py:replace="user_fields.initials.display(value_for(user_fields.initials))" />
+ <span py:if="tg.errors.get('initials')" class="fielderror"
+ py:content="tg.errors.get('initials')" />
+
+ </td>
+ </tr>
+ </table>
+
+ <h2 class="formsection">Account Details</h2>
+ <table class="formtable" cellpadding="2" cellspacing="0" border="0">
+ <tr>
+ <th>
+ <label class="fieldlabel" for="${user_fields.nsAccountLock.field_id}"
+ py:content="user_fields.nsAccountLock.label" />:
+ </th>
+ <td>
+ <span py:replace="user_fields.nsAccountLock.display(value_for(user_fields.nsAccountLock))" />
+ <span py:if="tg.errors.get('nsAccountLock')" class="fielderror"
+ py:content="tg.errors.get('nsAccountLock')" />
+ </td>
+ </tr>
+ <tr>
+ <th>
+ <label class="fieldlabel" for="${user_fields.uid.field_id}"
+ py:content="user_fields.uid.label" />:
+ </th>
+ <td>
+ <span py:replace="user_fields.uid.display(value_for(user_fields.uid))" />
+ <span py:if="tg.errors.get('uid')" class="fielderror"
+ py:content="tg.errors.get('uid')" />
+ </td>
+ </tr>
+
+ <tr>
+ <th>
+ <label class="fieldlabel" for="${user_fields.krbprincipalkey.field_id}"
+ py:content="user_fields.krbprincipalkey.label" />:
+ </th>
+ <td>
+ <span py:replace="user_fields.krbprincipalkey.display(value_for(user_fields.krbprincipalkey))" />
+ <span py:if="tg.errors.get('krbprincipalkey')" class="fielderror"
+ py:content="tg.errors.get('krbprincipalkey')" />
+
+ <!--
+ <input type="button" value="Generate Password"
+ onclick="new Ajax.Request('${tg.url('/user/generate_password')}',
+ {
+ method: 'get',
+ onSuccess: function(transport) {
+ document.getElementById('form_krbprincipalkey').value =
+ transport.responseText;
+ }
+ });" />
+ -->
+ </td>
+ </tr>
+
+ <tr>
+ <th>
+ <label class="fieldlabel" for="${user_fields.krbprincipalkey_confirm.field_id}"
+ py:content="user_fields.krbprincipalkey_confirm.label" />:
+ </th>
+ <td>
+ <span py:replace="user_fields.krbprincipalkey_confirm.display(
+ value_for(user_fields.krbprincipalkey_confirm))" />
+ <span py:if="tg.errors.get('krbprincipalkey_confirm')" class="fielderror"
+ py:content="tg.errors.get('krbprincipalkey_confirm')" />
+ </td>
+ </tr>
+
+ <tr>
+ <th>
+ <label class="fieldlabel" for="${user_fields.uidnumber.field_id}"
+ py:content="user_fields.uidnumber.label" />:
+ </th>
+ <td>
+ Generated by server
+ </td>
+ </tr>
+
+ <tr>
+ <th>
+ <label class="fieldlabel" for="${user_fields.gidnumber.field_id}"
+ py:content="user_fields.gidnumber.label" />:
+ </th>
+ <td>
+ Generated by server
+ </td>
+ </tr>
+
+ <tr>
+ <th>
+ <label class="fieldlabel" for="${user_fields.homedirectory.field_id}"
+ py:content="user_fields.homedirectory.label" />:
+ </th>
+ <td>
+ Generated by server
+ </td>
+ </tr>
+
+ <tr>
+ <th>
+ <label class="fieldlabel" for="${user_fields.loginshell.field_id}"
+ py:content="user_fields.loginshell.label" />:
+ </th>
+ <td>
+ <span py:replace="user_fields.loginshell.display(
+ value_for(user_fields.loginshell))" />
+ <span py:if="tg.errors.get('loginshell')" class="fielderror"
+ py:content="tg.errors.get('loginshell')" />
+ </td>
+ </tr>
+
+ <tr>
+ <th>
+ <label class="fieldlabel" for="${user_fields.gecos.field_id}"
+ py:content="user_fields.gecos.label" />:
+ </th>
+ <td>
+ <span py:replace="user_fields.gecos.display(
+ value_for(user_fields.gecos))" />
+ <span py:if="tg.errors.get('gecos')" class="fielderror"
+ py:content="tg.errors.get('gecos')" />
+ </td>
+ </tr>
+ </table>
+
+ <h2 class="formsection">Contact Details</h2>
+ <table class="formtable" cellpadding="2" cellspacing="0" border="0">
+ <tr>
+ <th>
+ <label class="fieldlabel" for="${user_fields.mail.field_id}"
+ py:content="user_fields.mail.label" />:
+ </th>
+ <td>
+ <span py:replace="user_fields.mail.display(value_for(user_fields.mail))" />
+ <span py:if="tg.errors.get('mail')" class="fielderror"
+ py:content="tg.errors.get('mail')" />
+ </td>
+ </tr>
+
+ <tr>
+ <th>
+ <label class="fieldlabel" for="${user_fields.telephonenumbers.field_id}"
+ py:content="user_fields.telephonenumbers.label" />:
+ </th>
+ <td colspan="3">
+ <table class="formtable" cellpadding="2" cellspacing="0" border="0" id="${user_fields.telephonenumbers.field_id}">
+ <tbody>
+ <?python repetition = 0
+ tele_index = 0
+ tele_error = tg.errors.get('telephonenumber')
+ values = value_for(user_fields.telephonenumber)
+ if values is None:
+ values=['']
+ ?>
+ <tr py:for="tele in values"
+ id="${user_fields.telephonenumbers.field_id}_${repetition}"
+ class="${user_fields.telephonenumbers.field_class}">
+
+ <td py:if="user_fields.telephonenumbers.fields is not None" py:for="field in user_fields.telephonenumbers.fields">
+ <span><input class="textfield" type="text" id="${user_fields.telephonenumbers.field_id}_${repetition}_telephonenumber" name="telephonenumbers-${repetition}.telephonenumber" value="${tele}"/></span>
+ <span py:if="tele_error and tele_error[tele_index]" class="fielderror"
+ py:content="tg.errors.get('telephonenumber')" />
+ </td>
+ <?python tele_index = tele_index + 1 ?>
+ <td>
+ <a
+ href="javascript:ExpandingForm.removeItem('${user_fields.telephonenumbers.field_id}_${repetition}')">Remove</a>
+ </td>
+ <?python repetition = repetition + 1?>
+ </tr>
+ </tbody>
+ </table>
+ <a id="${user_fields.telephonenumbers.field_id}_doclink" href="javascript:ExpandingForm.addItem('${user_fields.telephonenumbers.field_id}');">Add Work Number</a>
+ </td>
+ </tr>
+ <tr>
+ <th>
+ <label class="fieldlabel" for="${user_fields.facsimiletelephonenumbers.field_id}"
+ py:content="user_fields.facsimiletelephonenumbers.label" />:
+ </th>
+ <td colspan="3">
+ <table class="formtable" cellpadding="2" cellspacing="0" border="0" id="${user_fields.facsimiletelephonenumbers.field_id}">
+ <tbody>
+ <?python repetition = 0
+ fax_index = 0
+ fax_error = tg.errors.get('facsimiletelephonenumber')
+ values = value_for(user_fields.facsimiletelephonenumber)
+ if values is None:
+ values=['']
+ ?>
+ <tr py:for="fax in values"
+ id="${user_fields.facsimiletelephonenumbers.field_id}_${repetition}"
+ class="${user_fields.facsimiletelephonenumbers.field_class}">
+
+ <td py:for="field in user_fields.facsimiletelephonenumbers.fields">
+ <span><input class="textfield" type="text" id="${user_fields.facsimiletelephonenumbers.field_id}_${repetition}_facsimiletelephonenumber" name="facsimiletelephonenumbers-${repetition}.facsimiletelephonenumber" value="${fax}"/></span>
+ <span py:if="fax_error and fax_error[fax_index]" class="fielderror"
+ py:content="tg.errors.get('facsimiletelephonenumber')" />
+ </td>
+ <?python fax_index = fax_index + 1 ?>
+ <td>
+ <a
+ href="javascript:ExpandingForm.removeItem('${user_fields.facsimiletelephonenumbers.field_id}_${repetition}')">Remove</a>
+ </td>
+ <?python repetition = repetition + 1?>
+ </tr>
+ </tbody>
+ </table>
+ <a id="${user_fields.facsimiletelephonenumbers.field_id}_doclink" href="javascript:ExpandingForm.addItem('${user_fields.facsimiletelephonenumbers.field_id}');">Add Fax Number</a>
+ </td>
+ </tr>
+
+ <tr>
+ <th>
+ <label class="fieldlabel" for="${user_fields.mobiles.field_id}"
+ py:content="user_fields.mobiles.label" />:
+ </th>
+ <td colspan="3">
+ <table class="formtable" cellpadding="2" cellspacing="0" border="0" id="${user_fields.mobiles.field_id}">
+ <tbody>
+ <?python repetition = 0
+ mobile_index = 0
+ mobile_error = tg.errors.get('mobile')
+ values = value_for(user_fields.mobile)
+ if values is None:
+ values=['']
+ ?>
+ <tr py:for="mobile in values"
+ id="${user_fields.mobiles.field_id}_${repetition}"
+ class="${user_fields.mobiles.field_class}">
+
+ <td py:for="field in user_fields.mobiles.fields">
+ <span><input class="textfield" type="text" id="${user_fields.mobiles.field_id}_${repetition}_mobile" name="mobiles-${repetition}.mobile" value="${mobile}"/></span>
+ <span py:if="mobile_error and mobile_error[mobile_index]" class="fielderror"
+ py:content="tg.errors.get('mobile')" />
+ </td>
+ <?python mobile_index = mobile_index + 1 ?>
+ <td>
+ <a
+ href="javascript:ExpandingForm.removeItem('${user_fields.mobiles.field_id}_${repetition}')">Remove</a>
+ </td>
+ <?python repetition = repetition + 1?>
+ </tr>
+ </tbody>
+ </table>
+ <a id="${user_fields.mobiles.field_id}_doclink" href="javascript:ExpandingForm.addItem('${user_fields.mobiles.field_id}');">Add Cell Number</a>
+ </td>
+ </tr>
+
+ <tr>
+ <th>
+ <label class="fieldlabel" for="${user_fields.pagers.field_id}"
+ py:content="user_fields.pagers.label" />:
+ </th>
+ <td colspan="3">
+ <table class="formtable" cellpadding="2" cellspacing="0" border="0" id="${user_fields.pagers.field_id}">
+ <tbody>
+ <?python repetition = 0
+ pager_index = 0
+ pager_error = tg.errors.get('pager')
+ values = value_for(user_fields.pager)
+ if values is None:
+ values=['']
+ ?>
+ <tr py:for="pager in values"
+ id="${user_fields.pagers.field_id}_${repetition}"
+ class="${user_fields.pagers.field_class}">
+
+ <td py:for="field in user_fields.pagers.fields">
+ <span><input class="textfield" type="text" id="${user_fields.pagers.field_id}_${repetition}_pager" name="pagers-${repetition}.pager" value="${pager}"/></span>
+ <span py:if="pager_error and pager_error[pager_index]" class="fielderror"
+ py:content="tg.errors.get('pager')" />
+ </td>
+ <?python pager_index = pager_index + 1 ?>
+ <td>
+ <a
+ href="javascript:ExpandingForm.removeItem('${user_fields.pagers.field_id}_${repetition}')">Remove</a>
+ </td>
+ <?python repetition = repetition + 1?>
+ </tr>
+ </tbody>
+ </table>
+ <a id="${user_fields.pagers.field_id}_doclink" href="javascript:ExpandingForm.addItem('${user_fields.pagers.field_id}');">Add Pager Number</a>
+ </td>
+ </tr>
+
+ <tr>
+ <th>
+ <label class="fieldlabel" for="${user_fields.homephones.field_id}"
+ py:content="user_fields.homephones.label" />:
+ </th>
+ <td colspan="3">
+ <table class="formtable" cellpadding="2" cellspacing="0" border="0" id="${user_fields.homephones.field_id}">
+ <tbody>
+ <?python repetition = 0
+ homephone_index = 0
+ homephone_error = tg.errors.get('homephone')
+ values = value_for(user_fields.homephone)
+ if values is None:
+ values=['']
+ ?>
+ <tr py:for="homephone in values"
+ id="${user_fields.homephones.field_id}_${repetition}"
+ class="${user_fields.homephones.field_class}">
+
+ <td py:for="field in user_fields.homephones.fields">
+ <span><input class="textfield" type="text" id="${user_fields.homephones.field_id}_${repetition}_homephone" name="homephones-${repetition}.homephone" value="${homephone}"/></span>
+ <span py:if="homephone_error and homephone_error[homephone_index]" class="fielderror"
+ py:content="tg.errors.get('homephone')" />
+ </td>
+ <?python homephone_index = homephone_index + 1 ?>
+ <td>
+ <a
+ href="javascript:ExpandingForm.removeItem('${user_fields.homephones.field_id}_${repetition}')">Remove</a>
+ </td>
+ <?python repetition = repetition + 1?>
+ </tr>
+ </tbody>
+ </table>
+ <a id="${user_fields.homephones.field_id}_doclink" href="javascript:ExpandingForm.addItem('${user_fields.homephones.field_id}');">Add Home Phone</a>
+ </td>
+ </tr>
+
+ </table>
+
+ <h2 class="formsection">Mailing Address</h2>
+ <table class="formtable" cellpadding="2" cellspacing="0" border="0">
+ <tr>
+ <th>
+ <label class="fieldlabel" for="${user_fields.street.field_id}"
+ py:content="user_fields.street.label" />:
+ </th>
+ <td>
+ <span py:replace="user_fields.street.display(value_for(user_fields.street))" />
+ <span py:if="tg.errors.get('street')" class="fielderror"
+ py:content="tg.errors.get('street')" />
+ </td>
+ </tr>
+
+ <tr>
+ <th>
+ <label class="fieldlabel" for="${user_fields.roomnumber.field_id}"
+ py:content="user_fields.roomnumber.label" />:
+ </th>
+ <td>
+ <span py:replace="user_fields.roomnumber.display(value_for(user_fields.roomnumber))" />
+ <span py:if="tg.errors.get('roomnumber')" class="fielderror"
+ py:content="tg.errors.get('roomnumber')" />
+ </td>
+ </tr>
+
+ <tr>
+ <th>
+ <label class="fieldlabel" for="${user_fields.l.field_id}"
+ py:content="user_fields.l.label" />:
+ </th>
+ <td>
+ <span py:replace="user_fields.l.display(value_for(user_fields.l))" />
+ <span py:if="tg.errors.get('l')" class="fielderror"
+ py:content="tg.errors.get('l')" />
+ </td>
+ </tr>
+
+ <tr>
+ <th>
+ <label class="fieldlabel" for="${user_fields.st.field_id}"
+ py:content="user_fields.st.label" />:
+ </th>
+ <td>
+ <span py:replace="user_fields.st.display(value_for(user_fields.st))" />
+ <span py:if="tg.errors.get('st')" class="fielderror"
+ py:content="tg.errors.get('st')" />
+ </td>
+ </tr>
+
+ <tr>
+ <th>
+ <label class="fieldlabel" for="${user_fields.postalcode.field_id}"
+ py:content="user_fields.postalcode.label" />:
+ </th>
+ <td>
+ <span py:replace="user_fields.postalcode.display(value_for(user_fields.postalcode))" />
+ <span py:if="tg.errors.get('postalcode')" class="fielderror"
+ py:content="tg.errors.get('postalcode')" />
+ </td>
+ </tr>
+ </table>
+
+ <h2 class="formsection">Employee Information</h2>
+ <table class="formtable" cellpadding="2" cellspacing="0" border="0">
+ <tr>
+ <th>
+ <label class="fieldlabel" for="${user_fields.ou.field_id}"
+ py:content="user_fields.ou.label" />:
+ </th>
+ <td>
+ <span py:replace="user_fields.ou.display(value_for(user_fields.ou))" />
+ <span py:if="tg.errors.get('ou')" class="fielderror"
+ py:content="tg.errors.get('ou')" />
+ </td>
+ </tr>
+
+ <tr>
+ <th>
+ <label class="fieldlabel" for="${user_fields.businesscategory.field_id}"
+ py:content="user_fields.businesscategory.label" />:
+ </th>
+ <td>
+ <span py:replace="user_fields.businesscategory.display(value_for(user_fields.businesscategory))" />
+ <span py:if="tg.errors.get('businesscategory')" class="fielderror"
+ py:content="tg.errors.get('businesscategory')" />
+ </td>
+ </tr>
+
+ <tr>
+ <th>
+ <label class="fieldlabel" for="${user_fields.description.field_id}"
+ py:content="user_fields.description.label" />:
+ </th>
+ <td>
+ <span py:replace="user_fields.description.display(value_for(user_fields.description))" />
+ <span py:if="tg.errors.get('description')" class="fielderror"
+ py:content="tg.errors.get('description')" />
+ </td>
+ </tr>
+
+ <tr>
+ <th>
+ <label class="fieldlabel" for="${user_fields.employeetype.field_id}"
+ py:content="user_fields.employeetype.label" />:
+ </th>
+ <td>
+ <span py:replace="user_fields.employeetype.display(value_for(user_fields.employeetype))" />
+ <span py:if="tg.errors.get('employeetype')" class="fielderror"
+ py:content="tg.errors.get('employeetype')" />
+ </td>
+ </tr>
+
+ <tr>
+ <th valign="top">
+ <label class="fieldlabel" for="${user_fields.manager.field_id}"
+ py:content="user_fields.manager.label" />:
+ </th>
+ <td valign="top">
+ <div>
+ <span id='manager_select_cn'>${value_for(user_fields.manager)}</span>
+ <span id='manager_links'>
+ <a href="#" onclick="return clearSelect('manager');">clear</a>
+ <a href="#" onclick="return startSelect('manager');">change</a>
+ </span>
+ <span py:if="tg.errors.get('manager')" class="fielderror"
+ py:content="tg.errors.get('manager')" />
+ </div>
+ <div id="manager_searcharea" style="display:none">
+ <div>
+ <input id="manager_criteria" type="text"
+ onkeypress="return enterDoSelectSearch(event, 'manager');" />
+ <input type="button" value="Find"
+ onclick="return doSelectSearch('manager');"
+ />
+ </div>
+ <div id="manager_searchresults">
+ </div>
+ </div>
+ </td>
+ </tr>
+
+ <tr>
+ <th>
+ <label class="fieldlabel" for="${user_fields.secretary.field_id}"
+ py:content="user_fields.secretary.label" />:
+ </th>
+ <td>
+ <div>
+ <span id='secretary_select_cn'>${value_for(user_fields.secretary)}</span>
+ <span id='secretary_links'>
+ <a href="#" onclick="return clearSelect('secretary');">clear</a>
+ <a href="#" onclick="return startSelect('secretary');">change</a>
+ </span>
+ <span py:if="tg.errors.get('secretary')" class="fielderror"
+ py:content="tg.errors.get('secretary')" />
+ </div>
+ <div id="secretary_searcharea" style="display:none">
+ <div>
+ <input id="secretary_criteria" type="text"
+ onkeypress="return enterDoSelectSearch(event, 'secretary');" />
+ <input type="button" value="Find"
+ onclick="return doSelectSearch('secretary');"
+ />
+ </div>
+ <div id="secretary_searchresults">
+ </div>
+ </div>
+ </td>
+ </tr>
+ </table>
+
+ <h2 class="formsection">Misc Information</h2>
+ <table class="formtable" cellpadding="2" cellspacing="0" border="0">
+ <tr>
+ <th>
+ <label class="fieldlabel" for="${user_fields.carlicense.field_id}"
+ py:content="user_fields.carlicense.label" />:
+ </th>
+ <td>
+ <span py:replace="user_fields.carlicense.display(value_for(user_fields.carlicense))" />
+ <span py:if="tg.errors.get('carlicense')" class="fielderror"
+ py:content="tg.errors.get('carlicense')" />
+ </td>
+ </tr>
+ <tr>
+ <th>
+ <label class="fieldlabel" for="${user_fields.labeleduri.field_id}"
+ py:content="user_fields.labeleduri.label" />:
+ </th>
+ <td>
+ <span py:replace="user_fields.labeleduri.display(value_for(user_fields.labeleduri))" />
+ <span py:if="tg.errors.get('labeleduri')" class="fielderror"
+ py:content="tg.errors.get('labeleduri')" />
+ </td>
+ </tr>
+ </table>
+
+ <div py:if='len(custom_fields) &gt; 0'>
+ <div class="formsection" >Custom Fields</div>
+ <table class="formtable" cellpadding="2" cellspacing="0" border="0">
+ <tr py:for='custom_field in custom_fields'>
+ <th>
+ <label class="fieldlabel" for="${custom_field.field_id}"
+ py:content="custom_field.label" />:
+ </th>
+ <td>
+ <span py:replace="custom_field.display(value_for(custom_field))" />
+ <span py:if="tg.errors.get(custom_field.name)" class="fielderror"
+ py:content="tg.errors.get(custom_field.name)" />
+ </td>
+ </tr>
+ </table>
+ </div>
+
+ <div style="clear:both">
+ <h2 class="formsection">Add Groups</h2>
+
+
+ <div class="floatlist">
+ <div class="floatheader">To Add:</div>
+ <div id="newmembers">
+ </div>
+ </div>
+
+ <div>
+ <div id="search">
+ <input id="criteria" type="text" name="criteria"
+ onkeypress="return enterDoSearch(event);" />
+ <input class="searchbutton" type="button" value="Find"
+ onclick="return doSearch();"
+ />
+ </div>
+ <div id="searchresults">
+ </div>
+ </div>
+ </div>
+
+<hr />
+<input type="submit" class="submitbutton" name="submit" value="Add User"/>
+
+ </form>
+
+ <script type="text/javascript">
+ document.getElementById("form_title").focus();
+ </script>
+
+ <script type="text/javascript">
+ /*
+ * This section restores the contents of the add and remove lists
+ * dynamically if we have to refresh the page
+ */
+ if ($('form_dn_to_info_json').value != "") {
+ dn_to_info_hash = new Hash($('form_dn_to_info_json').value.evalJSON());
+ }
+ </script>
+
+ <?python
+ dnadds = value.get('dnadd', [])
+ if not(isinstance(dnadds,list) or isinstance(dnadds,tuple)):
+ dnadds = [dnadds]
+ ?>
+
+ <script py:for="dnadd in dnadds">
+ <?python
+ dnadd_esc = ipahelper.javascript_string_escape(dnadd)
+ ?>
+ var dn = "${dnadd_esc}";
+ var info = dn_to_info_hash[dn];
+ var newdiv = addmember(dn, info);
+ if (newdiv != null) {
+ newdiv.style.display = 'block';
+ }
+ </script>
+
+</div>
diff --git a/ipa-server/ipa-gui/ipagui/templates/userselectsearch.kid b/ipa-server/ipa-gui/ipagui/templates/userselectsearch.kid
new file mode 100644
index 00000000..b8a60ed5
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/templates/userselectsearch.kid
@@ -0,0 +1,53 @@
+<!--
+ Copyright (C) 2007 Red Hat
+ see file 'COPYING' for use and warranty information
+
+ This program is free software; you can redistribute it and/or
+ modify it under the terms of the GNU General Public License as
+ published by the Free Software Foundation; version 2 only
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+-->
+<div xmlns:py="http://purl.org/kid/ns#">
+
+<?python
+from ipagui.helpers import ipahelper
+?>
+ <div py:if='(users != None) and (len(users) > 0)'>
+ <div id="search-results-count">
+ ${len(users)} results returned:
+ <span py:if="counter &lt; 0">
+ (truncated)
+ </span>
+ </div>
+
+ <div py:for="user in users">
+ <?python
+ user_name = "%s %s" % (user.getValue('givenName', ''),
+ user.getValue('sn', ''))
+ user_descr = "(%s)" % user.uid
+
+ user_dn_esc = ipahelper.javascript_string_escape(user.dn)
+ user_name_esc = ipahelper.javascript_string_escape(user_name)
+ user_descr_esc = ipahelper.javascript_string_escape(user_descr)
+ which_select_esc = ipahelper.javascript_string_escape(which_select)
+ ?>
+
+ ${user_name} ${user_descr}
+ <a href=""
+ onclick="doSelect('${which_select_esc}', '${user_dn_esc}', '${user_name_esc}');
+ return false;"
+ >select</a>
+ </div>
+ </div>
+ <div py:if='(users != None) and (len(users) == 0)'>
+ No results found for "${criteria}"
+ </div>
+</div>
diff --git a/ipa-server/ipa-gui/ipagui/templates/usershow.kid b/ipa-server/ipa-gui/ipagui/templates/usershow.kid
new file mode 100644
index 00000000..0c2582b2
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/templates/usershow.kid
@@ -0,0 +1,399 @@
+<!--
+ Copyright (C) 2007 Red Hat
+ see file 'COPYING' for use and warranty information
+
+ This program is free software; you can redistribute it and/or
+ modify it under the terms of the GNU General Public License as
+ published by the Free Software Foundation; version 2 only
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+-->
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:py="http://purl.org/kid/ns#"
+ py:extends="'userlayout.kid'">
+<head>
+ <meta content="text/html; charset=utf-8" http-equiv="Content-Type" py:replace="''"/>
+ <title>View User</title>
+</head>
+<body>
+<?python
+edit_url = tg.url('/user/edit', uid=user.get('uid'))
+?>
+ <h1 class="user">View User</h1>
+
+ <input py:if="'editors' in tg.identity.groups or 'admins' in tg.identity.groups or tg.identity.display_name == user.get('uid')"
+ class="submitbutton" type="button"
+ onclick="document.location.href='${edit_url}'"
+ value="Edit User" />
+
+<?python
+from ipagui.helpers import userhelper
+pw_expires_days = userhelper.password_expires_in(user.get("krbPasswordExpiration"))
+pw_expires_soon = userhelper.password_expires_soon(pw_expires_days)
+pw_is_expired = userhelper.password_is_expired(pw_expires_days)
+if pw_expires_days != 1:
+ days_suffix = "s"
+else:
+ days_suffix = ""
+?>
+
+ <div id="alertbox" py:if='pw_expires_soon' class="warning_message">
+ ${user.get("uid")}'s password will expire in ${pw_expires_days} day${days_suffix}
+ </div>
+ <div id="alertbox" py:if='pw_is_expired' class="warning_message">
+ ${user.get("uid")}'s password has expired
+ </div>
+
+ <h2 class="formsection">Identity Details</h2>
+ <table class="formtable" cellpadding="2" cellspacing="0" border="0">
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="fields.title.label" />:
+ </th>
+ <td>${user.get("title")}</td>
+ </tr>
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="fields.givenname.label" />:
+ </th>
+ <td>${user.get("givenname")}</td>
+ </tr>
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="fields.sn.label" />:
+ </th>
+ <td>${user.get("sn")}</td>
+ </tr>
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="fields.cn.label" />:
+ </th>
+ <td>
+ <table cellpadding="2" cellspacing="0" border="0">
+ <tbody>
+ <?python
+ index = 0
+ values = user.get("cn")
+ if isinstance(values, str):
+ values = [values]
+ ?>
+ <tr py:for="index in range(len(values))">
+ <td>${values[index]}</td>
+ </tr>
+ </tbody>
+ </table>
+ </td>
+ </tr>
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="fields.displayname.label" />:
+ </th>
+ <td>${user.get("displayname")}</td>
+ </tr>
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="fields.initials.label" />:
+ </th>
+ <td>${user.get("initials")}</td>
+ </tr>
+ </table>
+
+ <h2 class="formsection">Account Details</h2>
+ <table class="formtable" cellpadding="2" cellspacing="0" border="0">
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="fields.nsAccountLock.label" />:
+ </th>
+ <td>${userhelper.account_status_display(user.get("nsAccountLock"))}</td>
+ </tr>
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="fields.uid.label" />:
+ </th>
+ <td>${user.get("uid")}</td>
+ </tr>
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="fields.uidnumber.label" />:
+ </th>
+ <td>${user.get("uidnumber")}</td>
+ </tr>
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="fields.gidnumber.label" />:
+ </th>
+ <td>${user.get("gidnumber")}</td>
+ </tr>
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="fields.homedirectory.label" />:
+ </th>
+ <td>${user.get("homedirectory")}</td>
+ </tr>
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="fields.loginshell.label" />:
+ </th>
+ <td>${user.get("loginshell")}</td>
+ </tr>
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="fields.gecos.label" />:
+ </th>
+ <td>${user.get("gecos")}</td>
+ </tr>
+ </table>
+
+ <h2 class="formsection">Contact Details</h2>
+ <table class="formtable" cellpadding="2" cellspacing="0" border="0">
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="fields.mail.label" />:
+ </th>
+ <td><a py:if="user.get('mail')"
+ href="mailto:${user.get('mail')}">${user.get("mail")}</a></td>
+ </tr>
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="fields.telephonenumber.label" />:
+ </th>
+ <td>
+ <table cellpadding="2" cellspacing="0" border="0">
+ <tbody>
+ <?python
+ index = 0
+ values = user.get("telephonenumber", '')
+ if isinstance(values, str):
+ values = [values]
+ ?>
+ <tr py:for="index in range(len(values))">
+ <td>${values[index]}</td>
+ </tr>
+ </tbody>
+ </table>
+ </td>
+ </tr>
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="fields.facsimiletelephonenumber.label" />:
+ </th>
+ <td>
+ <table cellpadding="2" cellspacing="0" border="0">
+ <tbody>
+ <?python
+ index = 0
+ values = user.get("facsimiletelephonenumber", '')
+ if isinstance(values, str):
+ values = [values]
+ ?>
+ <tr py:for="index in range(len(values))">
+ <td>${values[index]}</td>
+ </tr>
+ </tbody>
+ </table>
+ </td>
+ </tr>
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="fields.mobile.label" />:
+ </th>
+ <td>
+ <table cellpadding="2" cellspacing="0" border="0">
+ <tbody>
+ <?python
+ index = 0
+ values = user.get("mobile", '')
+ if isinstance(values, str):
+ values = [values]
+ ?>
+ <tr py:for="index in range(len(values))">
+ <td>${values[index]}</td>
+ </tr>
+ </tbody>
+ </table>
+ </td>
+ </tr>
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="fields.pager.label" />:
+ </th>
+ <td>
+ <table cellpadding="2" cellspacing="0" border="0">
+ <tbody>
+ <?python
+ index = 0
+ values = user.get("pager", '')
+ if isinstance(values, str):
+ values = [values]
+ ?>
+ <tr py:for="index in range(len(values))">
+ <td>${values[index]}</td>
+ </tr>
+ </tbody>
+ </table>
+ </td>
+ </tr>
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="fields.homephone.label" />:
+ </th>
+ <td>
+ <table cellpadding="2" cellspacing="0" border="0">
+ <tbody>
+ <?python
+ index = 0
+ values = user.get("homephone", '')
+ if isinstance(values, str):
+ values = [values]
+ ?>
+ <tr py:for="index in range(len(values))">
+ <td>${values[index]}</td>
+ </tr>
+ </tbody>
+ </table>
+ </td>
+ </tr>
+ </table>
+
+ <h2 class="formsection">Mailing Address</h2>
+ <table class="formtable" cellpadding="2" cellspacing="0" border="0">
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="fields.street.label" />:
+ </th>
+ <td>${user.get("street")}</td>
+ </tr>
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="fields.roomnumber.label" />:
+ </th>
+ <td>${user.get("roomnumber")}</td>
+ </tr>
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="fields.l.label" />:
+ </th>
+ <td>${user.get("l")}</td>
+ </tr>
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="fields.st.label" />:
+ </th>
+ <td>${user.get("st")}</td>
+ </tr>
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="fields.postalcode.label" />:
+ </th>
+ <td>${user.get("postalcode")}</td>
+ </tr>
+ </table>
+
+ <h2 class="formsection">Employee Information</h2>
+ <table class="formtable" cellpadding="2" cellspacing="0" border="0">
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="fields.ou.label" />:
+ </th>
+ <td>${user.get("ou")}</td>
+ </tr>
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="fields.businesscategory.label" />:
+ </th>
+ <td>${user.get("businesscategory")}</td>
+ </tr>
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="fields.description.label" />:
+ </th>
+ <td>${user.get("description")}</td>
+ </tr>
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="fields.employeetype.label" />:
+ </th>
+ <td>${user.get("employeetype")}</td>
+ </tr>
+ <tr py:if='user_manager'>
+ <th>
+ <label class="fieldlabel" py:content="fields.manager.label" />:
+ </th>
+ <td>
+ <a href="${tg.url('/user/show', uid=user_manager.uid)}"
+ >${user_manager.givenname} ${user_manager.sn}</a>
+ </td>
+ </tr>
+ <tr py:if='user_secretary'>
+ <th>
+ <label class="fieldlabel" py:content="fields.secretary.label" />:
+ </th>
+ <td>
+ <a href="${tg.url('/user/show', uid=user_secretary.uid)}"
+ >${user_secretary.givenname} ${user_secretary.sn}</a>
+ </td>
+ </tr>
+ </table>
+
+ <h2 class="formsection">Misc Information</h2>
+ <table class="formtable" cellpadding="2" cellspacing="0" border="0">
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="fields.carlicense.label" />:
+ </th>
+ <td>${user.get("carlicense")}</td>
+ </tr>
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="fields.labeleduri.label" />:
+ </th>
+ <td>
+ <a py:if="user.get('labeleduri')"
+ href="${user.get('labeleduri')}">${user.get('labeleduri')}</a>
+ </td>
+ </tr>
+ </table>
+
+ <div py:if='len(fields.custom_fields) &gt; 0'>
+ <h2 class="formsection">Custom Fields</h2>
+ <table class="formtable" cellpadding="2" cellspacing="0" border="0">
+ <tr py:for='custom_field in fields.custom_fields'>
+ <th>
+ <label class="fieldlabel" for="${custom_field.field_id}"
+ py:content="custom_field.label" />:
+ </th>
+ <td>
+ ${user.get(custom_field.name)}
+ </td>
+ </tr>
+ </table>
+ </div>
+
+ <h2 class="formsection" py:if='len(user_reports) &gt; 0'>Direct Reports</h2>
+ <ol py:if="len(user_reports) &gt; 0">
+ <li py:for="report in user_reports">
+ <a href="${tg.url('/user/show', uid=report.uid)}"
+ >${report.givenname} ${report.sn}</a>
+ </li>
+ </ol>
+
+ <h2 class="formsection">Groups</h2>
+ <div py:for="group in user_groups">
+ <a href="${tg.url('/group/show', cn=group.cn)}">${group.cn}</a>
+ </div>
+
+ <br/>
+<hr />
+ <input py:if="'editors' in tg.identity.groups or 'admins' in tg.identity.groups or tg.identity.display_name == user.get('uid')"
+ class="submitbutton" type="button"
+ onclick="document.location.href='${edit_url}'"
+ value="Edit User" />
+</body>
+</html>
diff --git a/ipa-server/ipa-gui/ipagui/templates/welcome.kid b/ipa-server/ipa-gui/ipagui/templates/welcome.kid
new file mode 100644
index 00000000..ce3b444c
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/templates/welcome.kid
@@ -0,0 +1,53 @@
+<!--
+ Copyright (C) 2007 Red Hat
+ see file 'COPYING' for use and warranty information
+
+ This program is free software; you can redistribute it and/or
+ modify it under the terms of the GNU General Public License as
+ published by the Free Software Foundation; version 2 only
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+-->
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:py="http://purl.org/kid/ns#"
+ py:extends="'master.kid'">
+<head>
+<meta content="text/html; charset=utf-8" http-equiv="Content-Type" py:replace="''"/>
+<title>Welcome</title>
+</head>
+<body>
+ <div id="details">
+ <div id="alertbox" py:if="value_of('tg_flash', None)">
+ <p py:content="XML(tg_flash)"></p></div>
+ <h1>Welcome to Free IPA</h1>
+
+ <noscript>
+ <span class="warning_message">
+ This site makes heavy use of JavaScript.<br />
+ Please enable JavaScript in your browser to make sure all pages function properly.
+ </span>
+ </noscript>
+
+ <p>
+IPA is used to manage Identity, Policy, and Auditing for your organization.
+ </p>
+ <p>
+ To get started, you can use the search box in the top right to find
+ users or groups you need to work on. Search automatically looks
+ across multiple fields. If you want to find Joe in Finance, try typing
+ "joe finance" into the search box.
+ </p>
+ <p>
+ Alternatively, select a task from the right sidebar.
+ </p>
+ </div>
+
+</body>
+</html>
diff --git a/ipa-server/ipa-gui/ipagui/tests/Makefile.am b/ipa-server/ipa-gui/ipagui/tests/Makefile.am
new file mode 100644
index 00000000..bf06ef2f
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/tests/Makefile.am
@@ -0,0 +1,16 @@
+NULL =
+
+appdir = $(IPA_DATA_DIR)/ipagui/tests
+app_PYTHON = \
+ __init__.py \
+ test_controllers.py \
+ test_model.py \
+ $(NULL)
+
+EXTRA_DIST = \
+ $(NULL)
+
+MAINTAINERCLEANFILES = \
+ *~ \
+ *.pyc \
+ Makefile.in
diff --git a/ipa-server/ipa-gui/ipagui/tests/__init__.py b/ipa-server/ipa-gui/ipagui/tests/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/tests/__init__.py
diff --git a/ipa-server/ipa-gui/ipagui/tests/test_controllers.py b/ipa-server/ipa-gui/ipagui/tests/test_controllers.py
new file mode 100644
index 00000000..fc4014d0
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/tests/test_controllers.py
@@ -0,0 +1,49 @@
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+import unittest
+import turbogears
+from turbogears import testutil
+from ipagui.controllers import Root
+import cherrypy
+
+cherrypy.root = Root()
+
+class TestPages(unittest.TestCase):
+
+ def setUp(self):
+ turbogears.startup.startTurboGears()
+
+ def tearDown(self):
+ """Tests for apps using identity need to stop CP/TG after each test to
+ stop the VisitManager thread.
+ See http://trac.turbogears.org/turbogears/ticket/1217 for details.
+ """
+ turbogears.startup.stopTurboGears()
+
+ def test_method(self):
+ "the index method should return a string called now"
+ import types
+ result = testutil.call(cherrypy.root.index)
+ assert type(result["now"]) == types.StringType
+
+ def test_indextitle(self):
+ "The indexpage should have the right title"
+ testutil.createRequest("/")
+ response = cherrypy.response.body[0].lower()
+ assert "<title>welcome to turbogears</title>" in response
+
diff --git a/ipa-server/ipa-gui/ipagui/tests/test_model.py b/ipa-server/ipa-gui/ipagui/tests/test_model.py
new file mode 100644
index 00000000..5bfb2315
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/tests/test_model.py
@@ -0,0 +1,39 @@
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+# If your project uses a database, you can set up database tests
+# similar to what you see below. Be sure to set the db_uri to
+# an appropriate uri for your testing database. sqlite is a good
+# choice for testing, because you can use an in-memory database
+# which is very fast.
+
+from turbogears import testutil, database
+# from ipagui.model import YourDataClass, User
+
+# database.set_db_uri("sqlite:///:memory:")
+
+# class TestUser(testutil.DBTest):
+# def get_model(self):
+# return User
+# def test_creation(self):
+# "Object creation should set the name"
+# obj = User(user_name = "creosote",
+# email_address = "spam@python.not",
+# display_name = "Mr Creosote",
+# password = "Wafer-thin Mint")
+# assert obj.display_name == "Mr Creosote"
+
diff --git a/ipa-server/ipa-gui/locales/ja/LC_MESSAGES/messages.po b/ipa-server/ipa-gui/locales/ja/LC_MESSAGES/messages.po
new file mode 100644
index 00000000..1e03419b
--- /dev/null
+++ b/ipa-server/ipa-gui/locales/ja/LC_MESSAGES/messages.po
@@ -0,0 +1,757 @@
+# Japanese Translation of IPA TurboGears UI
+# Copyright (C) 2007 Red Hat, Inc.
+# Masato Taruishi <taruishi@redhat.com>, 2007
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: 1.0\n"
+"POT-Creation-Date: 2007-12-20 18:55\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: Masato Taruishi <taruishi@redhat.com>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: utf-8\n"
+"Generated-By: pygettext.py 1.5\n"
+
+#: ipagui/templates/groupnew.kid:title
+msgid "Add Group"
+msgstr "グループã®è¿½åŠ "
+
+#: ipagui/templates/ipapolicyeditform.kid:h2
+msgid "Search"
+msgstr "検索"
+
+#: ipagui/templates/ipapolicyeditform.kid:h2
+msgid "Password Policy"
+msgstr "パスワードãƒãƒªã‚·"
+
+#: ipagui/templates/ipapolicyeditform.kid:h2
+msgid "User Settings"
+msgstr "ユーザ設定"
+
+#: ipagui/templates/ipapolicyeditform.kid:a
+msgid "Remove"
+msgstr "削除"
+
+#: ipagui/templates/ipapolicyeditform.kid:a
+msgid "Add User Object Class"
+msgstr "ユーザオブジェクトクラスã®è¿½åŠ "
+
+#: ipagui/templates/ipapolicyeditform.kid:a
+msgid "Add Group Object Class"
+msgstr "グループオブジェクトクラスã®è¿½åŠ "
+
+#: ipagui/templates/userlist.kid:title
+msgid "Find Users"
+msgstr "ãƒ¦ãƒ¼ã‚¶ã®æ¤œç´¢"
+
+#: ipagui/templates/userlist.kid:script
+msgid "document.getElementById(\"uid\").focus();"
+msgstr ""
+
+#: ipagui/templates/userlist.kid:h2
+msgid "${len(users)} results returned:"
+msgstr "${len(users)} 件見ã¤ã‹ã‚Šã¾ã—ãŸ:"
+
+#: ipagui/templates/userlist.kid:th
+msgid "User"
+msgstr "ユーザ"
+
+#: ipagui/templates/userlist.kid:th
+msgid "Phone"
+msgstr "電話"
+
+#: ipagui/templates/userlist.kid:th
+msgid "Unit"
+msgstr "ユニット"
+
+#: ipagui/templates/userlist.kid:th
+msgid "Job Title"
+msgstr ""
+
+#: ipagui/templates/userlist.kid:a
+msgid "${user.givenName} ${user.sn}"
+msgstr ""
+
+#: ipagui/templates/userlist.kid:td
+msgid "${user.telephoneNumber}"
+msgstr ""
+
+#: ipagui/templates/userlist.kid:td
+msgid "${user.ou}"
+msgstr ""
+
+#: ipagui/templates/userlist.kid:td
+msgid "${user.title}"
+msgstr ""
+
+#: ipagui/templates/userlist.kid:p
+msgid "No results found for \"${uid}\""
+msgstr ""
+
+#: ipagui/templates/userlist.kid:p
+msgid ""
+"Search automatically looks across multiple fields. If you want to find\n"
+" Joe in Finance, try typing \"joe finance\" into the search box."
+msgstr ""
+"検索ã¯è¤‡æ•°ã®ãƒ•ィールドã«å¯¾ã—ã¦è¡Œã‚れã¾ã™ã€‚ã—ãŸãŒã£ã¦ã€ä¾‹ãˆã°çµŒç†éƒ¨ã®ç”°ä¸­ã•ã‚“ã‚’\n"
+"検索ã—ãŸã„å ´åˆã¯ã€\"田中 経ç†\" ã¨æ¤œç´¢ãƒœãƒƒã‚¯ã‚¹ã«å…¥åŠ›ã—ã¾ã™ã€‚"
+
+#: ipagui/templates/userlist.kid:p
+msgid ""
+"Exact matches are listed first, followed by partial matches. If your search\n"
+" is too broad, you will get a warning that the search returned too many\n"
+" results. Try being more specific."
+msgstr ""
+"完全一致ã®çµæžœãŒåˆã‚ã«ã€æ¬¡ã«éƒ¨åˆ†ä¸€è‡´ã®çµæžœãŒè¡¨ç¤ºã•れã¾ã™ã€‚検索å¼ãŒçŸ­ã™ãŽã‚‹ã¨ã€\n"
+"æ¤œç´¢çµæžœãŒå¤šã™ãŽã‚‹ã¨ã„ã†è­¦å‘Šãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ãŒè¡¨ç¤ºã•れã¦ã—ã¾ã„ã¾ã™ã®ã§ã€é•·ã„検索å¼\n"
+"を利用ã—ã¦ãã ã•ã„。"
+
+#: ipagui/templates/userlist.kid:p
+msgid ""
+"The results that come back are sortable. Simply click on a column\n"
+" header to sort on that header. A triangle will indicate the sorted\n"
+" column, along with its direction. Clicking and dragging between headers\n"
+" will allow you to resize the header."
+msgstr ""
+"çµæžœã¯ã‚½ãƒ¼ãƒˆã•れã¾ã™ã€‚カラムをクリックã™ã‚‹ã“ã¨ã§ã‚½ãƒ¼ãƒˆã™ã‚‹ãƒ˜ãƒƒãƒ€ã‚’変ãˆã‚‰ã‚Œã¾ã™ã€‚\n"
+"ソートã«åˆ©ç”¨ã•れãŸã‚«ãƒ©ãƒ ã«ã¯ä¸‰è§’å½¢ãŒè¡¨ç¤ºã•れã€ä¸‰è§’å½¢ã®å‘ããŒã‚½ãƒ¼ãƒˆã®æ–¹å‘を表ã—ã¾ã™ã€‚\n"
+"ヘッダをドラッグã™ã‚‹ã“ã¨ã§ãƒ˜ãƒƒãƒ€ã®ã‚µã‚¤ã‚ºã‚’変ãˆã‚‰ã‚Œã¾ã™ã€‚"
+
+#: ipagui/templates/unhandled_exception.kid:title
+msgid "Error"
+msgstr ""
+
+#: ipagui/templates/unhandled_exception.kid:h1
+msgid "An unexpected error occured"
+msgstr ""
+
+#: ipagui/templates/unhandled_exception.kid:b
+msgid "Message:"
+msgstr ""
+
+#: ipagui/templates/unhandled_exception.kid:pre
+msgid "${message}"
+msgstr ""
+
+#: ipagui/templates/unhandled_exception.kid:b
+msgid "HTTP Error Message:"
+msgstr ""
+
+#: ipagui/templates/unhandled_exception.kid:pre
+msgid "${error_msg}"
+msgstr ""
+
+#: ipagui/templates/unhandled_exception.kid:b
+msgid "Stack Trace:"
+msgstr ""
+
+#: ipagui/templates/unhandled_exception.kid:pre
+msgid "${details}"
+msgstr ""
+
+#: ipagui/templates/ipapolicyshow.kid:title
+msgid "Manage IPA Policy"
+msgstr "IPA ãƒãƒªã‚·ã®ç®¡ç†"
+
+#: ipagui/templates/ipapolicyshow.kid:td
+msgid "${ipapolicy.get(\"ipasearchtimelimit\")}"
+msgstr ""
+
+#: ipagui/templates/ipapolicyshow.kid:td
+msgid "${ipapolicy.get(\"ipasearchrecordslimit\")}"
+msgstr ""
+
+#: ipagui/templates/ipapolicyshow.kid:td
+msgid "${ipapolicy.get(\"ipausersearchfields\")}"
+msgstr ""
+
+#: ipagui/templates/ipapolicyshow.kid:td
+msgid "${ipapolicy.get(\"ipagroupsearchfields\")}"
+msgstr ""
+
+#: ipagui/templates/ipapolicyshow.kid:td
+msgid "${ipapolicy.get(\"ipapwdexpadvnotify\")}"
+msgstr ""
+
+#: ipagui/templates/ipapolicyshow.kid:td
+msgid "${password.get(\"krbminpwdlife\")}"
+msgstr ""
+
+#: ipagui/templates/ipapolicyshow.kid:td
+msgid "${password.get(\"krbmaxpwdlife\")}"
+msgstr ""
+
+#: ipagui/templates/ipapolicyshow.kid:td
+msgid "${password.get(\"krbpwdmindiffchars\")}"
+msgstr ""
+
+#: ipagui/templates/ipapolicyshow.kid:td
+msgid "${password.get(\"krbpwdminlength\")}"
+msgstr ""
+
+#: ipagui/templates/ipapolicyshow.kid:td
+msgid "${password.get(\"krbpwdhistorylength\")}"
+msgstr ""
+
+#: ipagui/templates/ipapolicyshow.kid:td
+msgid "${ipapolicy.get(\"ipamaxusernamelength\")}"
+msgstr ""
+
+#: ipagui/templates/ipapolicyshow.kid:td
+msgid "${ipapolicy.get(\"ipahomesrootdir\")}"
+msgstr ""
+
+#: ipagui/templates/ipapolicyshow.kid:td
+msgid "${ipapolicy.get(\"ipadefaultloginshell\")}"
+msgstr ""
+
+#: ipagui/templates/ipapolicyshow.kid:td
+msgid "${ipapolicy.get(\"ipadefaultprimarygroup\")}"
+msgstr ""
+
+#: ipagui/templates/ipapolicyshow.kid:td
+msgid "${ipapolicy.get(\"ipadefaultemaildomain\")}"
+msgstr ""
+
+#: ipagui/templates/ipapolicyshow.kid:td
+msgid "${values[index]}"
+msgstr ""
+
+#: ipagui/templates/usernew.kid:title
+msgid "Add User"
+msgstr "ユーザã®è¿½åŠ "
+
+#: ipagui/templates/principalnewform.kid:h2
+msgid "Service Principal Details"
+msgstr "サービスプリンシパル詳細"
+
+#: ipagui/templates/delegateform.kid:h2
+msgid "Delegation Details"
+msgstr "権é™å§”è­²ã®è©³ç´°"
+
+#: ipagui/templates/delegateform.kid:span
+msgid "${value_for(delegate_fields.source_group_cn)}"
+msgstr ""
+
+#: ipagui/templates/delegateform.kid:a
+msgid "change"
+msgstr ""
+
+#: ipagui/templates/delegateform.kid:span
+msgid "${value_for(delegate_fields.dest_group_cn)}"
+msgstr ""
+
+#: ipagui/templates/ipapolicyedit.kid:title
+msgid "Edit IPA Policy"
+msgstr "IPA ãƒãƒªã‚·ã®ç·¨é›†"
+
+#: ipagui/templates/master.kid:title
+msgid "Your title goes here"
+msgstr ""
+
+#: ipagui/templates/master.kid:style
+msgid "@import \"${tg.url('/static/css/style.css')}\";"
+msgstr ""
+
+#: ipagui/templates/master.kid:a
+msgid "Free IPA"
+msgstr ""
+
+#: ipagui/templates/master.kid:option
+msgid "Users"
+msgstr ""
+
+#: ipagui/templates/master.kid:option
+msgid "Groups"
+msgstr ""
+
+#: ipagui/templates/master.kid:script
+msgid ""
+"function clearsearch() {\n"
+" topsearchbox = document.getElementById('topsearchbox');\n"
+" topsearchbox.onfocus = null;\n"
+" topsearchbox.value = \"\";\n"
+" }"
+msgstr ""
+
+#: ipagui/templates/master.kid:span
+msgid "Kerberos login failed."
+msgstr "Keberos ログインã«å¤±æ•—ã—ã¾ã—ãŸã€‚"
+
+#: ipagui/templates/master.kid:span
+msgid "Logged in as: ${tg.identity.user.display_name}"
+msgstr "${tg.identity.user.display_name} ã§ãƒ­ã‚°ã‚¤ãƒ³ã—ã¦ã„ã¾ã™"
+
+#: ipagui/templates/master.kid:h2
+msgid "Tasks"
+msgstr "タスク一覧"
+
+#: ipagui/templates/master.kid:a
+msgid "Find Groups"
+msgstr "ã‚°ãƒ«ãƒ¼ãƒ—ã®æ¤œç´¢"
+
+#: ipagui/templates/master.kid:a
+msgid "Add Service Principal"
+msgstr "サービスプリンシパルã®è¿½åŠ "
+
+#: ipagui/templates/master.kid:a
+msgid "Find Service Principal"
+msgstr "ã‚µãƒ¼ãƒ“ã‚¹ãƒ—ãƒªãƒ³ã‚·ãƒ‘ãƒ«ã®æ¤œç´¢"
+
+#: ipagui/templates/master.kid:a
+msgid "Manage Policy"
+msgstr "ãƒãƒªã‚·ã®ç®¡ç†"
+
+#: ipagui/templates/master.kid:a
+msgid "Self Service"
+msgstr "自サービス"
+
+#: ipagui/templates/master.kid:a
+msgid "Delegations"
+msgstr "権é™ã®å§”è­²"
+
+#: ipagui/templates/master.kid:a
+msgid "Powered by FreeIPA"
+msgstr ""
+
+#: ipagui/templates/groupeditform.kid:h2
+msgid "Group Details"
+msgstr "グループ詳細"
+
+#: ipagui/templates/groupeditform.kid:h2
+msgid "Group Members"
+msgstr "グループメンãƒ"
+
+#: ipagui/templates/groupeditform.kid:div
+msgid "To Remove:"
+msgstr ""
+
+#: ipagui/templates/groupeditform.kid:a
+msgid "remove"
+msgstr ""
+
+#: ipagui/templates/groupeditform.kid:h2
+msgid "Add Members"
+msgstr "メンãƒã®è¿½åŠ "
+
+#: ipagui/templates/groupeditform.kid:div
+msgid "To Add:"
+msgstr ""
+
+#: ipagui/templates/usereditform.kid:h2
+msgid "Identity Details"
+msgstr "アイデンティティ詳細"
+
+#: ipagui/templates/usereditform.kid:a
+msgid "Add Common Name"
+msgstr "ニックãƒãƒ¼ãƒ ã®è¿½åŠ "
+
+#: ipagui/templates/usereditform.kid:h2
+msgid "Account Details"
+msgstr "アカウント詳細"
+
+#: ipagui/templates/usereditform.kid:h2
+msgid "Contact Details"
+msgstr "連絡先詳細"
+
+#: ipagui/templates/usereditform.kid:a
+msgid "Add Work Number"
+msgstr "è·å ´ã®é›»è©±ç•ªå·ã‚’追加"
+
+#: ipagui/templates/usereditform.kid:a
+msgid "Add Fax Number"
+msgstr "ファックス番å·ã‚’追加"
+
+#: ipagui/templates/usereditform.kid:a
+msgid "Add Cell Number"
+msgstr ""
+
+#: ipagui/templates/usereditform.kid:a
+msgid "Add Pager Number"
+msgstr ""
+
+#: ipagui/templates/usereditform.kid:a
+msgid "Add Home Phone"
+msgstr "自宅ã®é›»è©±ç•ªå·ã‚’追加"
+
+#: ipagui/templates/usereditform.kid:h2
+msgid "Mailing Address"
+msgstr "メールアドレス"
+
+#: ipagui/templates/usereditform.kid:h2
+msgid "Employee Information"
+msgstr "社員情報"
+
+#: ipagui/templates/usereditform.kid:span
+msgid "${value_for(user_fields.manager_cn)}"
+msgstr ""
+
+#: ipagui/templates/usereditform.kid:a
+msgid "clear"
+msgstr ""
+
+#: ipagui/templates/usereditform.kid:span
+msgid "${value_for(user_fields.secretary_cn)}"
+msgstr ""
+
+#: ipagui/templates/usereditform.kid:h2
+msgid "Misc Information"
+msgstr "ãã®ä»–"
+
+#: ipagui/templates/usereditform.kid:h2
+msgid "Custom Fields"
+msgstr "カスタムフィールド"
+
+#: ipagui/templates/usereditform.kid:h2
+msgid "Add Groups"
+msgstr "グループã®è¿½åŠ "
+
+#: ipagui/templates/grouplist.kid:script
+msgid "document.getElementById(\"criteria\").focus();"
+msgstr ""
+
+#: ipagui/templates/grouplist.kid:h2
+msgid "${len(groups)} results returned:"
+msgstr ""
+
+#: ipagui/templates/grouplist.kid:th
+msgid "${fields.cn.label}"
+msgstr ""
+
+#: ipagui/templates/grouplist.kid:th
+msgid "${fields.description.label}"
+msgstr ""
+
+#: ipagui/templates/grouplist.kid:a
+msgid "${group.cn}"
+msgstr ""
+
+#: ipagui/templates/grouplist.kid:td
+msgid "${group.description}"
+msgstr ""
+
+#: ipagui/templates/grouplist.kid:h2
+msgid "No results found for \"${criteria}\""
+msgstr ""
+
+#: ipagui/templates/dynamiceditsearch.kid:a
+msgid "add"
+msgstr ""
+
+#: ipagui/templates/usernewform.kid:td
+msgid "Generated by server"
+msgstr "サーãƒã«ã‚ˆã‚Šç”Ÿæˆã•れã¾ã™"
+
+#: ipagui/templates/usernewform.kid:span
+msgid "${value_for(user_fields.manager)}"
+msgstr ""
+
+#: ipagui/templates/usernewform.kid:span
+msgid "${value_for(user_fields.secretary)}"
+msgstr ""
+
+#: ipagui/templates/userselectsearch.kid:span
+msgid "(truncated)"
+msgstr ""
+
+#: ipagui/templates/userselectsearch.kid:div
+msgid "${user_name} ${user_descr}"
+msgstr ""
+
+#: ipagui/templates/userselectsearch.kid:a
+msgid "select"
+msgstr ""
+
+#: ipagui/templates/groupshow.kid:title
+msgid "View Group"
+msgstr "グループã®é–²è¦§"
+
+#: ipagui/templates/groupshow.kid:td
+msgid "${group.get(\"cn\")}"
+msgstr ""
+
+#: ipagui/templates/groupshow.kid:td
+msgid "${group.get(\"description\")}"
+msgstr ""
+
+#: ipagui/templates/groupshow.kid:td
+msgid "${group.get(\"gidnumber\")}"
+msgstr ""
+
+#: ipagui/templates/groupshow.kid:td
+msgid "${userhelper.account_status_display(group.get(\"nsAccountLock\"))}"
+msgstr ""
+
+#: ipagui/templates/groupshow.kid:a
+msgid "${member_cn}"
+msgstr ""
+
+#: ipagui/templates/groupedit.kid:title
+msgid "Edit Group"
+msgstr "グループã®ç·¨é›†"
+
+#: ipagui/templates/groupedit.kid:span
+msgid "edit protected fields"
+msgstr "ä¿è­·ã•れãŸãƒ•ィールドã®ç·¨é›†"
+
+#: ipagui/templates/policyindex.kid:a
+msgid "IPA Policy"
+msgstr "IPA ãƒãƒªã‚·"
+
+#: ipagui/templates/delegatelist.kid:script
+msgid ""
+"function editDelegation(acistr) {\n"
+" $('edit_acistr').value = acistr;\n"
+" $('editform').submit();\n"
+" return false;\n"
+" }"
+msgstr ""
+
+#: ipagui/templates/delegatelist.kid:th
+msgid "${fields.name.label}"
+msgstr ""
+
+#: ipagui/templates/delegatelist.kid:th
+msgid "${fields.source_group_cn.label}"
+msgstr ""
+
+#: ipagui/templates/delegatelist.kid:th
+msgid "${fields.attrs.label}"
+msgstr ""
+
+#: ipagui/templates/delegatelist.kid:th
+msgid "${fields.dest_group_cn.label}"
+msgstr ""
+
+#: ipagui/templates/delegatelist.kid:a
+msgid "${aci.name}"
+msgstr ""
+
+#: ipagui/templates/delegatelist.kid:a
+msgid "${source_cn}"
+msgstr ""
+
+#: ipagui/templates/delegatelist.kid:td
+msgid "${\", \".join(aci.attrs)}"
+msgstr ""
+
+#: ipagui/templates/delegatelist.kid:a
+msgid "${dest_cn}"
+msgstr ""
+
+#: ipagui/templates/delegatelist.kid:a
+msgid "add new delegation"
+msgstr "æ–°ã—ã„æ¨©é™å§”è­²ã®è¿½åŠ "
+
+#: ipagui/templates/useredit.kid:title
+msgid "Edit User"
+msgstr "ユーザã®ç·¨é›†"
+
+#: ipagui/templates/useredit.kid:div
+msgid "${user.get(\"uid\")}'s password will expire in ${pw_expires_days} day${days_suffix}"
+msgstr ""
+
+#: ipagui/templates/useredit.kid:div
+msgid "${user.get(\"uid\")}'s password has expired"
+msgstr ""
+
+#: ipagui/templates/welcome.kid:title
+msgid "Welcome"
+msgstr "よã†ã“ã"
+
+#: ipagui/templates/welcome.kid:h1
+msgid "Welcome to Free IPA"
+msgstr "Free IPA ã¸ã‚ˆã†ã“ã"
+
+#: ipagui/templates/welcome.kid:span
+msgid "This site makes heavy use of JavaScript."
+msgstr "ã“ã®ã‚µã‚¤ãƒˆã¯ JavaScript を多用ã—ã¦ã„ã¾ã™ã€‚"
+
+#: ipagui/templates/welcome.kid:p
+msgid "IPA is used to manage Identity, Policy, and Auditing for your organization."
+msgstr "IPA ã¯çµ„ç¹”ã®ã‚¢ã‚¤ãƒ‡ãƒ³ãƒ†ã‚£ãƒ†ã‚£ã€ãƒãƒªã‚·ã€ç›£æŸ»ã‚·ã‚¹ãƒ†ãƒ ã‚’管ç†ã—ã¾ã™ã€‚"
+
+#: ipagui/templates/welcome.kid:p
+msgid ""
+"To get started, you can use the search box in the top right to find\n"
+" users or groups you need to work on. Search automatically looks\n"
+" across multiple fields. If you want to find Joe in Finance, try typing\n"
+" \"joe finance\" into the search box."
+msgstr ""
+"ãれã§ã¯å§‹ã‚ã¾ã—ょã†ã€‚作業ã™ã‚‹å¿…è¦ã®ã‚るユーザやグループを検索ã™ã‚‹ã®ã«å³ä¸Šã®æ¤œç´¢\n"
+"ボックスãŒåˆ©ç”¨ã§ãã¾ã™ã€‚検索ã¯è¤‡æ•°ã®ãƒ•ィールドã«å¯¾ã—ã¦è¡Œã‚れã¾ã™ï¼Žã—ãŸãŒã£ã¦ã€\n"
+"経ç†éƒ¨ã®ç”°ä¸­ã•んを検索ã—ãŸã„å ´åˆã¯ã€\"経ç†éƒ¨ 田中\" ã¨å…¥åŠ›ã—ã¾ã™ã€‚"
+
+#: ipagui/templates/welcome.kid:p
+msgid "Alternatively, select a task from the right sidebar."
+msgstr "ã‚‚ã—ãã¯å³ã®ã‚µã‚¤ãƒ‰ãƒãƒ¼ã‚ˆã‚Šã‚¿ã‚¹ã‚¯ã‚’é¸æŠžã—ã¦ãã ã•ã„。"
+
+#: ipagui/templates/principallist.kid:title
+msgid "Find Service Principals"
+msgstr "ã‚µãƒ¼ãƒ“ã‚¹ãƒ—ãƒªãƒ³ã‚·ãƒ‘ãƒ«ã®æ¤œç´¢"
+
+#: ipagui/templates/principallist.kid:script
+msgid "document.getElementById(\"hostname\").focus();"
+msgstr ""
+
+#: ipagui/templates/principallist.kid:h2
+msgid "${len(principals)} results returned:"
+msgstr "${len(principals)} 件見ã¤ã‹ã‚Šã¾ã—ãŸ:"
+
+#: ipagui/templates/principallist.kid:th
+msgid "Hostname"
+msgstr "ホストå"
+
+#: ipagui/templates/principallist.kid:th
+msgid "Service"
+msgstr "サービス"
+
+#: ipagui/templates/principallist.kid:td
+msgid "${principal.hostname}"
+msgstr ""
+
+#: ipagui/templates/principallist.kid:td
+msgid "${principal.service}"
+msgstr ""
+
+#: ipagui/templates/principallist.kid:p
+msgid "No results found for \"${hostname}\""
+msgstr ""
+
+#: ipagui/templates/delegateedit.kid:title
+msgid "Edit Delegation"
+msgstr "権é™å§”è­²ã®ç·¨é›†"
+
+#: ipagui/templates/usershow.kid:title
+msgid "View User"
+msgstr "ユーザ情報ã®é–²è¦§"
+
+#: ipagui/templates/usershow.kid:td
+msgid "${user.get(\"title\")}"
+msgstr ""
+
+#: ipagui/templates/usershow.kid:td
+msgid "${user.get(\"givenname\")}"
+msgstr ""
+
+#: ipagui/templates/usershow.kid:td
+msgid "${user.get(\"sn\")}"
+msgstr ""
+
+#: ipagui/templates/usershow.kid:td
+msgid "${user.get(\"displayname\")}"
+msgstr ""
+
+#: ipagui/templates/usershow.kid:td
+msgid "${user.get(\"initials\")}"
+msgstr ""
+
+#: ipagui/templates/usershow.kid:td
+msgid "${userhelper.account_status_display(user.get(\"nsAccountLock\"))}"
+msgstr ""
+
+#: ipagui/templates/usershow.kid:td
+msgid "${user.get(\"uid\")}"
+msgstr ""
+
+#: ipagui/templates/usershow.kid:td
+msgid "${user.get(\"uidnumber\")}"
+msgstr ""
+
+#: ipagui/templates/usershow.kid:td
+msgid "${user.get(\"gidnumber\")}"
+msgstr ""
+
+#: ipagui/templates/usershow.kid:td
+msgid "${user.get(\"homedirectory\")}"
+msgstr ""
+
+#: ipagui/templates/usershow.kid:td
+msgid "${user.get(\"loginshell\")}"
+msgstr ""
+
+#: ipagui/templates/usershow.kid:td
+msgid "${user.get(\"gecos\")}"
+msgstr ""
+
+#: ipagui/templates/usershow.kid:a
+msgid "${user.get(\"mail\")}"
+msgstr ""
+
+#: ipagui/templates/usershow.kid:td
+msgid "${user.get(\"street\")}"
+msgstr ""
+
+#: ipagui/templates/usershow.kid:td
+msgid "${user.get(\"roomnumber\")}"
+msgstr ""
+
+#: ipagui/templates/usershow.kid:td
+msgid "${user.get(\"l\")}"
+msgstr ""
+
+#: ipagui/templates/usershow.kid:td
+msgid "${user.get(\"st\")}"
+msgstr ""
+
+#: ipagui/templates/usershow.kid:td
+msgid "${user.get(\"postalcode\")}"
+msgstr ""
+
+#: ipagui/templates/usershow.kid:td
+msgid "${user.get(\"ou\")}"
+msgstr ""
+
+#: ipagui/templates/usershow.kid:td
+msgid "${user.get(\"businesscategory\")}"
+msgstr ""
+
+#: ipagui/templates/usershow.kid:td
+msgid "${user.get(\"description\")}"
+msgstr ""
+
+#: ipagui/templates/usershow.kid:td
+msgid "${user.get(\"employeetype\")}"
+msgstr ""
+
+#: ipagui/templates/usershow.kid:a
+msgid "${user_manager.givenname} ${user_manager.sn}"
+msgstr ""
+
+#: ipagui/templates/usershow.kid:a
+msgid "${user_secretary.givenname} ${user_secretary.sn}"
+msgstr ""
+
+#: ipagui/templates/usershow.kid:td
+msgid "${user.get(\"carlicense\")}"
+msgstr ""
+
+#: ipagui/templates/usershow.kid:a
+msgid "${user.get('labeleduri')}"
+msgstr ""
+
+#: ipagui/templates/usershow.kid:td
+msgid "${user.get(custom_field.name)}"
+msgstr ""
+
+#: ipagui/templates/usershow.kid:h2
+msgid "Direct Reports"
+msgstr ""
+
+#: ipagui/templates/usershow.kid:a
+msgid "${report.givenname} ${report.sn}"
+msgstr ""
+
+#: ipagui/templates/loginfailed.kid:title
+msgid "Permission Denied"
+msgstr "権é™ãŒã‚りã¾ã›ã‚“"
+
+#: ipagui/templates/loginfailed.kid:p
+msgid "You do not have permission to access this page."
+msgstr "ã“ã®ãƒšãƒ¼ã‚¸ã«ã‚¢ã‚¯ã‚»ã‚¹ã™ã‚‹æ¨©é™ãŒã‚りã¾ã›ã‚“。"
+
+#: ipagui/templates/delegatenew.kid:title
+msgid "Add Delegation"
+msgstr "権é™å§”è­²ã®è¿½åŠ "
diff --git a/ipa-server/ipa-gui/locales/messages.pot b/ipa-server/ipa-gui/locales/messages.pot
new file mode 100644
index 00000000..9140381c
--- /dev/null
+++ b/ipa-server/ipa-gui/locales/messages.pot
@@ -0,0 +1,782 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR ORGANIZATION
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"POT-Creation-Date: 2008-07-25 09:17\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: utf-8\n"
+"Generated-By: pygettext.py 1.5\n"
+
+
+#: ipagui/forms/delegate.py:83 ipagui/forms/delegate.py:85
+msgid "Please choose a group"
+msgstr ""
+
+#: ipagui/helpers/validators.py:42
+msgid "Duplicate values are not allowed"
+msgstr ""
+
+#: ipagui/helpers/validators.py:43
+msgid "Empty values not allowed"
+msgstr ""
+
+#: ipagui/subcontrollers/delegation.py:409
+msgid "Please select at least one value"
+msgstr ""
+
+#: ipagui/subcontrollers/delegation.py:411
+#: ipagui/subcontrollers/delegation.py:413
+msgid "Group not found"
+msgstr ""
+
+#: ipagui/templates/ipapolicyeditform.kid:div
+msgid ""
+msgstr ""
+
+#: ipagui/templates/ipapolicyeditform.kid:h2
+msgid "Search"
+msgstr ""
+
+#: ipagui/templates/ipapolicyeditform.kid:h2
+msgid "Password Policy"
+msgstr ""
+
+#: ipagui/templates/ipapolicyeditform.kid:h2
+msgid "User Settings"
+msgstr ""
+
+#: ipagui/templates/principalshow.kid:title
+msgid "View Service Principal"
+msgstr ""
+
+#: ipagui/templates/principalshow.kid:script
+msgid ""
+"function confirmDelete() {\n"
+" if (confirm(\"Are you sure you want to delete this service principal?\")) {\n"
+" $('deleteform').submit();\n"
+" }\n"
+" return false;\n"
+" }"
+msgstr ""
+
+#: ipagui/templates/principalshow.kid:h2
+msgid "Principal"
+msgstr ""
+
+#: ipagui/templates/principalshow.kid:label
+msgid "Host"
+msgstr ""
+
+#: ipagui/templates/principalshow.kid:td
+msgid "${principal.get(\"hostname\")}"
+msgstr ""
+
+#: ipagui/templates/principalshow.kid:label
+msgid "Service"
+msgstr ""
+
+#: ipagui/templates/principalshow.kid:td
+msgid "${principal.get(\"service\")}"
+msgstr ""
+
+#: ipagui/templates/delegatelist.kid:title
+msgid "Delegations"
+msgstr ""
+
+#: ipagui/templates/delegatelist.kid:script
+msgid ""
+"function editDelegation(acistr) {\n"
+" $('edit_acistr').value = acistr;\n"
+" $('editform').submit();\n"
+" return false;\n"
+" }"
+msgstr ""
+
+#: ipagui/templates/delegatelist.kid:th
+msgid "${fields.name.label}"
+msgstr ""
+
+#: ipagui/templates/delegatelist.kid:th
+msgid "${fields.source_group_cn.label}"
+msgstr ""
+
+#: ipagui/templates/delegatelist.kid:th
+msgid "${fields.attrs.label}"
+msgstr ""
+
+#: ipagui/templates/delegatelist.kid:th
+msgid "${fields.dest_group_cn.label}"
+msgstr ""
+
+#: ipagui/templates/delegatelist.kid:a
+msgid "${aci.name}"
+msgstr ""
+
+#: ipagui/templates/delegatelist.kid:a
+msgid "${source_cn}"
+msgstr ""
+
+#: ipagui/templates/delegatelist.kid:td
+msgid "${\", \".join(aci.attrs)}"
+msgstr ""
+
+#: ipagui/templates/delegatelist.kid:a
+msgid "${dest_cn}"
+msgstr ""
+
+#: ipagui/templates/delegatelist.kid:a
+msgid "add new delegation"
+msgstr ""
+
+#: ipagui/templates/welcome.kid:title
+msgid "Welcome"
+msgstr ""
+
+#: ipagui/templates/welcome.kid:h1
+msgid "Welcome to Free IPA"
+msgstr ""
+
+#: ipagui/templates/welcome.kid:span
+msgid "This site makes heavy use of JavaScript."
+msgstr ""
+
+#: ipagui/templates/welcome.kid:p
+msgid "IPA is used to manage Identity, Policy, and Auditing for your organization."
+msgstr ""
+
+#: ipagui/templates/welcome.kid:p
+msgid ""
+"To get started, you can use the search box in the top right to find\n"
+" users or groups you need to work on. Search automatically looks\n"
+" across multiple fields. If you want to find Joe in Finance, try typing\n"
+" \"joe finance\" into the search box."
+msgstr ""
+
+#: ipagui/templates/welcome.kid:p
+msgid "Alternatively, select a task from the right sidebar."
+msgstr ""
+
+#: ipagui/templates/delegateedit.kid:title
+msgid "Edit Delegation"
+msgstr ""
+
+#: ipagui/templates/delegategroupsearch.kid:div
+msgid "${len(groups)} results returned:"
+msgstr ""
+
+#: ipagui/templates/delegategroupsearch.kid:span
+msgid "(truncated)"
+msgstr ""
+
+#: ipagui/templates/delegategroupsearch.kid:div
+msgid "${group.cn}"
+msgstr ""
+
+#: ipagui/templates/delegategroupsearch.kid:a
+msgid "select"
+msgstr ""
+
+#: ipagui/templates/delegategroupsearch.kid:div
+msgid "No results found for \"${criteria}\""
+msgstr ""
+
+#: ipagui/templates/usernewform.kid:h2
+msgid "Identity Details"
+msgstr ""
+
+#: ipagui/templates/usernewform.kid:a
+msgid "Remove"
+msgstr ""
+
+#: ipagui/templates/usernewform.kid:a
+msgid "Add Full Name"
+msgstr ""
+
+#: ipagui/templates/usernewform.kid:h2
+msgid "Account Details"
+msgstr ""
+
+#: ipagui/templates/usernewform.kid:td
+msgid "Generated by server"
+msgstr ""
+
+#: ipagui/templates/usernewform.kid:h2
+msgid "Contact Details"
+msgstr ""
+
+#: ipagui/templates/usernewform.kid:a
+msgid "Add Work Number"
+msgstr ""
+
+#: ipagui/templates/usernewform.kid:a
+msgid "Add Fax Number"
+msgstr ""
+
+#: ipagui/templates/usernewform.kid:a
+msgid "Add Cell Number"
+msgstr ""
+
+#: ipagui/templates/usernewform.kid:a
+msgid "Add Pager Number"
+msgstr ""
+
+#: ipagui/templates/usernewform.kid:a
+msgid "Add Home Phone"
+msgstr ""
+
+#: ipagui/templates/usernewform.kid:h2
+msgid "Mailing Address"
+msgstr ""
+
+#: ipagui/templates/usernewform.kid:h2
+msgid "Employee Information"
+msgstr ""
+
+#: ipagui/templates/usernewform.kid:span
+msgid "${value_for(user_fields.manager)}"
+msgstr ""
+
+#: ipagui/templates/usernewform.kid:a
+msgid "clear"
+msgstr ""
+
+#: ipagui/templates/usernewform.kid:a
+msgid "change"
+msgstr ""
+
+#: ipagui/templates/usernewform.kid:span
+msgid "${value_for(user_fields.secretary)}"
+msgstr ""
+
+#: ipagui/templates/usernewform.kid:h2
+msgid "Misc Information"
+msgstr ""
+
+#: ipagui/templates/usernewform.kid:div
+msgid "Custom Fields"
+msgstr ""
+
+#: ipagui/templates/usernewform.kid:h2
+msgid "Add Groups"
+msgstr ""
+
+#: ipagui/templates/usernewform.kid:div
+msgid "To Add:"
+msgstr ""
+
+#: ipagui/templates/policyindex.kid:title
+msgid "Manage Policy"
+msgstr ""
+
+#: ipagui/templates/policyindex.kid:a
+msgid "IPA Policy"
+msgstr ""
+
+#: ipagui/templates/principallist.kid:title
+msgid "Find Service Principals"
+msgstr ""
+
+#: ipagui/templates/principallist.kid:script
+msgid "document.getElementById(\"hostname\").focus();"
+msgstr ""
+
+#: ipagui/templates/principallist.kid:h2
+msgid "${len(principals)} results returned:"
+msgstr ""
+
+#: ipagui/templates/principallist.kid:th
+msgid "Hostname"
+msgstr ""
+
+#: ipagui/templates/principallist.kid:a
+msgid "${principal.hostname}"
+msgstr ""
+
+#: ipagui/templates/principallist.kid:a
+msgid "${principal.service}"
+msgstr ""
+
+#: ipagui/templates/principallist.kid:p
+msgid "No results found for \"${hostname}\""
+msgstr ""
+
+#: ipagui/templates/principallist.kid:p
+msgid ""
+"Exact matches are listed first, followed by partial matches. If your search\n"
+" is too broad, you will get a warning that the search returned too many\n"
+" results. Try being more specific."
+msgstr ""
+
+#: ipagui/templates/principallist.kid:p
+msgid ""
+"The results that come back are sortable. Simply click on a column\n"
+" header to sort on that header. A triangle will indicate the sorted\n"
+" column, along with its direction. Clicking and dragging between headers\n"
+" will allow you to resize the header."
+msgstr ""
+
+#: ipagui/templates/usernew.kid:title
+msgid "Add User"
+msgstr ""
+
+#: ipagui/templates/delegatenew.kid:title
+msgid "Add Delegation"
+msgstr ""
+
+#: ipagui/templates/usershow.kid:title
+msgid "View User"
+msgstr ""
+
+#: ipagui/templates/usershow.kid:div
+msgid "${user.get(\"uid\")}'s password will expire in ${pw_expires_days} day${days_suffix}"
+msgstr ""
+
+#: ipagui/templates/usershow.kid:div
+msgid "${user.get(\"uid\")}'s password has expired"
+msgstr ""
+
+#: ipagui/templates/usershow.kid:td
+msgid "${user.get(\"title\")}"
+msgstr ""
+
+#: ipagui/templates/usershow.kid:td
+msgid "${user.get(\"givenname\")}"
+msgstr ""
+
+#: ipagui/templates/usershow.kid:td
+msgid "${user.get(\"sn\")}"
+msgstr ""
+
+#: ipagui/templates/usershow.kid:td
+msgid "${values[index]}"
+msgstr ""
+
+#: ipagui/templates/usershow.kid:td
+msgid "${user.get(\"displayname\")}"
+msgstr ""
+
+#: ipagui/templates/usershow.kid:td
+msgid "${user.get(\"initials\")}"
+msgstr ""
+
+#: ipagui/templates/usershow.kid:td
+msgid "${userhelper.account_status_display(user.get(\"nsAccountLock\"))}"
+msgstr ""
+
+#: ipagui/templates/usershow.kid:td
+msgid "${user.get(\"uid\")}"
+msgstr ""
+
+#: ipagui/templates/usershow.kid:td
+msgid "${user.get(\"uidnumber\")}"
+msgstr ""
+
+#: ipagui/templates/usershow.kid:td
+msgid "${user.get(\"gidnumber\")}"
+msgstr ""
+
+#: ipagui/templates/usershow.kid:td
+msgid "${user.get(\"homedirectory\")}"
+msgstr ""
+
+#: ipagui/templates/usershow.kid:td
+msgid "${user.get(\"loginshell\")}"
+msgstr ""
+
+#: ipagui/templates/usershow.kid:td
+msgid "${user.get(\"gecos\")}"
+msgstr ""
+
+#: ipagui/templates/usershow.kid:a
+msgid "${user.get(\"mail\")}"
+msgstr ""
+
+#: ipagui/templates/usershow.kid:td
+msgid "${user.get(\"street\")}"
+msgstr ""
+
+#: ipagui/templates/usershow.kid:td
+msgid "${user.get(\"roomnumber\")}"
+msgstr ""
+
+#: ipagui/templates/usershow.kid:td
+msgid "${user.get(\"l\")}"
+msgstr ""
+
+#: ipagui/templates/usershow.kid:td
+msgid "${user.get(\"st\")}"
+msgstr ""
+
+#: ipagui/templates/usershow.kid:td
+msgid "${user.get(\"postalcode\")}"
+msgstr ""
+
+#: ipagui/templates/usershow.kid:td
+msgid "${user.get(\"ou\")}"
+msgstr ""
+
+#: ipagui/templates/usershow.kid:td
+msgid "${user.get(\"businesscategory\")}"
+msgstr ""
+
+#: ipagui/templates/usershow.kid:td
+msgid "${user.get(\"description\")}"
+msgstr ""
+
+#: ipagui/templates/usershow.kid:td
+msgid "${user.get(\"employeetype\")}"
+msgstr ""
+
+#: ipagui/templates/usershow.kid:a
+msgid "${user_manager.givenname} ${user_manager.sn}"
+msgstr ""
+
+#: ipagui/templates/usershow.kid:a
+msgid "${user_secretary.givenname} ${user_secretary.sn}"
+msgstr ""
+
+#: ipagui/templates/usershow.kid:td
+msgid "${user.get(\"carlicense\")}"
+msgstr ""
+
+#: ipagui/templates/usershow.kid:a
+msgid "${user.get('labeleduri')}"
+msgstr ""
+
+#: ipagui/templates/usershow.kid:td
+msgid "${user.get(custom_field.name)}"
+msgstr ""
+
+#: ipagui/templates/usershow.kid:h2
+msgid "Direct Reports"
+msgstr ""
+
+#: ipagui/templates/usershow.kid:a
+msgid "${report.givenname} ${report.sn}"
+msgstr ""
+
+#: ipagui/templates/usershow.kid:h2
+msgid "Groups"
+msgstr ""
+
+#: ipagui/templates/ipapolicyshow.kid:title
+msgid "Manage IPA Policy"
+msgstr ""
+
+#: ipagui/templates/ipapolicyshow.kid:td
+msgid "${ipapolicy.get(\"ipasearchtimelimit\")}"
+msgstr ""
+
+#: ipagui/templates/ipapolicyshow.kid:td
+msgid "${ipapolicy.get(\"ipasearchrecordslimit\")}"
+msgstr ""
+
+#: ipagui/templates/ipapolicyshow.kid:td
+msgid "${ipapolicy.get(\"ipausersearchfields\")}"
+msgstr ""
+
+#: ipagui/templates/ipapolicyshow.kid:td
+msgid "${ipapolicy.get(\"ipagroupsearchfields\")}"
+msgstr ""
+
+#: ipagui/templates/ipapolicyshow.kid:td
+msgid "${ipapolicy.get(\"ipapwdexpadvnotify\")}"
+msgstr ""
+
+#: ipagui/templates/ipapolicyshow.kid:td
+msgid "${password.get(\"krbminpwdlife\")}"
+msgstr ""
+
+#: ipagui/templates/ipapolicyshow.kid:td
+msgid "${password.get(\"krbmaxpwdlife\")}"
+msgstr ""
+
+#: ipagui/templates/ipapolicyshow.kid:td
+msgid "${password.get(\"krbpwdmindiffchars\")}"
+msgstr ""
+
+#: ipagui/templates/ipapolicyshow.kid:td
+msgid "${password.get(\"krbpwdminlength\")}"
+msgstr ""
+
+#: ipagui/templates/ipapolicyshow.kid:td
+msgid "${password.get(\"krbpwdhistorylength\")}"
+msgstr ""
+
+#: ipagui/templates/ipapolicyshow.kid:td
+msgid "${ipapolicy.get(\"ipamaxusernamelength\")}"
+msgstr ""
+
+#: ipagui/templates/ipapolicyshow.kid:td
+msgid "${ipapolicy.get(\"ipahomesrootdir\")}"
+msgstr ""
+
+#: ipagui/templates/ipapolicyshow.kid:td
+msgid "${ipapolicy.get(\"ipadefaultloginshell\")}"
+msgstr ""
+
+#: ipagui/templates/ipapolicyshow.kid:td
+msgid "${ipapolicy.get(\"ipadefaultprimarygroup\")}"
+msgstr ""
+
+#: ipagui/templates/ipapolicyshow.kid:td
+msgid "${ipapolicy.get(\"ipadefaultemaildomain\")}"
+msgstr ""
+
+#: ipagui/templates/loginfailed.kid:title
+msgid "Permission Denied"
+msgstr ""
+
+#: ipagui/templates/loginfailed.kid:p
+msgid "You do not have permission to access this page."
+msgstr ""
+
+#: ipagui/templates/not_found.kid:title
+msgid "Page Not Found"
+msgstr ""
+
+#: ipagui/templates/not_found.kid:div
+msgid "${message}"
+msgstr ""
+
+#: ipagui/templates/groupedit.kid:title
+msgid "Edit Group"
+msgstr ""
+
+#: ipagui/templates/groupedit.kid:span
+msgid "edit protected fields"
+msgstr ""
+
+#: ipagui/templates/groupnew.kid:title
+msgid "Add Group"
+msgstr ""
+
+#: ipagui/templates/groupshow.kid:title
+msgid "View Group"
+msgstr ""
+
+#: ipagui/templates/groupshow.kid:h2
+msgid "Group Details"
+msgstr ""
+
+#: ipagui/templates/groupshow.kid:td
+msgid "${group.get(\"cn\")}"
+msgstr ""
+
+#: ipagui/templates/groupshow.kid:td
+msgid "${group.get(\"description\")}"
+msgstr ""
+
+#: ipagui/templates/groupshow.kid:td
+msgid "${group.get(\"gidnumber\")}"
+msgstr ""
+
+#: ipagui/templates/groupshow.kid:td
+msgid "${userhelper.account_status_display(group.get(\"nsAccountLock\"))}"
+msgstr ""
+
+#: ipagui/templates/groupshow.kid:h2
+msgid "Group Members"
+msgstr ""
+
+#: ipagui/templates/groupshow.kid:a
+msgid "${member_cn}"
+msgstr ""
+
+#: ipagui/templates/unhandled_exception.kid:title
+msgid "Error"
+msgstr ""
+
+#: ipagui/templates/unhandled_exception.kid:h1
+msgid "An unexpected error occured"
+msgstr ""
+
+#: ipagui/templates/unhandled_exception.kid:b
+msgid "Message:"
+msgstr ""
+
+#: ipagui/templates/unhandled_exception.kid:b
+msgid "HTTP Error Message:"
+msgstr ""
+
+#: ipagui/templates/unhandled_exception.kid:pre
+msgid "${error_msg}"
+msgstr ""
+
+#: ipagui/templates/unhandled_exception.kid:b
+msgid "Stack Trace:"
+msgstr ""
+
+#: ipagui/templates/unhandled_exception.kid:pre
+msgid "${details}"
+msgstr ""
+
+#: ipagui/templates/ipapolicyedit.kid:title
+msgid "Edit IPA Policy"
+msgstr ""
+
+#: ipagui/templates/principalnew.kid:title
+msgid "Add Service Principal"
+msgstr ""
+
+#: ipagui/templates/useredit.kid:title
+msgid "Edit User"
+msgstr ""
+
+#: ipagui/templates/userlist.kid:title
+msgid "Find Users"
+msgstr ""
+
+#: ipagui/templates/userlist.kid:script
+msgid "document.getElementById(\"uid\").focus();"
+msgstr ""
+
+#: ipagui/templates/userlist.kid:h2
+msgid "${len(users)} results returned:"
+msgstr ""
+
+#: ipagui/templates/userlist.kid:th
+msgid "User"
+msgstr ""
+
+#: ipagui/templates/userlist.kid:th
+msgid "Phone"
+msgstr ""
+
+#: ipagui/templates/userlist.kid:th
+msgid "Unit"
+msgstr ""
+
+#: ipagui/templates/userlist.kid:th
+msgid "Job Title"
+msgstr ""
+
+#: ipagui/templates/userlist.kid:a
+msgid "${user.givenName} ${user.sn}"
+msgstr ""
+
+#: ipagui/templates/userlist.kid:td
+msgid "${user.telephoneNumber}"
+msgstr ""
+
+#: ipagui/templates/userlist.kid:td
+msgid "${user.ou}"
+msgstr ""
+
+#: ipagui/templates/userlist.kid:td
+msgid "${user.title}"
+msgstr ""
+
+#: ipagui/templates/userlist.kid:p
+msgid "No results found for \"${uid}\""
+msgstr ""
+
+#: ipagui/templates/userlist.kid:p
+msgid ""
+"Search automatically looks across multiple fields. If you want to find\n"
+" Joe in Finance, try typing \"joe finance\" into the search box."
+msgstr ""
+
+#: ipagui/templates/userselectsearch.kid:div
+msgid "${user_name} ${user_descr}"
+msgstr ""
+
+#: ipagui/templates/delegateform.kid:h2
+msgid "Delegation Details"
+msgstr ""
+
+#: ipagui/templates/delegateform.kid:span
+msgid "${value_for(delegate_fields.source_group_cn)}"
+msgstr ""
+
+#: ipagui/templates/delegateform.kid:span
+msgid "${value_for(delegate_fields.dest_group_cn)}"
+msgstr ""
+
+#: ipagui/templates/principalnewform.kid:h2
+msgid "Service Principal Details"
+msgstr ""
+
+#: ipagui/templates/groupnewform.kid:h2
+msgid "Add Members"
+msgstr ""
+
+#: ipagui/templates/grouplist.kid:title
+msgid "Find Groups"
+msgstr ""
+
+#: ipagui/templates/grouplist.kid:script
+msgid "document.getElementById(\"criteria\").focus();"
+msgstr ""
+
+#: ipagui/templates/grouplist.kid:th
+msgid "${fields.cn.label}"
+msgstr ""
+
+#: ipagui/templates/grouplist.kid:th
+msgid "${fields.description.label}"
+msgstr ""
+
+#: ipagui/templates/grouplist.kid:td
+msgid "${group.description}"
+msgstr ""
+
+#: ipagui/templates/dynamiceditsearch.kid:a
+msgid "add"
+msgstr ""
+
+#: ipagui/templates/master.kid:title
+msgid "Your title goes here"
+msgstr ""
+
+#: ipagui/templates/master.kid:style
+msgid ""
+"@import \"${tg.url('/static/css/style_platform.css')}\";\n"
+" @import \"${tg.url('/static/css/style_platform-objects.css')}\";\n"
+" @import \"${tg.url('/static/css/style_freeipa.css')}\";"
+msgstr ""
+
+#: ipagui/templates/master.kid:a
+msgid "Free IPA"
+msgstr ""
+
+#: ipagui/templates/master.kid:option
+msgid "Users"
+msgstr ""
+
+#: ipagui/templates/master.kid:script
+msgid ""
+"function clearsearch() {\n"
+" topsearchbox = document.getElementById('topsearchbox');\n"
+" topsearchbox.onfocus = null;\n"
+" topsearchbox.value = \"\";\n"
+" }"
+msgstr ""
+
+#: ipagui/templates/master.kid:span
+msgid "Kerberos login failed."
+msgstr ""
+
+#: ipagui/templates/master.kid:span
+msgid "Logged in as: ${tg.identity.user.display_name}"
+msgstr ""
+
+#: ipagui/templates/master.kid:h2
+msgid "Tasks"
+msgstr ""
+
+#: ipagui/templates/master.kid:a
+msgid "Find Service Principal"
+msgstr ""
+
+#: ipagui/templates/master.kid:a
+msgid "Self Service"
+msgstr ""
+
diff --git a/ipa-server/ipa-gui/sample-prod.cfg b/ipa-server/ipa-gui/sample-prod.cfg
new file mode 100644
index 00000000..f8416b93
--- /dev/null
+++ b/ipa-server/ipa-gui/sample-prod.cfg
@@ -0,0 +1,77 @@
+[global]
+# DATABASE
+
+# no database for ipagui since everything is stored in LDAP
+
+# IDENTITY
+
+# Our our sqlobject-derived proxy provider
+identity.provider='proxyprovider'
+
+# the first thing checked on any request. We want to short-circuit this
+# as early as possible
+identity.source = 'visit'
+
+# Turn on identity and visit (visit is required for identity)
+identity.on=True
+identity.failure_url="/loginfailed"
+visit.on=True
+visit.manager='proxyvisit'
+
+# SERVER
+
+server.environment="production"
+autoreload.package="ipagui"
+autoreload.on = False
+
+# Sets the number of threads the server uses
+server.thread_pool = 10
+
+# if this is part of a larger site, you can set the path
+# to the TurboGears instance here
+# server.webpath=""
+
+# Set to True if you are deploying your App behind a proxy
+# e.g. Apache using mod_proxy
+# base_url_filter.on = False
+
+# Set to True if your proxy adds the x_forwarded_host header
+# base_url_filter.use_x_forwarded_host = True
+
+# If your proxy does not add the x_forwarded_host header, set
+# the following to the *public* host url.
+# (Note: This will be overridden by the use_x_forwarded_host option
+# if it is set to True and the proxy adds the header correctly.
+# base_url_filter.base_url = "http://www.example.com"
+
+# Set to True if you'd like to abort execution if a controller gets an
+# unexpected parameter. False by default
+# tg.strict_parameters = False
+
+# LOGGING
+# Logging configuration generally follows the style of the standard
+# Python logging module configuration. Note that when specifying
+# log format messages, you need to use *() for formatting variables.
+# Deployment independent log configuration is in ipagui/config/log.cfg
+[logging]
+
+[[handlers]]
+
+[[[access_out]]]
+# set the filename as the first argument below
+args="('ipagui',)"
+class='FileHandler'
+level='INFO'
+formatter='datestamped'
+
+[[loggers]]
+[[[ipagui]]]
+level='ERROR'
+qualname='ipagui'
+handlers=['debug_out']
+
+[[[access]]]
+level='INFO'
+qualname='turbogears.access'
+handlers=['access_out']
+propagate=0
diff --git a/ipa-server/ipa-gui/setup.py b/ipa-server/ipa-gui/setup.py
new file mode 100644
index 00000000..c8a7d73b
--- /dev/null
+++ b/ipa-server/ipa-gui/setup.py
@@ -0,0 +1,65 @@
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+from setuptools import setup, find_packages
+from turbogears.finddata import find_package_data
+
+import os
+execfile(os.path.join("ipagui", "release.py"))
+
+setup(
+ name="ipa-gui",
+ version=version,
+
+ # uncomment the following lines if you fill them out in release.py
+ #description=description,
+ #author=author,
+ #author_email=email,
+ #url=url,
+ #download_url=download_url,
+ #license=license,
+
+ install_requires = [
+ "TurboGears >= 1.0.2.2",
+ ],
+ zip_safe=False,
+ packages=find_packages(),
+ package_data = find_package_data(where='ipagui',
+ package='ipagui'),
+ classifiers = [
+ 'Development Status :: 3 - Alpha',
+ 'Operating System :: OS Independent',
+ 'Programming Language :: Python',
+ 'Topic :: Software Development :: Libraries :: Python Modules',
+ 'Framework :: TurboGears',
+ # if this is an application that you'll distribute through
+ # the Cheeseshop, uncomment the next line
+ # 'Framework :: TurboGears :: Applications',
+
+ # if this is a package that includes widgets that you'll distribute
+ # through the Cheeseshop, uncomment the next line
+ # 'Framework :: TurboGears :: Widgets',
+ ],
+ test_suite = 'nose.collector',
+ entry_points = """
+ [turbogears.identity.provider]
+ proxyprovider = ipagui.proxyprovider:ProxyIdentityProvider
+ [turbogears.visit.manager]
+ proxyvisit = ipagui.proxyvisit:ProxyVisitManager
+ """,
+ )
+
diff --git a/ipa-server/ipa-gui/start-ipagui.py b/ipa-server/ipa-gui/start-ipagui.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/ipa-server/ipa-gui/start-ipagui.py
diff --git a/ipa-server/ipa-gui/test.cfg b/ipa-server/ipa-gui/test.cfg
new file mode 100644
index 00000000..df909c99
--- /dev/null
+++ b/ipa-server/ipa-gui/test.cfg
@@ -0,0 +1,4 @@
+# You can place test-specific configuration options here (like test db uri, etc)
+
+sqlobject.dburi = "sqlite:///:memory:"
+
diff --git a/ipa-server/ipa-install/Makefile.am b/ipa-server/ipa-install/Makefile.am
new file mode 100644
index 00000000..3f566175
--- /dev/null
+++ b/ipa-server/ipa-install/Makefile.am
@@ -0,0 +1,24 @@
+NULL =
+
+SUBDIRS = \
+ share \
+ updates \
+ $(NULL)
+
+sbin_SCRIPTS = \
+ ipa-server-install \
+ ipa-replica-install \
+ ipa-replica-prepare \
+ ipa-replica-manage \
+ ipa-server-certinstall \
+ ipactl \
+ $(NULL)
+
+EXTRA_DIST = \
+ README \
+ $(sbin_SCRIPTS) \
+ $(NULL)
+
+MAINTAINERCLEANFILES = \
+ *~ \
+ Makefile.in
diff --git a/ipa-server/ipa-install/README b/ipa-server/ipa-install/README
new file mode 100644
index 00000000..a52cede0
--- /dev/null
+++ b/ipa-server/ipa-install/README
@@ -0,0 +1,67 @@
+
+Required packages:
+
+krb5-server
+fedora-ds-base
+fedora-ds-base-devel
+openldap-clients
+openldap-devel
+krb5-server-ldap
+cyrus-sasl-gssapi
+httpd
+mod_auth_kerb
+ntp
+openssl-devel
+nspr-devel
+nss-devel
+mozldap-devel
+mod_python
+gcc
+python-ldap
+TurboGears
+python-kerberos
+python-krbV
+python-tgexpandingformwidget
+python-pyasn1
+
+Installation example:
+
+TEMPORARY: until bug https://bugzilla.redhat.com/show_bug.cgi?id=248169 is
+ fixed.
+
+Please apply the fedora-ds.init.patch in freeipa/ipa-server/ipa-install/share/
+to patch your init scripts before running ipa-server-install. This tells
+FDS where to find its kerberos keytab.
+
+Things done as root are denoted by #. Things done as a unix user are denoted
+by %.
+
+# cd freeipa
+# patch -p0 < ipa-server/ipa-install/share/fedora-ds.init.patch
+
+Now to do the installation.
+
+# cd freeipa
+# make install
+
+To start an interactive installation use:
+# /usr/sbin/ipa-server-install
+
+For more verbose output add the -d flag run the command with -h to see all options
+
+You have a basic working system with one super administrator (named admin).
+
+To create another administrative user:
+
+% kinit admin@FREEIPA.ORG
+% /usr/sbin/ipa-adduser -f Test -l User test
+% ldappasswd -Y GSSAPI -h localhost -s password uid=test,cn=users,cn=accounts,dc=freeipa,dc=org
+% /usr/sbin/ipa-groupmod -a test admins
+
+An admin user is just a regular user in the group admin.
+
+Now you can destroy the old ticket and log in as test:
+
+% kdestroy
+% kinit test@FREEIPA.ORG
+% /usr/sbin/ipa-finduser test
diff --git a/ipa-server/ipa-install/ipa-replica-install b/ipa-server/ipa-install/ipa-replica-install
new file mode 100644
index 00000000..c2704be0
--- /dev/null
+++ b/ipa-server/ipa-install/ipa-replica-install
@@ -0,0 +1,312 @@
+#! /usr/bin/python -E
+# Authors: Karl MacMillan <kmacmillan@mentalrootkit.com>
+#
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+import sys
+
+import tempfile, os, pwd, traceback, logging, shutil
+from ConfigParser import SafeConfigParser
+import ldap
+
+from ipa import ipautil
+
+from ipaserver import dsinstance, replication, installutils, krbinstance, service
+from ipaserver import httpinstance, ntpinstance, certs, ipaldap
+from ipa import version
+
+CACERT="/usr/share/ipa/html/ca.crt"
+
+class ReplicaConfig:
+ def __init__(self):
+ self.realm_name = ""
+ self.domain_name = ""
+ self.master_host_name = ""
+ self.dirman_password = ""
+ self.ds_user = ""
+ self.host_name = ""
+ self.repl_password = ""
+ self.dir = ""
+
+def parse_options():
+ from optparse import OptionParser
+ parser = OptionParser(version=version.VERSION)
+ parser.add_option("-N", "--no-ntp", dest="conf_ntp", action="store_false",
+ help="do not configure ntp", default=True)
+ parser.add_option("-d", "--debug", dest="debug", action="store_true",
+ default=False, help="gather extra debugging information")
+ parser.add_option("-p", "--password", dest="password",
+ help="Directory Manager (existing master) password")
+
+ options, args = parser.parse_args()
+
+ if len(args) != 1:
+ parser.error("you must provide a file generated by ipa-replica-prepare")
+
+ return options, args[0]
+
+def get_dirman_password():
+ return installutils.read_password("Directory Manager (existing master)", confirm=False, validate=False)
+
+def expand_info(filename, password):
+ top_dir = tempfile.mkdtemp("ipa")
+ tarfile = top_dir+"/files.tar"
+ dir = top_dir + "/realm_info"
+ ipautil.decrypt_file(filename, tarfile, password, top_dir)
+ ipautil.run(["tar", "xf", tarfile, "-C", top_dir])
+ os.remove(tarfile)
+
+ return top_dir, dir
+
+def read_info(dir, rconfig):
+ filename = dir + "/realm_info"
+ fd = open(filename)
+ config = SafeConfigParser()
+ config.readfp(fd)
+
+ rconfig.realm_name = config.get("realm", "realm_name")
+ rconfig.master_host_name = config.get("realm", "master_host_name")
+ rconfig.ds_user = config.get("realm", "ds_user")
+ rconfig.domain_name = config.get("realm", "domain_name")
+ rconfig.host_name = config.get("realm", "destination_host")
+
+def get_host_name():
+ hostname = installutils.get_fqdn()
+ try:
+ installutils.verify_fqdn(hostname)
+ except RuntimeError, e:
+ logging.error(str(e))
+ sys.exit(1)
+
+ return hostname
+
+def set_owner(config, dir):
+ pw = pwd.getpwnam(config.ds_user)
+ os.chown(dir, pw.pw_uid, pw.pw_gid)
+
+def install_ds(config):
+ dsinstance.check_existing_installation()
+ dsinstance.check_ports()
+
+ # if we have a pkcs12 file, create the cert db from
+ # that. Otherwise the ds setup will create the CA
+ # cert
+ pkcs12_info = None
+ if ipautil.file_exists(config.dir + "/dscert.p12"):
+ pkcs12_info = (config.dir + "/dscert.p12",
+ config.dir + "/dirsrv_pin.txt")
+
+ ds = dsinstance.DsInstance()
+ ds.create_instance(config.ds_user, config.realm_name, config.host_name, config.domain_name, config.dirman_password, pkcs12_info)
+
+ return ds
+
+def install_krb(config):
+ krb = krbinstance.KrbInstance()
+ ldappwd_filename = config.dir + "/ldappwd"
+ kpasswd_filename = config.dir + "/kpasswd.keytab"
+ krb.create_replica(config.ds_user, config.realm_name, config.host_name,
+ config.domain_name, config.dirman_password,
+ ldappwd_filename, kpasswd_filename)
+
+def install_ca_cert(config):
+ if ipautil.file_exists(config.dir + "/ca.crt"):
+ try:
+ shutil.copy(config.dir + "/ca.crt", CACERT)
+ os.chmod(CACERT, 0444)
+ except Exception, e:
+ print "error copying files: " + str(e)
+ sys.exit(1)
+
+def install_http(config):
+ # if we have a pkcs12 file, create the cert db from
+ # that. Otherwise the ds setup will create the CA
+ # cert
+ pkcs12_info = None
+ if ipautil.file_exists(config.dir + "/httpcert.p12"):
+ pkcs12_info = (config.dir + "/httpcert.p12",
+ config.dir + "/http_pin.txt")
+
+ http = httpinstance.HTTPInstance()
+ http.create_instance(config.realm_name, config.host_name, config.domain_name, False, pkcs12_info)
+
+ # Now copy the autoconfiguration files
+ if ipautil.file_exists(config.dir + "/preferences.html"):
+ try:
+ shutil.copy(config.dir + "/preferences.html", "/usr/share/ipa/html/preferences.html")
+ shutil.copy(config.dir + "/configure.jar", "/usr/share/ipa/html/configure.jar")
+ except Exception, e:
+ print "error copying files: " + str(e)
+ sys.exit(1)
+
+def check_dirsrv():
+ serverids = dsinstance.check_existing_installation()
+ if serverids:
+ print ""
+ print "An existing Directory Server has been detected."
+ if not ipautil.user_input("Do you wish to remove it and create a new one?", False):
+ print ""
+ print "Only a single Directory Server instance is allowed on an IPA"
+ print "server, the one used by IPA itself."
+ sys.exit(1)
+
+ try:
+ service.stop("dirsrv")
+ except:
+ pass
+
+ for serverid in serverids:
+ dsinstance.erase_ds_instance_data(serverid)
+
+ (ds_unsecure, ds_secure) = dsinstance.check_ports()
+ if not ds_unsecure or not ds_secure:
+ print "IPA requires ports 389 and 636 for the Directory Server."
+ print "These are currently in use:"
+ if not ds_unsecure:
+ print "\t389"
+ if not ds_secure:
+ print "\t636"
+ sys.exit(1)
+
+def main():
+ options, filename = parse_options()
+ installutils.standard_logging_setup("/var/log/ipareplica-install.log", options.debug)
+
+ if not ipautil.file_exists(filename):
+ sys.exit("Replica file %s does not exist" % filename)
+
+ check_dirsrv()
+
+ # get the directory manager password
+ dirman_password = options.password
+ if not dirman_password:
+ try:
+ dirman_password = get_dirman_password()
+ except KeyboardInterrupt:
+ sys.exit(0)
+
+ try:
+ top_dir, dir = expand_info(filename, dirman_password)
+ except Exception, e:
+ print "ERROR: Failed to decrypt or open the replica file."
+ print "Verify you entered the correct Directory Manager password."
+ sys.exit(1)
+
+ config = ReplicaConfig()
+ read_info(dir, config)
+ config.dirman_password = dirman_password
+ host = get_host_name()
+ if config.host_name != host:
+ try:
+ print "This replica was created for '%s' but this machine is named '%s'" % (config.host_name, host)
+ if not ipautil.user_input("This may cause problems. Continue?", True):
+ sys.exit(0)
+ config.host_name = host
+ print ""
+ except KeyboardInterrupt:
+ sys.exit(0)
+ config.repl_password = ipautil.ipa_generate_password()
+ config.dir = dir
+
+ # Try out the password
+ try:
+ conn = ipaldap.IPAdmin(config.master_host_name)
+ conn.do_simple_bind(bindpw=config.dirman_password)
+ conn.unbind()
+ except ldap.CONNECT_ERROR, e:
+ sys.exit("\nUnable to connect to LDAP server %s" % config.master_host_name)
+ except ldap.SERVER_DOWN, e:
+ sys.exit("\nUnable to connect to LDAP server %s" % config.master_host_name)
+ except ldap.INVALID_CREDENTIALS, e :
+ sys.exit("\nThe password provided is incorrect for LDAP server %s" % config.master_host_name)
+
+ # Configure ntpd
+ if options.conf_ntp:
+ ntp = ntpinstance.NTPInstance()
+ ntp.create_instance()
+
+ # Configure dirsrv
+ ds = install_ds(config)
+
+ # Install CA cert so that we can do SSL connections with ldap
+ install_ca_cert(config)
+
+ try:
+ repl = replication.ReplicationManager(config.host_name, config.dirman_password)
+ ret = repl.setup_replication(config.master_host_name, config.realm_name)
+ except Exception, e:
+ logging.debug("Connection error: %s" % e)
+ raise RuntimeError("Unable to connect to LDAP server %s." % config.host_name)
+ if ret != 0:
+ raise RuntimeError("Failed to start replication")
+
+ install_krb(config)
+ install_http(config)
+
+ # Create the config file
+ fd = open("/etc/ipa/ipa.conf", "w")
+ fd.write("[defaults]\n")
+ fd.write("server=" + config.host_name + "\n")
+ fd.write("realm=" + config.realm_name + "\n")
+ fd.write("domain=" + config.domain_name + "\n")
+ fd.close()
+
+ # Create a Web Gui instance
+ webgui = httpinstance.WebGuiInstance()
+ webgui.create_instance()
+
+ # Apply any LDAP updates. Needs to be done after the replica is synced-up
+ service.print_msg("Applying LDAP updates")
+ ds.apply_updates()
+
+ service.restart("dirsrv")
+ service.restart("krb5kdc")
+
+ # Call client install script
+ try:
+ ipautil.run(["/usr/sbin/ipa-client-install", "--on-master", "--unattended", "--domain", config.domain_name, "--server", config.host_name, "--realm", config.realm_name])
+ except Exception, e:
+ print "Configuration of client side components failed!"
+ print "ipa-client-install returned: " + str(e)
+ raise RuntimeError("Failed to configure the client")
+
+ ds.init_memberof()
+
+try:
+ if not os.geteuid()==0:
+ sys.exit("\nYou must be root to run this script.\n")
+
+ main()
+ sys.exit(0)
+except SystemExit, e:
+ sys.exit(e)
+except Exception, e:
+ print "creation of replica failed: %s" % str(e)
+ message = str(e)
+ for str in traceback.format_tb(sys.exc_info()[2]):
+ message = message + "\n" + str
+ logging.debug(message)
+except KeyboardInterrupt:
+ print "Installation cancelled."
+
+print ""
+print "Your system may be partly configured."
+print "Run /usr/sbin/ipa-server-install --uninstall to clean up."
+
+# the only way to get here is on error or ^C
+sys.exit(1)
diff --git a/ipa-server/ipa-install/ipa-replica-manage b/ipa-server/ipa-install/ipa-replica-manage
new file mode 100755
index 00000000..db8c32d5
--- /dev/null
+++ b/ipa-server/ipa-install/ipa-replica-manage
@@ -0,0 +1,218 @@
+#! /usr/bin/python -E
+# Authors: Karl MacMillan <kmacmillan@mentalrootkit.com>
+#
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+import sys
+
+import getpass, ldap, re, krbV
+import traceback, logging
+
+from ipa import ipautil
+from ipaserver import replication, ipaldap, dsinstance, installutils
+from ipa import version
+
+def parse_options():
+ from optparse import OptionParser
+
+ parser = OptionParser(version=version.VERSION)
+ parser.add_option("-H", "--host", dest="host", help="starting host")
+ parser.add_option("-p", "--password", dest="dirman_passwd", help="Directory Manager password")
+ parser.add_option("-v", "--verbose", dest="verbose", action="store_true", default=False,
+ help="provide additional information")
+ parser.add_option("--port", type="int", dest="port",
+ help="port number of other server")
+ parser.add_option("--binddn", dest="binddn",
+ help="Bind DN to use with remote server")
+ parser.add_option("--bindpw", dest="bindpw",
+ help="Password for Bind DN to use with remote server")
+ parser.add_option("--winsync", dest="winsync", action="store_true", default=False,
+ help="This is a Windows Sync Agreement")
+ parser.add_option("--cacert", dest="cacert",
+ help="Full path and filename of CA certificate to use with TLS/SSL to the remote server")
+ parser.add_option("--win-subtree", dest="win_subtree",
+ help="DN of Windows subtree containing the users you want to sync (default cn=Users,<domain suffix)")
+ parser.add_option("--passsync", dest="passsync",
+ help="Password for the Windows PassSync user")
+
+ options, args = parser.parse_args()
+
+ if not len(args) or not ("list" in args[0] or "add" in args[0] or "del" in args[0] or "init" in args[0] or "synch" in args[0]):
+ parser.error("must provide a command [list | add | del | init | synch]")
+
+ # set log level
+ if options.verbose:
+ # if verbose, output events at INFO level if not already
+ mylogger = logging.getLogger()
+ if mylogger.getEffectiveLevel() > logging.INFO:
+ mylogger.setLevel(logging.INFO)
+ # else user has already configured logging externally lower
+ return options, args
+
+def get_realm_name():
+ c = krbV.default_context()
+ return c.default_realm
+
+def get_suffix():
+ suffix = ipaldap.IPAdmin.normalizeDN(dsinstance.realm_to_suffix(get_realm_name()))
+ return suffix
+
+def get_host_name():
+ hostname = installutils.get_fqdn()
+ try:
+ installutils.verify_fqdn(hostname)
+ except RuntimeError, e:
+ logging.error(str(e))
+ sys.exit(1)
+
+ return hostname
+
+def list_masters(replman, verbose):
+ dns = replman.find_replication_dns(replman.conn)
+
+ for dn in dns:
+ entry = replman.conn.search_s(dn, ldap.SCOPE_SUBTREE)[0]
+ print entry.getValue('nsds5replicahost')
+
+ if verbose:
+ print " last init status: %s" % entry.nsds5replicalastinitstatus
+ print " last init ended: %s" % str(ipautil.parse_generalized_time(entry.nsds5replicalastinitend))
+ print " last update status: %s" % entry.nsds5replicalastupdatestatus
+ print " last update ended: %s" % str(ipautil.parse_generalized_time(entry.nsds5replicalastupdateend))
+
+def del_master(replman, hostname):
+ try:
+ t = replman.get_agreement_type(hostname)
+ except ldap.NO_SUCH_OBJECT:
+ print "No replication agreement found for %s" % hostname
+
+ if t == replication.IPA_REPLICA:
+ dirman_passwd = getpass.getpass("Directory Manager password (%s): " % hostname)
+ other_replman = replication.ReplicationManager(hostname, dirman_passwd)
+ other_replman.suffix = get_suffix()
+ other_replman.delete_agreement(replman.conn.host)
+
+ replman.delete_agreement(hostname)
+
+def add_master(replman, hostname, options):
+ other_args = {}
+ if options.port:
+ other_args['port'] = options.port
+ if options.binddn:
+ other_args['binddn'] = options.binddn
+ if options.bindpw:
+ other_args['bindpw'] = options.bindpw
+ if options.cacert:
+ other_args['cacert'] = options.cacert
+ if options.win_subtree:
+ other_args['win_subtree'] = options.win_subtree
+ if options.passsync:
+ other_args['passsync'] = options.passsync
+ if options.winsync:
+ other_args['winsync'] = True
+ if not options.binddn or not options.bindpw or not options.cacert or not options.passsync:
+ logging.error("The arguments --binddn, --bindpw, --passsync and --cacert are required to create a winsync agreement")
+ sys.exit(1)
+ if options.cacert:
+ # have to install the given CA cert before doing anything else
+ ds = dsinstance.DsInstance(realm_name = get_realm_name(),
+ dm_password = replman.dirman_passwd)
+ if not ds.add_ca_cert(options.cacert):
+ logging.error("Could not load the required CA certificate file [%s]" %
+ options.cacert)
+ sys.exit(1)
+ else:
+ logging.info("Added CA certificate %s to certificate database for %s" %
+ (options.cacert, replman.hostname))
+ # have to reconnect replman connection since the directory server was restarted
+ replman = replication.ReplicationManager(replman.hostname, replman.dirman_passwd)
+ logging.info("Restarted directory server " + replman.hostname)
+ replman.setup_replication(hostname, get_realm_name(), **other_args)
+ logging.info("Added agreement for other host " + hostname)
+
+def init_master(replman, dirman_passwd, hostname):
+ filter = "(&(nsDS5ReplicaHost=%s)(|(objectclass=nsDSWindowsReplicationAgreement)(objectclass=nsds5ReplicationAgreement)))" % hostname
+ entry = replman.conn.search_s("cn=config", ldap.SCOPE_SUBTREE, filter)
+ if len(entry) == 0:
+ logging.error("Unable to find replication agreement for %s" % hostname)
+ sys.exit(1)
+ if len(entry) > 1:
+ logging.error("Found multiple agreements for %s. Only initializing the first one returned: %s" % (hostname, entry[0].dn))
+ replman.initialize_replication(entry[0].dn, replman.conn)
+ ds = dsinstance.DsInstance(realm_name = get_realm_name(), dm_password = dirman_passwd)
+ ds.init_memberof()
+
+def synch_master(replman, hostname):
+ filter = "(&(nsDS5ReplicaHost=%s)(|(objectclass=nsDSWindowsReplicationAgreement)(objectclass=nsds5ReplicationAgreement)))" % hostname
+ entry = replman.conn.search_s("cn=config", ldap.SCOPE_SUBTREE, filter)
+ if len(entry) == 0:
+ logging.error("Unable to find replication agreement for %s" % hostname)
+ sys.exit(1)
+ if len(entry) > 1:
+ logging.error("Found multiple agreements for %s. Only initializing the first one returned: %s" % (hostname, entry[0].dn))
+ replman.force_synch(entry[0].dn, entry[0].nsds5replicaupdateschedule, replman.conn)
+
+def main():
+ options, args = parse_options()
+
+ if options.dirman_passwd:
+ dirman_passwd = options.dirman_passwd
+ else:
+ dirman_passwd = getpass.getpass("Directory Manager password: ")
+
+ if options.host:
+ host = options.host
+ else:
+ host = get_host_name()
+
+ r = replication.ReplicationManager(host, dirman_passwd)
+ r.suffix = get_suffix()
+
+ if args[0] == "list":
+ list_masters(r, options.verbose)
+ elif args[0] == "del":
+ if len(args) != 2:
+ print "must provide hostname of master to delete"
+ sys.exit(1)
+ del_master(r, args[1])
+ elif args[0] == "add":
+ if len(args) != 2:
+ print "must provide hostname of master to add"
+ sys.exit(1)
+ add_master(r, args[1], options)
+ elif args[0] == "init":
+ if len(args) != 2:
+ print "hostname of master to initialize is required."
+ sys.exit(1)
+ init_master(r, dirman_passwd, args[1])
+ elif args[0] == "synch":
+ if len(args) != 2:
+ print "must provide hostname of supplier to synchronize with"
+ sys.exit(1)
+ synch_master(r, args[1])
+
+try:
+ main()
+except KeyboardInterrupt:
+ sys.exit(1)
+except SystemExit, e:
+ sys.exit(e)
+except ldap.INVALID_CREDENTIALS:
+ print "Invalid password"
+ sys.exit(1)
+except Exception, e:
+ print "unexpected error: %s" % str(e)
diff --git a/ipa-server/ipa-install/ipa-replica-prepare b/ipa-server/ipa-install/ipa-replica-prepare
new file mode 100644
index 00000000..eb962b4c
--- /dev/null
+++ b/ipa-server/ipa-install/ipa-replica-prepare
@@ -0,0 +1,294 @@
+#! /usr/bin/python -E
+# Authors: Karl MacMillan <kmacmillan@mentalrootkit.com>
+#
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+import sys
+
+import logging, tempfile, shutil, os, pwd
+import traceback
+from ConfigParser import SafeConfigParser
+import krbV
+from optparse import OptionParser
+
+import ipa.config
+from ipa import ipautil
+from ipaserver import dsinstance, installutils, certs, ipaldap
+from ipa import version
+import ldap
+
+def parse_options():
+ usage = "%prog [options] FQDN (e.g. replica.example.com)"
+ parser = OptionParser(usage=usage, version=version.VERSION)
+
+ parser.add_option("--dirsrv_pkcs12", dest="dirsrv_pkcs12",
+ help="install certificate for the directory server")
+ parser.add_option("--http_pkcs12", dest="http_pkcs12",
+ help="install certificate for the http server")
+ parser.add_option("--dirsrv_pin", dest="dirsrv_pin",
+ help="PIN for the Directory Server PKCS#12 file")
+ parser.add_option("--http_pin", dest="http_pin",
+ help="PIN for the Apache Server PKCS#12 file")
+ parser.add_option("-p", "--password", dest="password",
+ help="Directory Manager (existing master) password")
+
+ ipa.config.add_standard_options(parser)
+ options, args = parser.parse_args()
+
+ # If any of the PKCS#12 options are selected, all are required. Create a
+ # list of the options and count it to enforce that all are required without
+ # having a huge set of it blocks.
+ pkcs12 = [options.dirsrv_pkcs12, options.http_pkcs12, options.dirsrv_pin, options.http_pin]
+ cnt = pkcs12.count(None)
+ if cnt > 0 and cnt < 4:
+ parser.error("error: All PKCS#12 options are required if any are used.")
+
+ if len(args) != 1:
+ parser.error("must provide the fully-qualified name of the replica")
+
+ ipa.config.init_config(options)
+
+ return options, args
+
+def get_host_name():
+ hostname = installutils.get_fqdn()
+ try:
+ installutils.verify_fqdn(hostname)
+ except RuntimeError, e:
+ logging.error(str(e))
+ sys.exit(1)
+
+ return hostname
+
+def get_realm_name():
+ try:
+ c = krbV.default_context()
+ return c.default_realm
+ except Exception, e:
+ return None
+
+def get_domain_name():
+ try:
+ ipa.config.init_config()
+ domain_name = ipa.config.config.get_domain()
+ except Exception, e:
+ return None
+
+ return domain_name
+
+def check_ipa_configuration(realm_name):
+ config_dir = dsinstance.config_dirname(dsinstance.realm_to_serverid(realm_name))
+ if not ipautil.dir_exists(config_dir):
+ logging.error("could not find directory instance: %s" % config_dir)
+ sys.exit(1)
+
+def export_certdb(realm_name, ds_dir, dir, passwd_fname, fname, subject):
+ """realm is the kerberos realm for the IPA server.
+ ds_dir is the location of the master DS we are creating a replica for.
+ dir is the location of the files for the replica we are creating.
+ passwd_fname is the file containing the PKCS#12 password
+ fname is the filename of the PKCS#12 file for this cert (minus the .p12).
+ subject is the subject of the certificate we are creating
+ """
+ try:
+ ds_ca = certs.CertDB(dsinstance.config_dirname(dsinstance.realm_to_serverid(realm_name)))
+ ca = certs.CertDB(dir)
+ ca.create_from_cacert(ds_ca.cacert_fname)
+ ca.create_server_cert("Server-Cert", subject, ds_ca)
+ except Exception, e:
+ raise e
+
+ pkcs12_fname = dir + "/" + fname + ".p12"
+
+ try:
+ ca.export_pkcs12(pkcs12_fname, passwd_fname, "Server-Cert")
+ except ipautil.CalledProcessError, e:
+ print "error exporting CA certificate: " + str(e)
+ try:
+ os.unlink(pkcs12_fname)
+ os.unlink(passwd_fname)
+ except:
+ pass
+
+ os.unlink(dir + "/cert8.db")
+ os.unlink(dir + "/key3.db")
+ os.unlink(dir + "/secmod.db")
+ os.unlink(dir + "/noise.txt")
+ if ipautil.file_exists(passwd_fname + ".orig"):
+ os.unlink(passwd_fname + ".orig")
+
+def get_ds_user(ds_dir):
+ uid = os.stat(ds_dir).st_uid
+ user = pwd.getpwuid(uid)[0]
+
+ return user
+
+def save_config(dir, realm_name, host_name, ds_user, domain_name, dest_host):
+ config = SafeConfigParser()
+ config.add_section("realm")
+ config.set("realm", "realm_name", realm_name)
+ config.set("realm", "master_host_name", host_name)
+ config.set("realm", "ds_user", ds_user)
+ config.set("realm", "domain_name", domain_name)
+ config.set("realm", "destination_host", dest_host)
+ fd = open(dir + "/realm_info", "w")
+ config.write(fd)
+
+def copy_files(realm_name, dir):
+ config_dir = dsinstance.config_dirname(dsinstance.realm_to_serverid(realm_name))
+
+ try:
+ shutil.copy("/var/kerberos/krb5kdc/ldappwd", dir + "/ldappwd")
+ shutil.copy("/var/kerberos/krb5kdc/kpasswd.keytab", dir + "/kpasswd.keytab")
+ shutil.copy("/usr/share/ipa/html/ca.crt", dir + "/ca.crt")
+ if ipautil.file_exists("/usr/share/ipa/html/preferences.html"):
+ shutil.copy("/usr/share/ipa/html/preferences.html", dir + "/preferences.html")
+ shutil.copy("/usr/share/ipa/html/configure.jar", dir + "/configure.jar")
+ except Exception, e:
+ print "error copying files: " + str(e)
+ sys.exit(1)
+
+def get_dirman_password():
+ return installutils.read_password("Directory Manager (existing master)", confirm=False, validate=False)
+
+def main():
+ options, args = parse_options()
+
+ replica_fqdn = args[0]
+
+ if not ipautil.file_exists(certs.CA_SERIALNO) and not options.dirsrv_pin:
+ sys.exit("The replica must be created on the primary IPA server.\nIf you installed IPA with your own certificates using PKCS#12 files you must provide PKCS#12 files for any replicas you create as well.")
+
+ print "Determining current realm name"
+ realm_name = get_realm_name()
+ if realm_name is None:
+ print "Unable to determine default realm"
+ sys.exit(1)
+
+ check_ipa_configuration(realm_name)
+
+ print "Getting domain name from LDAP"
+ domain_name = get_domain_name()
+ if domain_name is None:
+ print "Unable to determine LDAP default domain"
+ sys.exit(1)
+
+ host_name = get_host_name()
+ if host_name == replica_fqdn:
+ print "You can't create a replica on itself"
+ sys.exit(1)
+ ds_dir = dsinstance.config_dirname(dsinstance.realm_to_serverid(realm_name))
+ ds_user = get_ds_user(ds_dir)
+
+ # get the directory manager password
+ dirman_password = options.password
+ if not options.password:
+ try:
+ dirman_password = get_dirman_password()
+ except KeyboardInterrupt:
+ sys.exit(0)
+
+ # Try out the password
+ try:
+ conn = ipaldap.IPAdmin(host_name)
+ conn.do_simple_bind(bindpw=dirman_password)
+ conn.unbind()
+ except ldap.CONNECT_ERROR, e:
+ sys.exit("\nUnable to connect to LDAP server %s" % host_name)
+ except ldap.SERVER_DOWN, e:
+ sys.exit("\nUnable to connect to LDAP server %s" % host_name)
+ except ldap.INVALID_CREDENTIALS, e :
+ sys.exit("\nThe password provided is incorrect for LDAP server %s" % host_name)
+
+ print "Preparing replica for %s from %s" % (replica_fqdn, host_name)
+
+ top_dir = tempfile.mkdtemp("ipa")
+ dir = top_dir + "/realm_info"
+ os.mkdir(dir, 0700)
+
+ if options.dirsrv_pin:
+ passwd = options.dirsrv_pin
+ else:
+ passwd = ""
+
+ passwd_fname = dir + "/dirsrv_pin.txt"
+ fd = open(passwd_fname, "w")
+ fd.write("%s\n" % passwd)
+ fd.close()
+
+ if options.dirsrv_pkcs12:
+ print "Copying SSL certificate for the Directory Server from %s" % options.dirsrv_pkcs12
+ try:
+ shutil.copy(options.dirsrv_pkcs12, dir + "/dscert.p12")
+ except IOError, e:
+ print "Copy failed %s" % e
+ sys.exit(1)
+ else:
+ print "Creating SSL certificate for the Directory Server"
+ export_certdb(realm_name, ds_dir, dir, passwd_fname, "dscert", "cn=%s,ou=Fedora Directory Server" % replica_fqdn)
+
+ if options.http_pin:
+ passwd = options.http_pin
+ else:
+ passwd = ""
+
+ passwd_fname = dir + "/http_pin.txt"
+ fd = open(passwd_fname, "w")
+ fd.write("%s\n" % passwd)
+ fd.close()
+
+ if options.http_pkcs12:
+ print "Copying SSL certificate for the Web Server from %s" % options.http_pkcs12
+ try:
+ shutil.copy(options.http_pkcs12, dir + "/httpcert.p12")
+ except IOError, e:
+ print "Copy failed %s" % e
+ sys.exit(1)
+ else:
+ print "Creating SSL certificate for the Web Server"
+ export_certdb(realm_name, ds_dir, dir, passwd_fname, "httpcert", "cn=%s,ou=Apache Web Server" % replica_fqdn)
+ print "Copying additional files"
+ copy_files(realm_name, dir)
+ print "Finalizing configuration"
+ save_config(dir, realm_name, host_name, ds_user, domain_name, replica_fqdn)
+
+ replicafile = "/var/lib/ipa/replica-info-" + replica_fqdn
+ encfile = replicafile+".gpg"
+
+ print "Packaging replica information into %s" % encfile
+ ipautil.run(["/bin/tar", "cf", replicafile, "-C", top_dir, "realm_info"])
+ ipautil.encrypt_file(replicafile, encfile, dirman_password, top_dir);
+
+ os.remove(replicafile)
+ shutil.rmtree(dir)
+
+try:
+ if not os.geteuid()==0:
+ sys.exit("\nYou must be root to run this script.\n")
+
+ main()
+except SystemExit, e:
+ sys.exit(e)
+except Exception, e:
+ print "preparation of replica failed: %s" % str(e)
+ message = str(e)
+ for str in traceback.format_tb(sys.exc_info()[2]):
+ message = message + "\n" + str
+ logging.debug(message)
+ print message
+ sys.exit(1)
diff --git a/ipa-server/ipa-install/ipa-server-certinstall b/ipa-server/ipa-install/ipa-server-certinstall
new file mode 100644
index 00000000..a0d11856
--- /dev/null
+++ b/ipa-server/ipa-install/ipa-server-certinstall
@@ -0,0 +1,157 @@
+#! /usr/bin/python -E
+# Authors: Karl MacMillan <kmacmillan@mentalrootkit.com>
+#
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+import sys
+import os
+import pwd
+import tempfile
+
+import traceback
+
+import krbV, ldap, getpass
+
+from ipa.ipautil import user_input
+from ipaserver import certs, dsinstance, httpinstance, ipaldap, installutils
+
+def get_realm_name():
+ c = krbV.default_context()
+ return c.default_realm
+
+def parse_options():
+ from optparse import OptionParser
+ parser = OptionParser()
+
+ parser.add_option("-d", "--dirsrv", dest="dirsrv", action="store_true",
+ default=False, help="install certificate for the directory server")
+ parser.add_option("-w", "--http", dest="http", action="store_true",
+ default=False, help="install certificate for the http server")
+ parser.add_option("--dirsrv_pin", dest="dirsrv_pin",
+ help="The password of the Directory Server PKCS#12 file")
+ parser.add_option("--http_pin", dest="http_pin",
+ help="The password of the Apache Server PKCS#12 file")
+
+ options, args = parser.parse_args()
+
+ if not options.dirsrv and not options.http:
+ parser.error("you must specify dirsrv and/or http")
+ if ((options.dirsrv and not options.dirsrv_pin) or
+ (options.http and not options.http_pin)):
+ parser.error("you must provide the password for the PKCS#12 file")
+
+ if len(args) != 1:
+ parser.error("you must provide a pkcs12 filename")
+
+ return options, args[0]
+
+def set_ds_cert_name(cert_name, dm_password):
+ conn = ipaldap.IPAdmin("127.0.0.1")
+ conn.simple_bind_s("cn=directory manager", dm_password)
+
+ mod = [(ldap.MOD_REPLACE, "nsSSLPersonalitySSL", cert_name)]
+
+ conn.modify_s("cn=RSA,cn=encryption,cn=config", mod)
+
+ conn.unbind()
+
+def choose_server_cert(server_certs):
+ print "Please select the certificate to use:"
+ num = 1
+ for cert in server_certs:
+ print "%d. %s" % (num, cert[0])
+ num += 1
+
+ while 1:
+ num = user_input("Certificate number", 1)
+ print ""
+ if num < 1 or num > len(server_certs):
+ print "number out of range"
+ else:
+ break
+
+ return server_certs[num - 1]
+
+def import_cert(dirname, pkcs12_fname, pkcs12_passwd, db_password):
+ cdb = certs.CertDB(dirname)
+ cdb.create_passwd_file(db_password)
+ cdb.create_certdbs()
+ [pw_fd, pw_name] = tempfile.mkstemp()
+ os.write(pw_fd, pkcs12_passwd)
+ os.close(pw_fd)
+
+ try:
+ try:
+ cdb.import_pkcs12(pkcs12_fname, pw_name)
+ except RuntimeError, e:
+ print str(e)
+ sys.exit(1)
+ finally:
+ os.remove(pw_name)
+
+ server_certs = cdb.find_server_certs()
+ if len(server_certs) == 0:
+ print "could not find a suitable server cert in import"
+ sys.exit(1)
+ elif len(server_certs) == 1:
+ server_cert = server_certs[0]
+ else:
+ server_cert = choose_server_cert(server_certs)
+
+ cdb.trust_root_cert(server_cert[0])
+
+ return server_cert
+
+def main():
+ options, pkcs12_fname = parse_options()
+
+ try:
+ if options.dirsrv:
+ dm_password = getpass.getpass("Directory Manager password: ")
+ realm = get_realm_name()
+ dirname = dsinstance.config_dirname(dsinstance.realm_to_serverid(realm))
+ fd = open(dirname + "/pwdfile.txt")
+ passwd = fd.read()
+ fd.close()
+
+ server_cert = import_cert(dirname, pkcs12_fname, options.dirsrv_pin, passwd)
+ set_ds_cert_name(server_cert[0], dm_password)
+
+ if options.http:
+ dirname = httpinstance.NSS_DIR
+ server_cert = import_cert(dirname, pkcs12_fname, options.http_pin, "")
+ installutils.set_directive(httpinstance.NSS_CONF, 'NSSNickname', server_cert[0])
+
+ # Fix the database permissions
+ os.chmod(dirname + "/cert8.db", 0640)
+ os.chmod(dirname + "/key3.db", 0640)
+ os.chmod(dirname + "/secmod.db", 0640)
+
+ pent = pwd.getpwnam("apache")
+ os.chown(dirname + "/cert8.db", 0, pent.pw_gid )
+ os.chown(dirname + "/key3.db", 0, pent.pw_gid )
+ os.chown(dirname + "/secmod.db", 0, pent.pw_gid )
+
+ except Exception, e:
+ print "an unexpected error occurred: %s" % str(e)
+ traceback.print_exc()
+ return 1
+
+ return 0
+
+sys.exit(main())
diff --git a/ipa-server/ipa-install/ipa-server-install b/ipa-server/ipa-install/ipa-server-install
new file mode 100644
index 00000000..c9d5c5bf
--- /dev/null
+++ b/ipa-server/ipa-install/ipa-server-install
@@ -0,0 +1,622 @@
+#! /usr/bin/python -E
+# Authors: Karl MacMillan <kmacmillan@mentalrootkit.com>
+#
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+
+# requires the following packages:
+# fedora-ds-base
+# openldap-clients
+# nss-tools
+
+import sys
+import os
+import socket
+import errno
+import logging
+import pwd
+import subprocess
+import signal
+import shutil
+import glob
+import traceback
+from optparse import OptionParser
+
+import ipaserver.dsinstance
+import ipaserver.krbinstance
+import ipaserver.bindinstance
+import ipaserver.httpinstance
+import ipaserver.ntpinstance
+
+from ipaserver import service
+from ipa import version
+from ipaserver.installutils import *
+
+from ipa import sysrestore
+from ipa.ipautil import *
+
+pw_name = None
+
+def parse_options():
+ parser = OptionParser(version=version.VERSION)
+ parser.add_option("-u", "--user", dest="ds_user",
+ help="ds user")
+ parser.add_option("-r", "--realm", dest="realm_name",
+ help="realm name")
+ parser.add_option("-n", "--domain", dest="domain_name",
+ help="domain name")
+ parser.add_option("-p", "--ds-password", dest="dm_password",
+ help="admin password")
+ parser.add_option("-P", "--master-password", dest="master_password",
+ help="kerberos master password (normally autogenerated)")
+ parser.add_option("-a", "--admin-password", dest="admin_password",
+ help="admin user kerberos password")
+ parser.add_option("-d", "--debug", dest="debug", action="store_true",
+ default=False, help="print debugging information")
+ parser.add_option("--hostname", dest="host_name", help="fully qualified name of server")
+ parser.add_option("--ip-address", dest="ip_address", help="Master Server IP Address")
+ parser.add_option("--setup-bind", dest="setup_bind", action="store_true",
+ default=False, help="configure bind with our zone file")
+ parser.add_option("-U", "--unattended", dest="unattended", action="store_true",
+ default=False, help="unattended installation never prompts the user")
+ parser.add_option("", "--uninstall", dest="uninstall", action="store_true",
+ default=False, help="uninstall an existing installation")
+ parser.add_option("-N", "--no-ntp", dest="conf_ntp", action="store_false",
+ help="do not configure ntp", default=True)
+ parser.add_option("--dirsrv_pkcs12", dest="dirsrv_pkcs12",
+ help="PKCS#12 file containing the Directory Server SSL certificate")
+ parser.add_option("--http_pkcs12", dest="http_pkcs12",
+ help="PKCS#12 file containing the Apache Server SSL certificate")
+ parser.add_option("--dirsrv_pin", dest="dirsrv_pin",
+ help="The password of the Directory Server PKCS#12 file")
+ parser.add_option("--http_pin", dest="http_pin",
+ help="The password of the Apache Server PKCS#12 file")
+ parser.add_option("--no-host-dns", dest="no_host_dns", action="store_true",
+ default=False,
+ help="Do not use DNS for hostname lookup during installation")
+
+ options, args = parser.parse_args()
+
+ if options.uninstall:
+ if (options.ds_user or options.realm_name or
+ options.dm_password or options.admin_password or
+ options.master_password):
+ parser.error("error: In uninstall mode, -u, r, -p and -P options are not allowed")
+ elif options.unattended:
+ if (not options.ds_user or not options.realm_name or
+ not options.dm_password or not options.admin_password):
+ parser.error("error: In unattended mode you need to provide at least -u, -r, -p and -a options")
+
+ # If any of the PKCS#12 options are selected, all are required. Create a
+ # list of the options and count it to enforce that all are required without
+ # having a huge set of it blocks.
+ pkcs12 = [options.dirsrv_pkcs12, options.http_pkcs12, options.dirsrv_pin, options.http_pin]
+ cnt = pkcs12.count(None)
+ if cnt > 0 and cnt < 4:
+ parser.error("error: All PKCS#12 options are required if any are used.")
+
+ return options
+
+def signal_handler(signum, frame):
+ global ds
+ print "\nCleaning up..."
+ if ds:
+ print "Removing configuration for %s instance" % ds.serverid
+ ds.stop()
+ if ds.serverid:
+ ipaserver.dsinstance.erase_ds_instance_data (ds.serverid)
+ sys.exit(1)
+
+def read_host_name(host_default,no_host_dns=False):
+ host_name = ""
+
+ print "Enter the fully qualified domain name of the computer"
+ print "on which you're setting up server software. Using the form"
+ print "<hostname>.<domainname>"
+ print "Example: master.example.com."
+ print ""
+ print ""
+ if host_default == "":
+ host_default = "master.example.com"
+ while True:
+ host_name = user_input("Server host name", host_default, allow_empty = False)
+ print ""
+ try:
+ verify_fqdn(host_name,no_host_dns)
+ except Exception, e:
+ raise e
+ else:
+ break
+ return host_name
+
+def resolve_host(host_name):
+ ip = ""
+ try:
+ ip = socket.gethostbyname(host_name)
+
+ if ip == "127.0.0.1" or ip == "::1":
+ print "The hostname resolves to the localhost address (127.0.0.1/::1)"
+ print "Please change your /etc/hosts file so that the hostname"
+ print "resolves to the ip address of your network interface."
+ print "The KDC service does not listen on localhost"
+ print ""
+ print "Please fix your /etc/hosts file and restart the setup program"
+ return None
+
+ except:
+ print "Unable to lookup the IP address of the provided host"
+ return ip
+
+def verify_ip_address(ip):
+ is_ok = True
+ try:
+ socket.inet_pton(socket.AF_INET, ip)
+ except:
+ try:
+ socket.inet_pton(socket.AF_INET6, ip)
+ except:
+ print "Unable to verify IP address"
+ is_ok = False
+ return is_ok
+
+def read_ip_address(host_name):
+ while True:
+ ip = user_input("Please provide the IP address to be used for this host name", allow_empty = False)
+
+ if ip == "127.0.0.1" or ip == "::1":
+ print "The IPA Server can't use localhost as a valid IP"
+ continue
+
+ if not verify_ip_address(ip):
+ continue
+
+ print "Adding ["+ip+" "+host_name+"] to your /etc/hosts file"
+ fstore.backup_file("/etc/hosts")
+ hosts_fd = open('/etc/hosts', 'r+')
+ hosts_fd.seek(0, 2)
+ hosts_fd.write(ip+'\t'+host_name+' '+host_name.split('.')[0]+'\n')
+ hosts_fd.close()
+
+ return ip
+
+def read_ds_user():
+ print "The server must run as a specific user in a specific group."
+ print "It is strongly recommended that this user should have no privileges"
+ print "on the computer (i.e. a non-root user). The setup procedure"
+ print "will give this user/group some permissions in specific paths/files"
+ print "to perform server-specific operations."
+ print ""
+
+ ds_user = ""
+ try:
+ pwd.getpwnam('dirsrv')
+
+ print "A user account named 'dirsrv' already exists. This is the user id"
+ print "that the Directory Server will run as."
+ print ""
+ if user_input("Do you want to use the existing 'dirsrv' account?", True):
+ ds_user = "dirsrv"
+ else:
+ print ""
+ ds_user = user_input_plain("Which account name do you want to use for the DS instance?", allow_empty = False, allow_spaces = False)
+ print ""
+ except KeyError:
+ ds_user = "dirsrv"
+
+ return ds_user
+
+def read_domain_name(domain_name, unattended):
+ print "The domain name has been calculated based on the host name."
+ print ""
+ if not unattended:
+ domain_name = user_input("Please confirm the domain name", domain_name)
+ print ""
+ return domain_name
+
+def read_realm_name(domain_name, unattended):
+ print "The kerberos protocol requires a Realm name to be defined."
+ print "This is typically the domain name converted to uppercase."
+ print ""
+
+ if unattended:
+ return domain_name.upper()
+ realm_name = user_input("Please provide a realm name", domain_name.upper())
+ upper_dom = realm_name.upper()
+ if upper_dom != realm_name:
+ print "An upper-case realm name is required."
+ if not user_input("Do you want to use " + upper_dom + " as realm name?", True):
+ print ""
+ print "An upper-case realm name is required. Unable to continue."
+ sys.exit(1)
+ else:
+ realm_name = upper_dom
+ print ""
+ return realm_name
+
+
+def read_dm_password():
+ print "Certain directory server operations require an administrative user."
+ print "This user is referred to as the Directory Manager and has full access"
+ print "to the Directory for system management tasks and will be added to the"
+ print "instance of directory server created for IPA."
+ print "The password must be at least 8 characters long."
+ print ""
+ #TODO: provide the option of generating a random password
+ dm_password = read_password("Directory Manager")
+ return dm_password
+
+def read_admin_password():
+ print "The IPA server requires an administrative user, named 'admin'."
+ print "This user is a regular system account used for IPA server administration."
+ print ""
+ #TODO: provide the option of generating a random password
+ admin_password = read_password("IPA admin")
+ return admin_password
+
+def check_dirsrv(unattended):
+ serverids = ipaserver.dsinstance.check_existing_installation()
+ if serverids:
+ print ""
+ print "An existing Directory Server has been detected."
+ if unattended or not user_input("Do you wish to remove it and create a new one?", False):
+ print ""
+ print "Only a single Directory Server instance is allowed on an IPA"
+ print "server, the one used by IPA itself."
+ sys.exit(1)
+
+ try:
+ service.stop("dirsrv")
+ except:
+ pass
+
+ for serverid in serverids:
+ ipaserver.dsinstance.erase_ds_instance_data(serverid)
+
+ (ds_unsecure, ds_secure) = ipaserver.dsinstance.check_ports()
+ if not ds_unsecure or not ds_secure:
+ print "IPA requires ports 389 and 636 for the Directory Server."
+ print "These are currently in use:"
+ if not ds_unsecure:
+ print "\t389"
+ if not ds_secure:
+ print "\t636"
+ sys.exit(1)
+
+def uninstall():
+ try:
+ run(["/usr/sbin/ipa-client-install", "--on-master", "--unattended", "--uninstall"])
+ except Exception, e:
+ print "Uninstall of client side components failed!"
+ print "ipa-client-install returned: " + str(e)
+ pass
+
+ ipaserver.ntpinstance.NTPInstance(fstore).uninstall()
+ ipaserver.bindinstance.BindInstance(fstore).uninstall()
+ ipaserver.httpinstance.WebGuiInstance().uninstall()
+ ipaserver.httpinstance.HTTPInstance(fstore).uninstall()
+ ipaserver.krbinstance.KrbInstance(fstore).uninstall()
+ ipaserver.dsinstance.DsInstance().uninstall()
+ fstore.restore_all_files()
+ return 0
+
+def main():
+ global ds
+ global pw_name
+ ds = None
+
+ options = parse_options()
+
+ if os.getegid() != 0:
+ print "Must be root to setup server"
+ return 1
+
+ signal.signal(signal.SIGTERM, signal_handler)
+ signal.signal(signal.SIGINT, signal_handler)
+
+ if options.uninstall:
+ standard_logging_setup("/var/log/ipaserver-uninstall.log", options.debug)
+ else:
+ standard_logging_setup("/var/log/ipaserver-install.log", options.debug)
+ print "\nThe log file for this installation can be found in /var/log/ipaserver-install.log"
+
+ global fstore
+ fstore = sysrestore.FileStore('/var/lib/ipa/sysrestore')
+
+ if options.uninstall:
+ if not options.unattended:
+ print "\nThis is a NON REVERSIBLE operation and will delete all data and configuration!\n"
+ if not user_input("Are you sure you want to continue with the uninstall procedure?", False):
+ print ""
+ print "Aborting uninstall operation."
+ sys.exit(1)
+
+ return uninstall()
+
+ print "=============================================================================="
+ print "This program will setup the FreeIPA Server."
+ print ""
+ print "This includes:"
+ if options.conf_ntp:
+ print " * Configure the Network Time Daemon (ntpd)"
+ print " * Create and configure an instance of Directory Server"
+ print " * Create and configure a Kerberos Key Distribution Center (KDC)"
+ print " * Configure Apache (httpd)"
+ print " * Configure TurboGears"
+ if options.setup_bind:
+ print " * Configure DNS (bind)"
+ if not options.conf_ntp:
+ print ""
+ print "Excluded by options:"
+ print " * Configure the Network Time Daemon (ntpd)"
+ print ""
+ print "To accept the default shown in brackets, press the Enter key."
+ print ""
+
+ check_dirsrv(options.unattended)
+
+ ds_user = ""
+ realm_name = ""
+ host_name = ""
+ domain_name = ""
+ ip_address = ""
+ master_password = ""
+ dm_password = ""
+ admin_password = ""
+
+ # check bind packages are installed
+ if options.setup_bind:
+ if not ipaserver.bindinstance.check_inst():
+ print "--setup-bind was specified but bind is not installed on the system"
+ print "Please install bind and restart the setup program"
+ return 1
+
+ # check the hostname is correctly configured, it must be as the kldap
+ # utilities just use the hostname as returned by gethostbyname to set
+ # up some of the standard entries
+
+ host_default = ""
+ if options.host_name:
+ host_default = options.host_name
+ else:
+ host_default = get_fqdn()
+
+ if options.unattended:
+ try:
+ verify_fqdn(host_default,options.no_host_dns)
+ except RuntimeError, e:
+ logging.error(str(e) + "\n")
+ return 1
+
+ host_name = host_default
+ else:
+ host_name = read_host_name(host_default,options.no_host_dns)
+
+ host_name = host_name.lower()
+
+ if not options.domain_name:
+ domain_name = read_domain_name(host_name[host_name.find(".")+1:], options.unattended)
+ else:
+ domain_name = options.domain_name
+
+ domain_name = domain_name.lower()
+
+ # Check we have a public IP that is associated with the hostname
+ ip = resolve_host(host_name)
+ if ip is None:
+ if options.ip_address:
+ ip = options.ip_address
+ if ip is None and options.unattended:
+ print "Unable to resolve IP address for host name"
+ return 1
+
+ if not verify_ip_address(ip):
+ ip = ""
+ if options.unattended:
+ return 1
+
+ if options.ip_address and options.ip_address != ip:
+ if options.setup_bind:
+ ip = options.ip_address
+ else:
+ print "Error: the hostname resolves to an IP address that is different"
+ print "from the one provided on the command line. Please fix your DNS"
+ print "or /etc/hosts file and restart the installation."
+ return 1
+
+ if options.unattended:
+ if not ip:
+ print "Unable to resolve IP address"
+ return 1
+
+ if not ip:
+ ip = read_ip_address(host_name)
+ ip_address = ip
+
+ print "The IPA Master Server will be configured with"
+ print "Hostname: " + host_name
+ print "IP address: " + ip_address
+ print "Domain name: " + domain_name
+ print ""
+
+ if not options.ds_user:
+ ds_user = read_ds_user()
+ if ds_user == "":
+ return 1
+ else:
+ ds_user = options.ds_user
+
+ if not options.realm_name:
+ realm_name = read_realm_name(domain_name, options.unattended)
+ else:
+ realm_name = options.realm_name.upper()
+
+ if not options.dm_password:
+ dm_password = read_dm_password()
+ else:
+ dm_password = options.dm_password
+
+ if not options.master_password:
+ master_password = ipa_generate_password()
+ else:
+ master_password = options.master_password
+
+ if not options.admin_password:
+ admin_password = read_admin_password()
+ else:
+ admin_password = options.admin_password
+
+ if not options.unattended:
+ print ""
+ print "The following operations may take some minutes to complete."
+ print "Please wait until the prompt is returned."
+
+ # Configure ntpd
+ if options.conf_ntp:
+ ntp = ipaserver.ntpinstance.NTPInstance(fstore)
+ ntp.create_instance()
+
+ if options.dirsrv_pin:
+ [pw_fd, pw_name] = tempfile.mkstemp()
+ os.write(pw_fd, options.dirsrv_pin)
+ os.close(pw_fd)
+
+ # Create a directory server instance
+ ds = ipaserver.dsinstance.DsInstance()
+ if options.dirsrv_pkcs12:
+ pkcs12_info = (options.dirsrv_pkcs12, pw_name)
+ ds.create_instance(ds_user, realm_name, host_name, domain_name, dm_password, pkcs12_info)
+ os.remove(pw_name)
+ else:
+ ds.create_instance(ds_user, realm_name, host_name, domain_name, dm_password)
+
+ # Create a kerberos instance
+ krb = ipaserver.krbinstance.KrbInstance(fstore)
+ krb.create_instance(ds_user, realm_name, host_name, domain_name, dm_password, master_password)
+
+ # Create a HTTP instance
+
+ if options.http_pin:
+ [pw_fd, pw_name] = tempfile.mkstemp()
+ os.write(pw_fd, options.http_pin)
+ os.close(pw_fd)
+
+ http = ipaserver.httpinstance.HTTPInstance(fstore)
+ if options.http_pkcs12:
+ pkcs12_info = (options.http_pkcs12, pw_name)
+ http.create_instance(realm_name, host_name, domain_name, autoconfig=False, pkcs12_info=pkcs12_info)
+ os.remove(pw_name)
+ else:
+ http.create_instance(realm_name, host_name, domain_name, autoconfig=True)
+
+ # Create the config file
+ fstore.backup_file("/etc/ipa/ipa.conf")
+ fd = open("/etc/ipa/ipa.conf", "w")
+ fd.write("[defaults]\n")
+ fd.write("server=" + host_name + "\n")
+ fd.write("realm=" + realm_name + "\n")
+ fd.write("domain=" + domain_name + "\n")
+ fd.close()
+
+ # Create a Web Gui instance
+ webgui = ipaserver.httpinstance.WebGuiInstance()
+ webgui.create_instance()
+
+ bind = ipaserver.bindinstance.BindInstance(fstore)
+ bind.setup(host_name, ip_address, realm_name, domain_name)
+ if options.setup_bind:
+ bind.create_instance()
+ else:
+ bind.create_sample_bind_zone()
+
+ # Apply any LDAP updates. Needs to be done after the configuration file
+ # is created
+ service.print_msg("Applying LDAP updates")
+ ds.apply_updates()
+
+ # Restart ds and krb after configurations have been changed
+ service.print_msg("restarting the directory server")
+ ds.restart()
+
+ service.print_msg("restarting the KDC")
+ krb.restart()
+
+ # Set the admin user kerberos password
+ ds.change_admin_password(admin_password)
+
+ # Call client install script
+ try:
+ run(["/usr/sbin/ipa-client-install", "--on-master", "--unattended", "--domain", domain_name, "--server", host_name, "--realm", realm_name])
+ except Exception, e:
+ print "Configuration of client side components failed!"
+ print "ipa-client-install returned: " + str(e)
+ return 1
+
+ print "=============================================================================="
+ print "Setup complete"
+ print ""
+ print "Next steps:"
+ print "\t1. You must make sure these network ports are open:"
+ print "\t\tTCP Ports:"
+ print "\t\t * 80, 443: HTTP/HTTPS"
+ print "\t\t * 389, 636: LDAP/LDAPS"
+ print "\t\t * 88, 464: kerberos"
+ if options.setup_bind:
+ print "\t\t * 53: bind"
+ print "\t\tUDP Ports:"
+ print "\t\t * 88, 464: kerberos"
+ if options.setup_bind:
+ print "\t\t * 53: bind"
+ if options.conf_ntp:
+ print "\t\t * 123: ntp"
+ print ""
+ print "\t2. You can now obtain a kerberos ticket using the command: 'kinit admin'"
+ print "\t This ticket will allow you to use the IPA tools (e.g., ipa-adduser)"
+ print "\t and the web user interface."
+
+ if not service.is_running("ntpd"):
+ print "\t3. Kerberos requires time synchronization between clients"
+ print "\t and servers for correct operation. You should consider enabling ntpd."
+
+ print ""
+ if not options.dirsrv_pkcs12:
+ print "Be sure to back up the CA certificate stored in " + ipaserver.dsinstance.config_dirname(ds.serverid) + "cacert.p12"
+ print "The password for this file is in " + ipaserver.dsinstance.config_dirname(ds.serverid) + "pwdfile.txt"
+ else:
+ print "In order for Firefox autoconfiguration to work you will need to"
+ print "use a SSL signing certificate. See the IPA documentation for more details."
+ print "You also need to install a PEM copy of the HTTP issuing CA into"
+ print "/usr/share/ipa/html/ca.crt"
+
+ return 0
+
+try:
+ try:
+ sys.exit(main())
+ except SystemExit, e:
+ sys.exit(e)
+ except Exception, e:
+ message = "Unexpected error - see ipaserver-install.log for details:\n %s" % str(e)
+ print message
+ message = str(e)
+ for str in traceback.format_tb(sys.exc_info()[2]):
+ message = message + "\n" + str
+ logging.debug(message)
+ sys.exit(1)
+finally:
+ if pw_name and ipautil.file_exists(pw_name):
+ os.remove(pw_name)
diff --git a/ipa-server/ipa-install/ipactl b/ipa-server/ipa-install/ipactl
new file mode 100644
index 00000000..11038394
--- /dev/null
+++ b/ipa-server/ipa-install/ipactl
@@ -0,0 +1,57 @@
+#!/bin/sh
+#
+# Copyright (C) 2008 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+#
+# IPA control to start/stop the various services required for IPA in the
+# proper order
+#
+
+function start() {
+ /sbin/service dirsrv start
+ /sbin/service ntpd start
+ /sbin/service krb5kdc start
+ /sbin/service ipa_kpasswd start
+ /sbin/service ipa_webgui start
+ /sbin/service httpd start
+}
+
+function stop() {
+ /sbin/service ipa_webgui stop
+ /sbin/service ipa_kpasswd stop
+ /sbin/service httpd stop
+ /sbin/service krb5kdc stop
+ /sbin/service dirsrv stop
+ /sbin/service ntpd stop
+}
+
+case "$1" in
+restart)
+ stop
+ start
+ ;;
+start)
+ start
+ ;;
+stop)
+ stop
+ ;;
+*)
+ echo "Usage: ipactl {start|stop|restart}"
+ exit 1
+ ;;
+esac
diff --git a/ipa-server/ipa-install/share/60ipaconfig.ldif b/ipa-server/ipa-install/share/60ipaconfig.ldif
new file mode 100644
index 00000000..f4edbcc9
--- /dev/null
+++ b/ipa-server/ipa-install/share/60ipaconfig.ldif
@@ -0,0 +1,42 @@
+## schema file for ipa configuration
+##
+## IPA Base OID: 2.16.840.1.113730.3.8
+##
+## Attributes: 2.16.840.1.113730.3.8.1
+## ObjectClasses: 2.16.840.1.113730.3.8.2
+dn: cn=schema
+###############################################
+##
+## Attributes
+##
+## ipaUserSearchFields - attribute names to search against when looking for users
+attributetypes: ( 2.16.840.1.113730.3.8.1.1 NAME 'ipaUserSearchFields' EQUALITY caseExactIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26)
+## ipaGroupSearchFields - attribute names to search against when looking for groups
+attributetypes: ( 2.16.840.1.113730.3.8.1.2 NAME 'ipaGroupSearchFields' EQUALITY caseExactIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26)
+## ipaSearchTimeLimit - search time limit in seconds
+attributetypes: ( 2.16.840.1.113730.3.8.1.3 NAME 'ipaSearchTimeLimit' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE)
+## ipaSearchRecordsLimit - maximum number of records to return
+attributetypes: ( 2.16.840.1.113730.3.8.1.4 NAME 'ipaSearchRecordsLimit' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE)
+## ipaCustomFields - custom fields to show in the UI in addition to pre-defined ones
+attributetypes: ( 2.16.840.1.113730.3.8.1.5 NAME 'ipaCustomFields' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15)
+## ipaHomesRootDir - default posix home directory root dir to use when creating new accounts
+attributetypes: ( 2.16.840.1.113730.3.8.1.6 NAME 'ipaHomesRootDir' EQUALITY caseExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE)
+## ipaDefaultLoginShell - default posix login shell to use when creating new accounts
+attributetypes: ( 2.16.840.1.113730.3.8.1.7 NAME 'ipaDefaultLoginShell' EQUALITY caseExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE)
+## ipaDefaultPrimaryGroup - default posix primary group to assign when creating new accounts
+attributetypes: ( 2.16.840.1.113730.3.8.1.8 NAME 'ipaDefaultPrimaryGroup' EQUALITY caseExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE)
+## ipaMaxUsernameLength - maximum username length to allow in the UI
+attributetypes: ( 2.16.840.1.113730.3.8.1.9 NAME 'ipaMaxUsernameLength' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE)
+## ipaPwdExpAdvNotify - time in days to send out paswword expiration notification before passwpord actually expires
+attributetypes: ( 2.16.840.1.113730.3.8.1.10 NAME 'ipaPwdExpAdvNotify' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE)
+# ipaUserObjectClasses - required objectclasses for users
+attributetypes: ( 2.16.840.1.113730.3.8.1.11 NAME 'ipaUserObjectClasses' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15)
+# ipaGroupObjectClasses - required objectclasses for groups
+attributetypes: ( 2.16.840.1.113730.3.8.1.12 NAME 'ipaGroupObjectClasses' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15)
+attributetypes: ( 2.16.840.1.113730.3.8.1.13 NAME 'ipaDefaultEmailDomain' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15)
+###############################################
+##
+## ObjectClasses
+##
+## ipaGuiConfig - GUI config parameters objectclass
+objectClasses: ( 2.16.840.1.113730.3.8.2.1 NAME 'ipaGuiConfig' AUXILIARY MAY ( ipaUserSearchFields $ ipaGroupSearchFields $ ipaSearchTimeLimit $ ipaSearchRecordsLimit $ ipaCustomFields $ ipaHomesRootDir $ ipaDefaultLoginShell $ ipaDefaultPrimaryGroup $ ipaMaxUsernameLength $ ipaPwdExpAdvNotify $ ipaUserObjectClasses $ ipaGroupObjectClasses $ ipaDefaultEmailDomain) )
diff --git a/ipa-server/ipa-install/share/60kerberos.ldif b/ipa-server/ipa-install/share/60kerberos.ldif
new file mode 100644
index 00000000..3431d22e
--- /dev/null
+++ b/ipa-server/ipa-install/share/60kerberos.ldif
@@ -0,0 +1,283 @@
+dn: cn=schema
+# Novell Kerberos Schema Definitions
+# Novell Inc.
+# 1800 South Novell Place
+# Provo, UT 84606
+#
+# VeRsIoN=1.0
+# CoPyRiGhT=(c) Copyright 2006, Novell, Inc. All rights reserved
+#
+# OIDs:
+# joint-iso-ccitt(2)
+# country(16)
+# us(840)
+# organization(1)
+# Novell(113719)
+# applications(1)
+# kerberos(301)
+# Kerberos Attribute Type(4) attr# version#
+# specific attribute definitions
+# Kerberos Attribute Syntax(5)
+# specific syntax definitions
+# Kerberos Object Class(6) class# version#
+# specific class definitions
+########################################################################
+########################################################################
+# Attribute Type Definitions #
+########################################################################
+##### This is the principal name in the RFC 1964 specified format
+attributetypes: ( 2.16.840.1.113719.1.301.4.1.1 NAME 'krbPrincipalName' EQUALITY caseExactIA5Match SUBSTR caseExactSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26)
+##### This specifies the type of the principal, the types could be any of
+##### the types mentioned in section 6.2 of RFC 4120
+attributetypes: ( 2.16.840.1.113719.1.301.4.3.1 NAME 'krbPrincipalType' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE)
+##### This flag is used to find whether directory User Password has to be used
+##### as kerberos password.
+##### TRUE, if User Password is to be used as the kerberos password.
+##### FALSE, if User Password and the kerberos password are different.
+attributetypes: ( 2.16.840.1.113719.1.301.4.5.1 NAME 'krbUPEnabled' DESC 'Boolean' SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE)
+##### The time at which the principal expires
+attributetypes: ( 2.16.840.1.113719.1.301.4.6.1 NAME 'krbPrincipalExpiration' EQUALITY generalizedTimeMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.24 SINGLE-VALUE)
+##### The krbTicketFlags attribute holds information about the kerberos flags for a principal
+##### The values (0x00000001 - 0x00800000) are reserved for standards and
+##### values (0x01000000 - 0x80000000) can be used for proprietary extensions.
+##### The flags and values as per RFC 4120 and MIT implementation are,
+##### DISALLOW_POSTDATED 0x00000001
+##### DISALLOW_FORWARDABLE 0x00000002
+##### DISALLOW_TGT_BASED 0x00000004
+##### DISALLOW_RENEWABLE 0x00000008
+##### DISALLOW_PROXIABLE 0x00000010
+##### DISALLOW_DUP_SKEY 0x00000020
+##### DISALLOW_ALL_TIX 0x00000040
+##### REQUIRES_PRE_AUTH 0x00000080
+##### REQUIRES_HW_AUTH 0x00000100
+##### REQUIRES_PWCHANGE 0x00000200
+##### DISALLOW_SVR 0x00001000
+##### PWCHANGE_SERVICE 0x00002000
+attributetypes: ( 2.16.840.1.113719.1.301.4.8.1 NAME 'krbTicketFlags' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE)
+##### The maximum ticket lifetime for a principal in seconds
+attributetypes: ( 2.16.840.1.113719.1.301.4.9.1 NAME 'krbMaxTicketLife' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE)
+##### Maximum renewable lifetime for a principal's ticket in seconds
+attributetypes: ( 2.16.840.1.113719.1.301.4.10.1 NAME 'krbMaxRenewableAge' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE)
+##### Forward reference to the Realm object.
+##### (FDN of the krbRealmContainer object).
+##### Example: cn=ACME.COM, cn=Kerberos, cn=Security
+attributetypes: ( 2.16.840.1.113719.1.301.4.14.1 NAME 'krbRealmReferences' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12)
+##### List of LDAP servers that kerberos servers can contact.
+##### The attribute holds data in the ldap uri format,
+##### Example: ldaps://acme.com:636
+#####
+##### The values of this attribute need to be updated, when
+##### the LDAP servers listed here are renamed, moved or deleted.
+attributetypes: ( 2.16.840.1.113719.1.301.4.15.1 NAME 'krbLdapServers' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15)
+##### A set of forward references to the KDC Service objects.
+##### (FDNs of the krbKdcService objects).
+##### Example: cn=kdc - server 1, ou=uvw, o=xyz
+attributetypes: ( 2.16.840.1.113719.1.301.4.17.1 NAME 'krbKdcServers' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12)
+##### A set of forward references to the Password Service objects.
+##### (FDNs of the krbPwdService objects).
+##### Example: cn=kpasswdd - server 1, ou=uvw, o=xyz
+attributetypes: ( 2.16.840.1.113719.1.301.4.18.1 NAME 'krbPwdServers' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12)
+##### This attribute holds the Host Name or the ip address,
+##### transport protocol and ports of the kerberos service host
+##### The format is host_name-or-ip_address#protocol#port
+##### Protocol can be 0 or 1. 0 is for UDP. 1 is for TCP.
+attributetypes: ( 2.16.840.1.113719.1.301.4.24.1 NAME 'krbHostServer' EQUALITY caseExactIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26)
+##### This attribute holds the scope for searching the principals
+##### under krbSubTree attribute of krbRealmContainer
+##### The value can either be 1 (ONE) or 2 (SUB_TREE).
+attributetypes: ( 2.16.840.1.113719.1.301.4.25.1 NAME 'krbSearchScope' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE)
+##### FDNs pointing to Kerberos principals
+attributetypes: ( 2.16.840.1.113719.1.301.4.26.1 NAME 'krbPrincipalReferences' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12)
+##### This attribute specifies which attribute of the user objects
+##### be used as the principal name component for Kerberos.
+##### The allowed values are cn, sn, uid, givenname, fullname.
+attributetypes: ( 2.16.840.1.113719.1.301.4.28.1 NAME 'krbPrincNamingAttr' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE)
+##### A set of forward references to the Administration Service objects.
+##### (FDNs of the krbAdmService objects).
+##### Example: cn=kadmindd - server 1, ou=uvw, o=xyz
+attributetypes: ( 2.16.840.1.113719.1.301.4.29.1 NAME 'krbAdmServers' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12)
+##### Maximum lifetime of a principal's password
+attributetypes: ( 2.16.840.1.113719.1.301.4.30.1 NAME 'krbMaxPwdLife' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE)
+##### Minimum lifetime of a principal's password
+attributetypes: ( 2.16.840.1.113719.1.301.4.31.1 NAME 'krbMinPwdLife' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE)
+##### Minimum number of character clases allowed in a password
+attributetypes: ( 2.16.840.1.113719.1.301.4.32.1 NAME 'krbPwdMinDiffChars' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE)
+##### Minimum length of the password
+attributetypes: ( 2.16.840.1.113719.1.301.4.33.1 NAME 'krbPwdMinLength' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE)
+##### Number of previous versions of passwords that are stored
+attributetypes: ( 2.16.840.1.113719.1.301.4.34.1 NAME 'krbPwdHistoryLength' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE)
+##### FDN pointing to a Kerberos Password Policy object
+attributetypes: ( 2.16.840.1.113719.1.301.4.36.1 NAME 'krbPwdPolicyReference' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 SINGLE-VALUE)
+##### The time at which the principal's password expires
+attributetypes: ( 2.16.840.1.113719.1.301.4.37.1 NAME 'krbPasswordExpiration' EQUALITY generalizedTimeMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.24 SINGLE-VALUE)
+##### This attribute holds the principal's key (krbPrincipalKey) that is encrypted with
+##### the master key (krbMKey).
+##### The attribute is ASN.1 encoded.
+#####
+##### The format of the value for this attribute is explained below,
+##### KrbKeySet ::= SEQUENCE {
+##### attribute-major-vno [0] UInt16,
+##### attribute-minor-vno [1] UInt16,
+##### kvno [2] UInt32,
+##### mkvno [3] UInt32 OPTIONAL,
+##### keys [4] SEQUENCE OF KrbKey,
+##### ...
+##### }
+#####
+##### KrbKey ::= SEQUENCE {
+##### salt [0] KrbSalt OPTIONAL,
+##### key [1] EncryptionKey,
+##### s2kparams [2] OCTET STRING OPTIONAL,
+##### ...
+##### }
+#####
+##### KrbSalt ::= SEQUENCE {
+##### type [0] Int32,
+##### salt [1] OCTET STRING OPTIONAL
+##### }
+#####
+##### EncryptionKey ::= SEQUENCE {
+##### keytype [0] Int32,
+##### keyvalue [1] OCTET STRING
+##### }
+attributetypes: ( 2.16.840.1.113719.1.301.4.39.1 NAME 'krbPrincipalKey' EQUALITY octetStringMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.40)
+##### FDN pointing to a Kerberos Ticket Policy object.
+attributetypes: ( 2.16.840.1.113719.1.301.4.40.1 NAME 'krbTicketPolicyReference' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 SINGLE-VALUE)
+##### Forward reference to an entry that starts sub-trees
+##### where principals and other kerberos objects in the realm are configured.
+##### Example: ou=acme, ou=pq, o=xyz
+attributetypes: ( 2.16.840.1.113719.1.301.4.41.1 NAME 'krbSubTrees' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12)
+##### Holds the default encryption/salt type combinations of principals for
+##### the Realm. Stores in the form of key:salt strings.
+##### Example: des-cbc-crc:normal
+attributetypes: ( 2.16.840.1.113719.1.301.4.42.1 NAME 'krbDefaultEncSaltTypes' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15)
+##### Holds the Supported encryption/salt type combinations of principals for
+##### the Realm. Stores in the form of key:salt strings.
+##### The supported encryption types are mentioned in RFC 3961
+##### The supported salt types are,
+##### NORMAL
+##### V4
+##### NOREALM
+##### ONLYREALM
+##### SPECIAL
+##### AFS3
+##### Example: des-cbc-crc:normal
+#####
+##### This attribute obsoletes the krbSupportedEncTypes and krbSupportedSaltTypes
+##### attributes.
+attributetypes: ( 2.16.840.1.113719.1.301.4.43.1 NAME 'krbSupportedEncSaltTypes' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15)
+##### This attribute holds the principal's old keys (krbPwdHistory) that is encrypted with
+##### the kadmin/history key.
+##### The attribute is ASN.1 encoded.
+#####
+##### The format of the value for this attribute is explained below,
+##### KrbKeySet ::= SEQUENCE {
+##### attribute-major-vno [0] UInt16,
+##### attribute-minor-vno [1] UInt16,
+##### kvno [2] UInt32,
+##### mkvno [3] UInt32 OPTIONAL -- actually kadmin/history key,
+##### keys [4] SEQUENCE OF KrbKey,
+##### ...
+##### }
+#####
+##### KrbKey ::= SEQUENCE {
+##### salt [0] KrbSalt OPTIONAL,
+##### key [1] EncryptionKey,
+##### s2kparams [2] OCTET STRING OPTIONAL,
+##### ...
+##### }
+#####
+##### KrbSalt ::= SEQUENCE {
+##### type [0] Int32,
+##### salt [1] OCTET STRING OPTIONAL
+##### }
+#####
+##### EncryptionKey ::= SEQUENCE {
+##### keytype [0] Int32,
+##### keyvalue [1] OCTET STRING
+##### }
+attributetypes: ( 2.16.840.1.113719.1.301.4.44.1 NAME 'krbPwdHistory' EQUALITY octetStringMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.40)
+##### The time at which the principal's password last password change happened.
+attributetypes: ( 2.16.840.1.113719.1.301.4.45.1 NAME 'krbLastPwdChange' EQUALITY generalizedTimeMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.24 SINGLE-VALUE)
+##### This attribute holds the kerberos master key.
+##### This can be used to encrypt principal keys.
+##### This attribute has to be secured in directory.
+#####
+##### This attribute is ASN.1 encoded.
+##### The format of the value for this attribute is explained below,
+##### KrbMKey ::= SEQUENCE {
+##### kvno [0] UInt32,
+##### key [1] MasterKey
+##### }
+#####
+##### MasterKey ::= SEQUENCE {
+##### keytype [0] Int32,
+##### keyvalue [1] OCTET STRING
+##### }
+attributetypes: ( 2.16.840.1.113719.1.301.4.46.1 NAME 'krbMKey' EQUALITY octetStringMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.40)
+##### This stores the alternate principal names for the principal in the RFC 1961 specified format
+attributetypes: ( 2.16.840.1.113719.1.301.4.47.1 NAME 'krbPrincipalAliases' EQUALITY caseExactIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26)
+##### The time at which the principal's last successful authentication happened.
+attributetypes: ( 2.16.840.1.113719.1.301.4.48.1 NAME 'krbLastSuccessfulAuth' EQUALITY generalizedTimeMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.24 SINGLE-VALUE)
+##### The time at which the principal's last failed authentication happened.
+attributetypes: ( 2.16.840.1.113719.1.301.4.49.1 NAME 'krbLastFailedAuth' EQUALITY generalizedTimeMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.24 SINGLE-VALUE)
+##### This attribute stores the number of failed authentication attempts
+##### happened for the principal since the last successful authentication.
+attributetypes: ( 2.16.840.1.113719.1.301.4.50.1 NAME 'krbLoginFailedCount' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE)
+##### This attribute holds the application specific data.
+attributetypes: ( 2.16.840.1.113719.1.301.4.51.1 NAME 'krbExtraData' EQUALITY octetStringMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.40)
+##### This attributes holds references to the set of directory objects.
+##### This stores the DNs of the directory objects to which the
+##### principal object belongs to.
+attributetypes: ( 2.16.840.1.113719.1.301.4.52.1 NAME 'krbObjectReferences' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12)
+##### This attribute holds references to a Container object where
+##### the additional principal objects and stand alone principal
+##### objects (krbPrincipal) can be created.
+attributetypes: ( 2.16.840.1.113719.1.301.4.53.1 NAME 'krbPrincContainerRef' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12)
+########################################################################
+########################################################################
+# Object Class Definitions #
+########################################################################
+#### This is a kerberos container for all the realms in a tree.
+objectClasses: ( 2.16.840.1.113719.1.301.6.1.1 NAME 'krbContainer' SUP top MUST ( cn ) )
+##### The krbRealmContainer is created per realm and holds realm specific data.
+objectClasses: ( 2.16.840.1.113719.1.301.6.2.1 NAME 'krbRealmContainer' SUP top MUST ( cn ) MAY ( krbMKey $ krbUPEnabled $ krbSubTrees $ krbSearchScope $ krbLdapServers $ krbSupportedEncSaltTypes $ krbDefaultEncSaltTypes $ krbTicketPolicyReference $ krbKdcServers $ krbPwdServers $ krbAdmServers $ krbPrincNamingAttr $krbPwdPolicyReference $ krbPrincContainerRef ) )
+##### An instance of a class derived from krbService is created per
+##### kerberos authentication or administration server in an realm and holds
+##### references to the realm objects. These references is used to further read
+##### realm specific data to service AS/TGS requests. Additionally this object
+##### contains some server specific data like pathnames and ports that the
+##### server uses. This is the identity the kerberos server logs in with. A key
+##### pair for the same is created and the kerberos server logs in with the same.
+#####
+##### krbKdcService, krbAdmService and krbPwdService derive from this class.
+objectClasses: ( 2.16.840.1.113719.1.301.6.3.1 NAME 'krbService' ABSTRACT SUP ( top ) MUST ( cn ) MAY ( krbHostServer $ krbRealmReferences ) )
+##### Representative object for the KDC server to bind into a LDAP directory
+##### and have a connection to access Kerberos data with the required
+##### access rights.
+objectClasses: ( 2.16.840.1.113719.1.301.6.4.1 NAME 'krbKdcService' SUP ( krbService ) )
+##### Representative object for the Kerberos Password server to bind into a LDAP directory
+##### and have a connection to access Kerberos data with the required
+##### access rights.
+objectClasses: ( 2.16.840.1.113719.1.301.6.5.1 NAME 'krbPwdService' SUP ( krbService ) )
+###### The principal data auxiliary class. Holds principal information
+###### and is used to store principal information for Person, Service objects.
+objectClasses: ( 2.16.840.1.113719.1.301.6.8.1 NAME 'krbPrincipalAux' AUXILIARY MAY ( krbPrincipalName $ krbUPEnabled $ krbPrincipalKey $ krbTicketPolicyReference $ krbPrincipalExpiration $ krbPasswordExpiration $ krbPwdPolicyReference $ krbPrincipalType $ krbPwdHistory $ krbLastPwdChange $ krbPrincipalAliases $ krbLastSuccessfulAuth $ krbLastFailedAuth $ krbLoginFailedCount $ krbExtraData ) )
+###### This class is used to create additional principals and stand alone principals.
+objectClasses: ( 2.16.840.1.113719.1.301.6.9.1 NAME 'krbPrincipal' SUP ( top ) MUST ( krbPrincipalName ) MAY ( krbObjectReferences ) )
+###### The principal references auxiliary class. Holds all principals referred
+###### from a service
+objectClasses: ( 2.16.840.1.113719.1.301.6.11.1 NAME 'krbPrincRefAux' SUP top AUXILIARY MAY krbPrincipalReferences )
+##### Representative object for the Kerberos Administration server to bind into a LDAP directory
+##### and have a connection Id to access Kerberos data with the required access rights.
+objectClasses: ( 2.16.840.1.113719.1.301.6.13.1 NAME 'krbAdmService' SUP ( krbService ) )
+##### The krbPwdPolicy object is a template password policy that
+##### can be applied to principals when they are created.
+##### These policy attributes will be in effect, when the Kerberos
+##### passwords are different from users' passwords (UP).
+objectClasses: ( 2.16.840.1.113719.1.301.6.14.1 NAME 'krbPwdPolicy' SUP top MUST ( cn ) MAY ( krbMaxPwdLife $ krbMinPwdLife $ krbPwdMinDiffChars $ krbPwdMinLength $ krbPwdHistoryLength ) )
+##### The krbTicketPolicyAux holds Kerberos ticket policy attributes.
+##### This class can be attached to a principal object or realm object.
+objectClasses: ( 2.16.840.1.113719.1.301.6.16.1 NAME 'krbTicketPolicyAux' AUXILIARY MAY ( krbTicketFlags $ krbMaxTicketLife $ krbMaxRenewableAge ) )
+##### The krbTicketPolicy object is an effective ticket policy that is associated with a realm or a principal
+objectClasses: ( 2.16.840.1.113719.1.301.6.17.1 NAME 'krbTicketPolicy' SUP top MUST ( cn ) )
diff --git a/ipa-server/ipa-install/share/60radius.ldif b/ipa-server/ipa-install/share/60radius.ldif
new file mode 100644
index 00000000..93a5ba31
--- /dev/null
+++ b/ipa-server/ipa-install/share/60radius.ldif
@@ -0,0 +1,559 @@
+# This is a LDAPv3 schema for RADIUS attributes.
+# Tested on OpenLDAP 2.0.7
+# Posted by Javier Fernandez-Sanguino Pena <jfernandez@sgi.es>
+# LDAP v3 version by Jochen Friedrich <jochen@scram.de>
+# Updates by Adrian Pavlykevych <pam@polynet.lviv.ua>
+# Modified by John Dennis <jdennis@redhat.com> for use with Directory Sever/IPA
+#
+# Note: These OID's do not seem to be registered, the closest I could find
+# was 1.3.6.1.4.1.3317
+# {iso(1) identified-organization(3) dod(6) internet(1) private(4) enterprise(1) gnome(3317)}
+#
+##############
+dn: cn=schema
+attributeTypes:
+ ( 1.3.6.1.4.1.3317.4.3.1.1
+ NAME 'radiusArapFeatures'
+ DESC ''
+ EQUALITY caseIgnoreIA5Match
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
+ SINGLE-VALUE
+ )
+attributeTypes:
+ ( 1.3.6.1.4.1.3317.4.3.1.2
+ NAME 'radiusArapSecurity'
+ DESC ''
+ EQUALITY caseIgnoreIA5Match
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
+ SINGLE-VALUE
+ )
+attributeTypes:
+ ( 1.3.6.1.4.1.3317.4.3.1.3
+ NAME 'radiusArapZoneAccess'
+ DESC ''
+ EQUALITY caseIgnoreIA5Match
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
+ SINGLE-VALUE
+ )
+attributeTypes:
+ ( 1.3.6.1.4.1.3317.4.3.1.44
+ NAME 'radiusAuthType'
+ DESC ''
+ EQUALITY caseIgnoreIA5Match
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
+ SINGLE-VALUE
+ )
+attributeTypes:
+ ( 1.3.6.1.4.1.3317.4.3.1.4
+ NAME 'radiusCallbackId'
+ DESC ''
+ EQUALITY caseIgnoreIA5Match
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
+ SINGLE-VALUE
+ )
+attributeTypes:
+ ( 1.3.6.1.4.1.3317.4.3.1.5
+ NAME 'radiusCallbackNumber'
+ DESC ''
+ EQUALITY caseIgnoreIA5Match
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
+ SINGLE-VALUE
+ )
+attributeTypes:
+ ( 1.3.6.1.4.1.3317.4.3.1.6
+ NAME 'radiusCalledStationId'
+ DESC ''
+ EQUALITY caseIgnoreIA5Match
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
+ SINGLE-VALUE
+ )
+attributeTypes:
+ ( 1.3.6.1.4.1.3317.4.3.1.7
+ NAME 'radiusCallingStationId'
+ DESC ''
+ EQUALITY caseIgnoreIA5Match
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
+ SINGLE-VALUE
+ )
+attributeTypes:
+ ( 1.3.6.1.4.1.3317.4.3.1.8
+ NAME 'radiusClass'
+ DESC ''
+ EQUALITY caseIgnoreIA5Match
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
+ )
+attributeTypes:
+ ( 1.3.6.1.4.1.3317.4.3.1.45
+ NAME 'radiusClientIPAddress'
+ DESC ''
+ EQUALITY caseIgnoreIA5Match
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
+ SINGLE-VALUE
+ )
+attributeTypes:
+ ( 1.3.6.1.4.1.3317.4.3.1.9
+ NAME 'radiusFilterId'
+ DESC ''
+ EQUALITY caseIgnoreIA5Match
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
+ )
+attributeTypes:
+ ( 1.3.6.1.4.1.3317.4.3.1.10
+ NAME 'radiusFramedAppleTalkLink'
+ DESC ''
+ EQUALITY caseIgnoreIA5Match
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
+ SINGLE-VALUE
+ )
+attributeTypes:
+ ( 1.3.6.1.4.1.3317.4.3.1.11
+ NAME 'radiusFramedAppleTalkNetwork'
+ DESC ''
+ EQUALITY caseIgnoreIA5Match
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
+ )
+attributeTypes:
+ ( 1.3.6.1.4.1.3317.4.3.1.12
+ NAME 'radiusFramedAppleTalkZone'
+ DESC ''
+ EQUALITY caseIgnoreIA5Match
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
+ SINGLE-VALUE
+ )
+attributeTypes:
+ ( 1.3.6.1.4.1.3317.4.3.1.13
+ NAME 'radiusFramedCompression'
+ DESC ''
+ EQUALITY caseIgnoreIA5Match
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
+ )
+attributeTypes:
+ ( 1.3.6.1.4.1.3317.4.3.1.14
+ NAME 'radiusFramedIPAddress'
+ DESC ''
+ EQUALITY caseIgnoreIA5Match
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
+ SINGLE-VALUE
+ )
+attributeTypes:
+ ( 1.3.6.1.4.1.3317.4.3.1.15
+ NAME 'radiusFramedIPNetmask'
+ DESC ''
+ EQUALITY caseIgnoreIA5Match
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
+ SINGLE-VALUE
+ )
+attributeTypes:
+ ( 1.3.6.1.4.1.3317.4.3.1.16
+ NAME 'radiusFramedIPXNetwork'
+ DESC ''
+ EQUALITY caseIgnoreIA5Match
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
+ SINGLE-VALUE
+ )
+attributeTypes:
+ ( 1.3.6.1.4.1.3317.4.3.1.17
+ NAME 'radiusFramedMTU'
+ DESC ''
+ EQUALITY caseIgnoreIA5Match
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
+ SINGLE-VALUE
+ )
+attributeTypes:
+ ( 1.3.6.1.4.1.3317.4.3.1.18
+ NAME 'radiusFramedProtocol'
+ DESC ''
+ EQUALITY caseIgnoreIA5Match
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
+ SINGLE-VALUE
+ )
+attributeTypes:
+ ( 1.3.6.1.4.1.3317.4.3.1.19
+ NAME 'radiusFramedRoute'
+ DESC ''
+ EQUALITY caseIgnoreIA5Match
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
+ )
+attributeTypes:
+ ( 1.3.6.1.4.1.3317.4.3.1.20
+ NAME 'radiusFramedRouting'
+ DESC ''
+ EQUALITY caseIgnoreIA5Match
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
+ SINGLE-VALUE
+ )
+attributeTypes:
+ ( 1.3.6.1.4.1.3317.4.3.1.46
+ NAME 'radiusGroupName'
+ DESC ''
+ EQUALITY caseIgnoreIA5Match
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
+ )
+attributeTypes:
+ ( 1.3.6.1.4.1.3317.4.3.1.47
+ NAME 'radiusHint'
+ DESC ''
+ EQUALITY caseIgnoreIA5Match
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
+ SINGLE-VALUE
+ )
+attributeTypes:
+ ( 1.3.6.1.4.1.3317.4.3.1.48
+ NAME 'radiusHuntgroupName'
+ DESC ''
+ EQUALITY caseIgnoreIA5Match
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
+ )
+attributeTypes:
+ ( 1.3.6.1.4.1.3317.4.3.1.21
+ NAME 'radiusIdleTimeout'
+ DESC ''
+ EQUALITY caseIgnoreIA5Match
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
+ SINGLE-VALUE
+ )
+attributeTypes:
+ ( 1.3.6.1.4.1.3317.4.3.1.22
+ NAME 'radiusLoginIPHost'
+ DESC ''
+ EQUALITY caseIgnoreIA5Match
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
+ )
+attributeTypes:
+ ( 1.3.6.1.4.1.3317.4.3.1.23
+ NAME 'radiusLoginLATGroup'
+ DESC ''
+ EQUALITY caseIgnoreIA5Match
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
+ SINGLE-VALUE
+ )
+attributeTypes:
+ ( 1.3.6.1.4.1.3317.4.3.1.24
+ NAME 'radiusLoginLATNode'
+ DESC ''
+ EQUALITY caseIgnoreIA5Match
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
+ SINGLE-VALUE
+ )
+attributeTypes:
+ ( 1.3.6.1.4.1.3317.4.3.1.25
+ NAME 'radiusLoginLATPort'
+ DESC ''
+ EQUALITY caseIgnoreIA5Match
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
+ SINGLE-VALUE
+ )
+attributeTypes:
+ ( 1.3.6.1.4.1.3317.4.3.1.26
+ NAME 'radiusLoginLATService'
+ DESC ''
+ EQUALITY caseIgnoreIA5Match
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
+ SINGLE-VALUE
+ )
+attributeTypes:
+ ( 1.3.6.1.4.1.3317.4.3.1.27
+ NAME 'radiusLoginService'
+ DESC ''
+ EQUALITY caseIgnoreIA5Match
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
+ SINGLE-VALUE
+ )
+attributeTypes:
+ ( 1.3.6.1.4.1.3317.4.3.1.28
+ NAME 'radiusLoginTCPPort'
+ DESC ''
+ EQUALITY caseIgnoreIA5Match
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
+ SINGLE-VALUE
+ )
+attributeTypes:
+ ( 1.3.6.1.4.1.3317.4.3.1.29
+ NAME 'radiusPasswordRetry'
+ DESC ''
+ EQUALITY caseIgnoreIA5Match
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
+ SINGLE-VALUE
+ )
+attributeTypes:
+ ( 1.3.6.1.4.1.3317.4.3.1.30
+ NAME 'radiusPortLimit'
+ DESC ''
+ EQUALITY caseIgnoreIA5Match
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
+ SINGLE-VALUE
+ )
+attributeTypes:
+ ( 1.3.6.1.4.1.3317.4.3.1.49
+ NAME 'radiusProfileDn'
+ DESC ''
+ EQUALITY distinguishedNameMatch
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.12
+ SINGLE-VALUE
+ )
+attributeTypes:
+ ( 1.3.6.1.4.1.3317.4.3.1.31
+ NAME 'radiusPrompt'
+ DESC ''
+ EQUALITY caseIgnoreIA5Match
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
+ SINGLE-VALUE
+ )
+attributeTypes:
+ ( 1.3.6.1.4.1.3317.4.3.1.50
+ NAME 'radiusProxyToRealm'
+ DESC ''
+ EQUALITY caseIgnoreIA5Match
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
+ SINGLE-VALUE
+ )
+attributeTypes:
+ ( 1.3.6.1.4.1.3317.4.3.1.51
+ NAME 'radiusReplicateToRealm'
+ DESC ''
+ EQUALITY caseIgnoreIA5Match
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
+ SINGLE-VALUE
+ )
+attributeTypes:
+ ( 1.3.6.1.4.1.3317.4.3.1.52
+ NAME 'radiusRealm'
+ DESC ''
+ EQUALITY caseIgnoreIA5Match
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
+ SINGLE-VALUE
+ )
+attributeTypes:
+ ( 1.3.6.1.4.1.3317.4.3.1.32
+ NAME 'radiusServiceType'
+ DESC ''
+ EQUALITY caseIgnoreIA5Match
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
+ SINGLE-VALUE
+ )
+attributeTypes:
+ ( 1.3.6.1.4.1.3317.4.3.1.33
+ NAME 'radiusSessionTimeout'
+ DESC ''
+ EQUALITY caseIgnoreIA5Match
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
+ SINGLE-VALUE
+ )
+attributeTypes:
+ ( 1.3.6.1.4.1.3317.4.3.1.34
+ NAME 'radiusTerminationAction'
+ DESC ''
+ EQUALITY caseIgnoreIA5Match
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
+ SINGLE-VALUE
+ )
+attributeTypes:
+ ( 1.3.6.1.4.1.3317.4.3.1.35
+ NAME 'radiusTunnelAssignmentId'
+ DESC ''
+ EQUALITY caseIgnoreIA5Match
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
+ )
+attributeTypes:
+ ( 1.3.6.1.4.1.3317.4.3.1.36
+ NAME 'radiusTunnelMediumType'
+ DESC ''
+ EQUALITY caseIgnoreIA5Match
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
+ )
+attributeTypes:
+ ( 1.3.6.1.4.1.3317.4.3.1.37
+ NAME 'radiusTunnelPassword'
+ DESC ''
+ EQUALITY caseIgnoreIA5Match
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
+ SINGLE-VALUE
+ )
+attributeTypes:
+ ( 1.3.6.1.4.1.3317.4.3.1.38
+ NAME 'radiusTunnelPreference'
+ DESC ''
+ EQUALITY caseIgnoreIA5Match
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
+ )
+attributeTypes:
+ ( 1.3.6.1.4.1.3317.4.3.1.39
+ NAME 'radiusTunnelPrivateGroupId'
+ DESC ''
+ EQUALITY caseIgnoreIA5Match
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
+ )
+attributeTypes:
+ ( 1.3.6.1.4.1.3317.4.3.1.40
+ NAME 'radiusTunnelServerEndpoint'
+ DESC ''
+ EQUALITY caseIgnoreIA5Match
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
+ )
+attributeTypes:
+ ( 1.3.6.1.4.1.3317.4.3.1.41
+ NAME 'radiusTunnelType'
+ DESC ''
+ EQUALITY caseIgnoreIA5Match
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
+ )
+attributeTypes:
+ ( 1.3.6.1.4.1.3317.4.3.1.42
+ NAME 'radiusVSA'
+ DESC ''
+ EQUALITY caseIgnoreIA5Match
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
+ )
+attributeTypes:
+ ( 1.3.6.1.4.1.3317.4.3.1.43
+ NAME 'radiusTunnelClientEndpoint'
+ DESC ''
+ EQUALITY caseIgnoreIA5Match
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
+ )
+#need to change asn1.id
+attributeTypes:
+ ( 1.3.6.1.4.1.3317.4.3.1.53
+ NAME 'radiusSimultaneousUse'
+ DESC ''
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.27
+ SINGLE-VALUE
+ )
+attributeTypes:
+ ( 1.3.6.1.4.1.3317.4.3.1.54
+ NAME 'radiusLoginTime'
+ DESC ''
+ EQUALITY caseIgnoreIA5Match
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
+ SINGLE-VALUE
+ )
+attributeTypes:
+ ( 1.3.6.1.4.1.3317.4.3.1.55
+ NAME 'radiusUserCategory'
+ DESC ''
+ EQUALITY caseIgnoreIA5Match
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
+ SINGLE-VALUE
+ )
+attributeTypes:
+ ( 1.3.6.1.4.1.3317.4.3.1.56
+ NAME 'radiusStripUserName'
+ DESC ''
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.7
+ SINGLE-VALUE
+ )
+attributeTypes:
+ ( 1.3.6.1.4.1.3317.4.3.1.57
+ NAME 'dialupAccess'
+ DESC ''
+ EQUALITY caseIgnoreIA5Match
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
+ SINGLE-VALUE
+ )
+attributeTypes:
+ ( 1.3.6.1.4.1.3317.4.3.1.58
+ NAME 'radiusExpiration'
+ DESC ''
+ EQUALITY caseIgnoreIA5Match
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
+ SINGLE-VALUE
+ )
+attributeTypes:
+ ( 1.3.6.1.4.1.3317.4.3.1.59
+ NAME 'radiusCheckItem'
+ DESC ''
+ EQUALITY caseIgnoreIA5Match
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
+ )
+attributeTypes:
+ ( 1.3.6.1.4.1.3317.4.3.1.60
+ NAME 'radiusReplyItem'
+ DESC ''
+ EQUALITY caseIgnoreIA5Match
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
+ )
+attributeTypes:
+ ( 1.3.6.1.4.1.3317.4.3.1.61
+ NAME 'radiusNASIpAddress'
+ DESC ''
+ EQUALITY caseIgnoreIA5Match
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
+ SINGLE-VALUE
+ )
+attributeTypes:
+ ( 1.3.6.1.4.1.3317.4.3.1.62
+ NAME 'radiusReplyMessage'
+ DESC ''
+ EQUALITY caseIgnoreIA5Match
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
+ )
+objectClasses:
+ ( 1.3.6.1.4.1.3317.4.3.2.1
+ NAME 'radiusprofile'
+ SUP top AUXILIARY
+ DESC ''
+ MUST uid
+ MAY ( radiusArapFeatures $ radiusArapSecurity $ radiusArapZoneAccess $
+ radiusAuthType $ radiusCallbackId $ radiusCallbackNumber $
+ radiusCalledStationId $ radiusCallingStationId $ radiusClass $
+ radiusClientIPAddress $ radiusFilterId $ radiusFramedAppleTalkLink $
+ radiusFramedAppleTalkNetwork $ radiusFramedAppleTalkZone $
+ radiusFramedCompression $ radiusFramedIPAddress $
+ radiusFramedIPNetmask $ radiusFramedIPXNetwork $
+ radiusFramedMTU $ radiusFramedProtocol $
+ radiusCheckItem $ radiusReplyItem $
+ radiusFramedRoute $ radiusFramedRouting $ radiusIdleTimeout $
+ radiusGroupName $ radiusHint $ radiusHuntgroupName $
+ radiusLoginIPHost $ radiusLoginLATGroup $ radiusLoginLATNode $
+ radiusLoginLATPort $ radiusLoginLATService $ radiusLoginService $
+ radiusLoginTCPPort $ radiusLoginTime $ radiusPasswordRetry $
+ radiusPortLimit $ radiusPrompt $ radiusProxyToRealm $
+ radiusRealm $ radiusReplicateToRealm $ radiusServiceType $
+ radiusSessionTimeout $ radiusStripUserName $
+ radiusTerminationAction $ radiusTunnelClientEndpoint $ radiusProfileDn $
+ radiusSimultaneousUse $ radiusTunnelAssignmentId $
+ radiusTunnelMediumType $ radiusTunnelPassword $ radiusTunnelPreference $
+ radiusTunnelPrivateGroupId $ radiusTunnelServerEndpoint $
+ radiusTunnelType $ radiusUserCategory $ radiusVSA $
+ radiusExpiration $ dialupAccess $ radiusNASIpAddress $
+ radiusReplyMessage )
+ )
+objectClasses:
+ ( 1.3.6.1.4.1.3317.4.3.2.2
+ NAME 'radiusObjectProfile'
+ SUP top STRUCTURAL
+ DESC 'A Container Objectclass to be used for creating radius profile object'
+ MUST cn
+ MAY ( uid $ userPassword $ description )
+ )
+attributeTypes:
+ ( 1.3.6.1.4.1.3317.4.3.1.64
+ NAME 'radiusClientSecret'
+ DESC ''
+ EQUALITY caseIgnoreIA5Match
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
+ SINGLE-VALUE
+ )
+attributeTypes:
+ ( 1.3.6.1.4.1.3317.4.3.1.65
+ NAME 'radiusClientNASType'
+ DESC ''
+ EQUALITY caseIgnoreIA5Match
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
+ SINGLE-VALUE
+ )
+attributeTypes:
+ ( 1.3.6.1.4.1.3317.4.3.1.66
+ NAME 'radiusClientShortName'
+ DESC ''
+ EQUALITY caseIgnoreIA5Match
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
+ )
+objectClasses:
+ ( 1.3.6.1.4.1.3317.4.3.2.3
+ NAME 'radiusClientProfile'
+ SUP top STRUCTURAL
+ DESC 'A Container Objectclass to be used for describing radius clients'
+ MUST (radiusClientIPAddress $ radiusClientSecret)
+ MAY ( radiusClientNASType $ radiusClientShortName $ description )
+ )
diff --git a/ipa-server/ipa-install/share/60samba.ldif b/ipa-server/ipa-install/share/60samba.ldif
new file mode 100644
index 00000000..d3a6d31b
--- /dev/null
+++ b/ipa-server/ipa-install/share/60samba.ldif
@@ -0,0 +1,152 @@
+## schema file for Fedora DS
+##
+## Schema for storing Samba user accounts and group maps in LDAP
+## OIDs are owned by the Samba Team
+##
+## Prerequisite schemas - uid (cosine.schema)
+## - displayName (inetorgperson.schema)
+## - gidNumber (nis.schema)
+##
+## 1.3.6.1.4.1.7165.2.1.x - attributeTypess
+## 1.3.6.1.4.1.7165.2.2.x - objectClasseses
+##
+## Printer support
+## 1.3.6.1.4.1.7165.2.3.1.x - attributeTypess
+## 1.3.6.1.4.1.7165.2.3.2.x - objectClasseses
+##
+## Samba4
+## 1.3.6.1.4.1.7165.4.1.x - attributeTypess
+## 1.3.6.1.4.1.7165.4.2.x - objectClasseses
+## 1.3.6.1.4.1.7165.4.3.x - LDB/LDAP Controls
+## 1.3.6.1.4.1.7165.4.4.x - LDB/LDAP Extended Operations
+## 1.3.6.1.4.1.7165.4.255.x - mapped OIDs due to conflicts between AD and standards-track
+##
+dn: cn=schema
+##
+#######################################################################
+## Attributes used by Samba 3.0 schema ##
+#######################################################################
+##
+## Password hashes##
+attributeTypes: ( 1.3.6.1.4.1.7165.2.1.24 NAME 'sambaLMPassword' DESC 'LanManager Password' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{32} SINGLE-VALUE )
+attributeTypes: ( 1.3.6.1.4.1.7165.2.1.25 NAME 'sambaNTPassword' DESC 'MD4 hash of the unicode password' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{32} SINGLE-VALUE )
+##
+## Account flags in string format ([UWDX ])
+##
+attributeTypes: ( 1.3.6.1.4.1.7165.2.1.26 NAME 'sambaAcctFlags' DESC 'Account Flags' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{16} SINGLE-VALUE )
+##
+## Password timestamps & policies
+##
+attributeTypes: ( 1.3.6.1.4.1.7165.2.1.27 NAME 'sambaPwdLastSet' DESC 'Timestamp of the last password update' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )
+attributeTypes: ( 1.3.6.1.4.1.7165.2.1.28 NAME 'sambaPwdCanChange' DESC 'Timestamp of when the user is allowed to update the password' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )
+attributeTypes: ( 1.3.6.1.4.1.7165.2.1.29 NAME 'sambaPwdMustChange' DESC 'Timestamp of when the password will expire' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )
+attributeTypes: ( 1.3.6.1.4.1.7165.2.1.30 NAME 'sambaLogonTime' DESC 'Timestamp of last logon' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )
+attributeTypes: ( 1.3.6.1.4.1.7165.2.1.31 NAME 'sambaLogoffTime' DESC 'Timestamp of last logoff' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )
+attributeTypes: ( 1.3.6.1.4.1.7165.2.1.32 NAME 'sambaKickoffTime' DESC 'Timestamp of when the user will be logged off automatically' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )
+attributeTypes: ( 1.3.6.1.4.1.7165.2.1.48 NAME 'sambaBadPasswordCount' DESC 'Bad password attempt count' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )
+attributeTypes: ( 1.3.6.1.4.1.7165.2.1.49 NAME 'sambaBadPasswordTime' DESC 'Time of the last bad password attempt' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )
+attributeTypes: ( 1.3.6.1.4.1.7165.2.1.55 NAME 'sambaLogonHours' DESC 'Logon Hours' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{42} SINGLE-VALUE )
+##
+## string settings
+##
+attributeTypes: ( 1.3.6.1.4.1.7165.2.1.33 NAME 'sambaHomeDrive' DESC 'Driver letter of home directory mapping' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{4} SINGLE-VALUE )
+attributeTypes: ( 1.3.6.1.4.1.7165.2.1.34 NAME 'sambaLogonScript' DESC 'Logon script path' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{255} SINGLE-VALUE )
+attributeTypes: ( 1.3.6.1.4.1.7165.2.1.35 NAME 'sambaProfilePath' DESC 'Roaming profile path' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{255} SINGLE-VALUE )
+attributeTypes: ( 1.3.6.1.4.1.7165.2.1.36 NAME 'sambaUserWorkstations' DESC 'List of user workstations the user is allowed to logon to' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{255} SINGLE-VALUE )
+attributeTypes: ( 1.3.6.1.4.1.7165.2.1.37 NAME 'sambaHomePath' DESC 'Home directory UNC path' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{128} )
+attributeTypes: ( 1.3.6.1.4.1.7165.2.1.38 NAME 'sambaDomainName' DESC 'Windows NT domain to which the user belongs' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{128} )
+attributeTypes: ( 1.3.6.1.4.1.7165.2.1.47 NAME 'sambaMungedDial' DESC 'Base64 encoded user parameter string' EQUALITY caseExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{1050} )
+attributeTypes: ( 1.3.6.1.4.1.7165.2.1.54 NAME 'sambaPasswordHistory' DESC 'Concatenated MD5 hashes of the salted NT passwords used on this account' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{32} )
+##
+## SID, of any type
+##
+attributeTypes: ( 1.3.6.1.4.1.7165.2.1.20 NAME 'sambaSID' DESC 'Security ID' EQUALITY caseIgnoreIA5Match SUBSTR caseExactIA5SubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{64} SINGLE-VALUE )
+##
+## Primary group SID, compatible with ntSid
+##
+attributeTypes: ( 1.3.6.1.4.1.7165.2.1.23 NAME 'sambaPrimaryGroupSID' DESC 'Primary Group Security ID' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{64} SINGLE-VALUE )
+attributeTypes: ( 1.3.6.1.4.1.7165.2.1.51 NAME 'sambaSIDList' DESC 'Security ID List' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{64} )
+##
+## group mapping attributes
+##
+attributeTypes: ( 1.3.6.1.4.1.7165.2.1.19 NAME 'sambaGroupType' DESC 'NT Group Type' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )
+##
+## Store info on the domain
+##
+attributeTypes: ( 1.3.6.1.4.1.7165.2.1.21 NAME 'sambaNextUserRid' DESC 'Next NT rid to give our for users' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )
+attributeTypes: ( 1.3.6.1.4.1.7165.2.1.22 NAME 'sambaNextGroupRid' DESC 'Next NT rid to give out for groups' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )
+attributeTypes: ( 1.3.6.1.4.1.7165.2.1.39 NAME 'sambaNextRid' DESC 'Next NT rid to give out for anything' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )
+attributeTypes: ( 1.3.6.1.4.1.7165.2.1.40 NAME 'sambaAlgorithmicRidBase' DESC 'Base at which the samba RID generation algorithm should operate' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )
+attributeTypes: ( 1.3.6.1.4.1.7165.2.1.41 NAME 'sambaShareName' DESC 'Share Name' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )
+attributeTypes: ( 1.3.6.1.4.1.7165.2.1.42 NAME 'sambaOptionName' DESC 'Option Name' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} )
+attributeTypes: ( 1.3.6.1.4.1.7165.2.1.43 NAME 'sambaBoolOption' DESC 'A boolean option' EQUALITY booleanMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE )
+attributeTypes: ( 1.3.6.1.4.1.7165.2.1.44 NAME 'sambaIntegerOption' DESC 'An integer option' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )
+attributeTypes: ( 1.3.6.1.4.1.7165.2.1.45 NAME 'sambaStringOption' DESC 'A string option' EQUALITY caseExactIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE )
+attributeTypes: ( 1.3.6.1.4.1.7165.2.1.46 NAME 'sambaStringListOption' DESC 'A string list option' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )
+##attributeTypes: ( 1.3.6.1.4.1.7165.2.1.50 NAME 'sambaPrivName'
+## SUP name )
+##
+##attributeTypes: ( 1.3.6.1.4.1.7165.2.1.52 NAME 'sambaPrivilegeList'
+## DESC 'Privileges List'
+## EQUALITY caseIgnoreIA5Match
+## SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{64} )
+attributeTypes: ( 1.3.6.1.4.1.7165.2.1.53 NAME 'sambaTrustFlags' DESC 'Trust Password Flags' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )
+# "min password length"
+attributeTypes: ( 1.3.6.1.4.1.7165.2.1.58 NAME 'sambaMinPwdLength' DESC 'Minimal password length (default: 5)' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )
+# "password history"
+attributeTypes: ( 1.3.6.1.4.1.7165.2.1.59 NAME 'sambaPwdHistoryLength' DESC 'Length of Password History Entries (default: 0 => off)' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )
+# "user must logon to change password"
+attributeTypes: ( 1.3.6.1.4.1.7165.2.1.60 NAME 'sambaLogonToChgPwd' DESC 'Force Users to logon for password change (default: 0 => off, 2 => on)' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )
+# "maximum password age"
+attributeTypes: ( 1.3.6.1.4.1.7165.2.1.61 NAME 'sambaMaxPwdAge' DESC 'Maximum password age, in seconds (default: -1 => never expire passwords)' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )
+# "minimum password age"
+attributeTypes: ( 1.3.6.1.4.1.7165.2.1.62 NAME 'sambaMinPwdAge' DESC 'Minimum password age, in seconds (default: 0 => allow immediate password change)' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )
+# "lockout duration"
+attributeTypes: ( 1.3.6.1.4.1.7165.2.1.63 NAME 'sambaLockoutDuration' DESC 'Lockout duration in minutes (default: 30, -1 => forever)' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )
+# "reset count minutes"
+attributeTypes: ( 1.3.6.1.4.1.7165.2.1.64 NAME 'sambaLockoutObservationWindow' DESC 'Reset time after lockout in minutes (default: 30)' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )
+# "bad lockout attempt"
+attributeTypes: ( 1.3.6.1.4.1.7165.2.1.65 NAME 'sambaLockoutThreshold' DESC 'Lockout users after bad logon attempts (default: 0 => off)' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )
+# "disconnect time"
+attributeTypes: ( 1.3.6.1.4.1.7165.2.1.66 NAME 'sambaForceLogoff' DESC 'Disconnect Users outside logon hours (default: -1 => off, 0 => on)' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )
+# "refuse machine password change"
+attributeTypes: ( 1.3.6.1.4.1.7165.2.1.67 NAME 'sambaRefuseMachinePwdChange' DESC 'Allow Machine Password changes (default: 0 => off)' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )
+##
+#######################################################################
+## objectClasses: used by Samba 3.0 schema ##
+#######################################################################
+##
+## The X.500 data model (and therefore LDAPv3) says that each entry can
+## only have one structural objectClasses. OpenLDAP 2.0 does not enforce
+## this currently but will in v2.1
+##
+## added new objectClasses: (and OID) for 3.0 to help us deal with backwards
+## compatibility with 2.2 installations (e.g. ldapsam_compat) --jerry
+##
+objectClasses: ( 1.3.6.1.4.1.7165.2.2.6 NAME 'sambaSamAccount' SUP top AUXILIARY DESC 'Samba 3.0 Auxilary SAM Account' MUST ( uid $ sambaSID ) MAY ( cn $ sambaLMPassword $ sambaNTPassword $ sambaPwdLastSet $ sambaLogonTime $ sambaLogoffTime $ sambaKickoffTime $ sambaPwdCanChange $ sambaPwdMustChange $ sambaAcctFlags $ displayName $ sambaHomePath $ sambaHomeDrive $ sambaLogonScript $ sambaProfilePath $ description $ sambaUserWorkstations $ sambaPrimaryGroupSID $ sambaDomainName $ sambaMungedDial $ sambaBadPasswordCount $ sambaBadPasswordTime $ sambaPasswordHistory $ sambaLogonHours))
+##
+## Group mapping info
+##
+objectClasses: ( 1.3.6.1.4.1.7165.2.2.4 NAME 'sambaGroupMapping' SUP top AUXILIARY DESC 'Samba Group Mapping' MUST ( gidNumber $ sambaSID $ sambaGroupType ) MAY ( displayName $ description $ sambaSIDList ))
+##
+## Trust password for trust relationships (any kind)
+##
+objectClasses: ( 1.3.6.1.4.1.7165.2.2.14 NAME 'sambaTrustPassword' SUP top STRUCTURAL DESC 'Samba Trust Password' MUST ( sambaDomainName $ sambaNTPassword $ sambaTrustFlags ) MAY ( sambaSID $ sambaPwdLastSet ))
+##
+## Whole-of-domain info
+##
+objectClasses: ( 1.3.6.1.4.1.7165.2.2.5 NAME 'sambaDomain' SUP top STRUCTURAL DESC 'Samba Domain Information' MUST ( sambaDomainName $ sambaSID ) MAY ( sambaNextRid $ sambaNextGroupRid $ sambaNextUserRid $ sambaAlgorithmicRidBase $ sambaMinPwdLength $ sambaPwdHistoryLength $ sambaLogonToChgPwd $ sambaMaxPwdAge $ sambaMinPwdAge $ sambaLockoutDuration $ sambaLockoutObservationWindow $ sambaLockoutThreshold $ sambaForceLogoff $ sambaRefuseMachinePwdChange ))
+##
+## used for idmap_ldap module
+##
+objectClasses: ( 1.3.6.1.4.1.7165.2.2.7 NAME 'sambaUnixIdPool' SUP top AUXILIARY DESC 'Pool for allocating UNIX uids/gids' MUST ( uidNumber $ gidNumber ) )
+objectClasses: ( 1.3.6.1.4.1.7165.2.2.8 NAME 'sambaIdmapEntry' SUP top AUXILIARY DESC 'Mapping from a SID to an ID' MUST ( sambaSID ) MAY ( uidNumber $ gidNumber ) )
+objectClasses: ( 1.3.6.1.4.1.7165.2.2.9 NAME 'sambaSidEntry' SUP top STRUCTURAL DESC 'Structural Class for a SID' MUST ( sambaSID ) )
+objectClasses: ( 1.3.6.1.4.1.7165.2.2.10 NAME 'sambaConfig' SUP top AUXILIARY DESC 'Samba Configuration Section' MAY ( description ) )
+objectClasses: ( 1.3.6.1.4.1.7165.2.2.11 NAME 'sambaShare' SUP top STRUCTURAL DESC 'Samba Share Section' MUST ( sambaShareName ) MAY ( description ) )
+objectClasses: ( 1.3.6.1.4.1.7165.2.2.12 NAME 'sambaConfigOption' SUP top STRUCTURAL DESC 'Samba Configuration Option' MUST ( sambaOptionName ) MAY ( sambaBoolOption $ sambaIntegerOption $ sambaStringOption $ sambaStringListoption $ description ) )
+## retired during privilege rewrite
+##objectClasses: ( 1.3.6.1.4.1.7165.2.2.13 NAME 'sambaPrivilege' SUP top AUXILIARY
+## DESC 'Samba Privilege'
+## MUST ( sambaSID )
+## MAY ( sambaPrivilegeList ) )
diff --git a/ipa-server/ipa-install/share/Makefile.am b/ipa-server/ipa-install/share/Makefile.am
new file mode 100644
index 00000000..6be2e13d
--- /dev/null
+++ b/ipa-server/ipa-install/share/Makefile.am
@@ -0,0 +1,39 @@
+NULL =
+
+appdir = $(IPA_DATA_DIR)
+app_DATA = \
+ 60kerberos.ldif \
+ 60samba.ldif \
+ 60radius.ldif \
+ 60ipaconfig.ldif \
+ bootstrap-template.ldif \
+ default-aci.ldif \
+ default-keytypes.ldif \
+ kerberos.ldif \
+ indices.ldif \
+ bind.named.conf.template \
+ bind.zone.db.template \
+ certmap.conf.template \
+ kdc.conf.template \
+ krb5.conf.template \
+ krb5.ini.template \
+ krb.con.template \
+ krbrealm.con.template \
+ ntp.conf.server.template \
+ ntpd.sysconfig.template \
+ preferences.html.template \
+ referint-conf.ldif \
+ dna-posix.ldif \
+ master-entry.ldif \
+ memberof-task.ldif \
+ unique-attributes.ldif \
+ schema_compat.uldif \
+ $(NULL)
+
+EXTRA_DIST = \
+ $(app_DATA) \
+ $(NULL)
+
+MAINTAINERCLEANFILES = \
+ *~ \
+ Makefile.in
diff --git a/ipa-server/ipa-install/share/bind.named.conf.template b/ipa-server/ipa-install/share/bind.named.conf.template
new file mode 100644
index 00000000..c1d2817e
--- /dev/null
+++ b/ipa-server/ipa-install/share/bind.named.conf.template
@@ -0,0 +1,41 @@
+options {
+ /* make named use port 53 for the source of all queries, to allow
+ * firewalls to block all ports except 53:
+ */
+ query-source port 53;
+ query-source-v6 port 53;
+
+ // Put files that named is allowed to write in the data/ directory:
+ directory "/var/named"; // the default
+ dump-file "data/cache_dump.db";
+ statistics-file "data/named_stats.txt";
+ memstatistics-file "data/named_mem_stats.txt";
+
+ /* Not used yet, support only on very recent bind versions */
+# tkey-gssapi-credential "DNS/$FQDN";
+# tkey-domain "$REALM";
+};
+
+logging {
+/* If you want to enable debugging, eg. using the 'rndc trace' command,
+ * By default, SELinux policy does not allow named to modify the /var/named directory,
+ * so put the default debug log file in data/ :
+ */
+ channel default_debug {
+ file "data/named.run";
+ severity dynamic;
+ };
+};
+
+zone "." IN {
+ type hint;
+ file "named.ca";
+};
+
+include "/etc/named.rfc1912.zones";
+
+zone "$DOMAIN" {
+ type master;
+ file "$DOMAIN.zone.db";
+};
+
diff --git a/ipa-server/ipa-install/share/bind.zone.db.template b/ipa-server/ipa-install/share/bind.zone.db.template
new file mode 100644
index 00000000..aca7d2d2
--- /dev/null
+++ b/ipa-server/ipa-install/share/bind.zone.db.template
@@ -0,0 +1,28 @@
+$$ORIGIN $DOMAIN.
+$$TTL 86400
+@ IN SOA $DOMAIN. root.$DOMAIN. (
+ 01 ; serial
+ 3H ; refresh
+ 15M ; retry
+ 1W ; expiry
+ 1D ) ; minimum
+
+ IN NS $HOST
+$HOST IN A $IP
+;
+; ldap servers
+_ldap._tcp IN SRV 0 100 389 $HOST
+
+;kerberos realm
+_kerberos IN TXT $REALM
+
+; kerberos servers
+_kerberos._tcp IN SRV 0 100 88 $HOST
+_kerberos._udp IN SRV 0 100 88 $HOST
+_kerberos-master._tcp IN SRV 0 100 88 $HOST
+_kerberos-master._udp IN SRV 0 100 88 $HOST
+_kpasswd._tcp IN SRV 0 100 464 $HOST
+_kpasswd._udp IN SRV 0 100 464 $HOST
+
+;ntp server
+_ntp._udp IN SRV 0 100 123 $HOST
diff --git a/ipa-server/ipa-install/share/bootstrap-template.ldif b/ipa-server/ipa-install/share/bootstrap-template.ldif
new file mode 100644
index 00000000..eb69ae4d
--- /dev/null
+++ b/ipa-server/ipa-install/share/bootstrap-template.ldif
@@ -0,0 +1,202 @@
+dn: cn=accounts,$SUFFIX
+changetype: add
+objectClass: top
+objectClass: nsContainer
+objectClass: krbPwdPolicy
+cn: accounts
+krbMinPwdLife: 3600
+krbPwdMinDiffChars: 0
+krbPwdMinLength: 8
+krbPwdHistoryLength: 0
+krbMaxPwdLife: 7776000
+
+dn: cn=users,cn=accounts,$SUFFIX
+changetype: add
+objectClass: top
+objectClass: nsContainer
+cn: users
+
+dn: cn=groups,cn=accounts,$SUFFIX
+changetype: add
+objectClass: top
+objectClass: nsContainer
+cn: groups
+
+dn: cn=services,cn=accounts,$SUFFIX
+changetype: add
+objectClass: top
+objectClass: nsContainer
+cn: services
+
+dn: cn=computers,cn=accounts,$SUFFIX
+changetype: add
+objectClass: top
+objectClass: nsContainer
+cn: computers
+
+dn: cn=etc,$SUFFIX
+changetype: add
+objectClass: nsContainer
+objectClass: top
+cn: etc
+
+dn: cn=sysaccounts,cn=etc,$SUFFIX
+changetype: add
+objectClass: nsContainer
+objectClass: top
+cn: sysaccounts
+
+dn: cn=ipa,cn=etc,$SUFFIX
+changetype: add
+objectClass: nsContainer
+objectClass: top
+cn: ipa
+
+dn: cn=masters,cn=ipa,cn=etc,$SUFFIX
+changetype: add
+objectClass: nsContainer
+objectClass: top
+cn: masters
+
+dn: uid=admin,cn=users,cn=accounts,$SUFFIX
+changetype: add
+objectClass: top
+objectClass: person
+objectClass: posixAccount
+objectClass: KrbPrincipalAux
+objectClass: inetUser
+uid: admin
+krbPrincipalName: admin@$REALM
+cn: Administrator
+sn: Administrator
+uidNumber: 999
+gidNumber: 1001
+homeDirectory: /home/admin
+loginShell: /bin/bash
+gecos: Administrator
+nsAccountLock: False
+
+dn: cn=radius,$SUFFIX
+changetype: add
+objectClass: nsContainer
+objectClass: top
+cn: radius
+
+dn: cn=clients,cn=radius,$SUFFIX
+changetype: add
+objectClass: nsContainer
+objectClass: top
+cn: clients
+
+dn: cn=profiles,cn=radius,$SUFFIX
+changetype: add
+objectClass: nsContainer
+objectClass: top
+cn: profiles
+
+dn: uid=ipa_default, cn=profiles,cn=radius,$SUFFIX
+changetype: add
+objectClass: top
+objectClass: radiusprofile
+uid: ipa_default
+
+dn: cn=admins,cn=groups,cn=accounts,$SUFFIX
+changetype: add
+objectClass: top
+objectClass: groupofnames
+objectClass: posixGroup
+cn: admins
+description: Account administrators group
+gidNumber: 1001
+member: uid=admin,cn=users,cn=accounts,$SUFFIX
+nsAccountLock: False
+
+dn: cn=ipausers,cn=groups,cn=accounts,$SUFFIX
+changetype: add
+objectClass: top
+objectClass: groupofnames
+objectClass: posixGroup
+gidNumber: 1002
+description: Default group for all users
+cn: ipausers
+
+dn: cn=editors,cn=groups,cn=accounts,$SUFFIX
+changetype: add
+objectClass: top
+objectClass: groupofnames
+objectClass: posixGroup
+gidNumber: 1003
+description: Limited admins who can edit other users
+cn: editors
+
+dn: cn=ipaConfig,cn=etc,$SUFFIX
+changetype: add
+objectClass: nsContainer
+objectClass: top
+objectClass: ipaGuiConfig
+ipaUserSearchFields: uid,givenName,sn,telephoneNumber,ou,title
+ipaGroupSearchFields: cn,description
+ipaSearchTimeLimit: 2
+ipaSearchRecordsLimit: 0
+ipaHomesRootDir: /home
+ipaDefaultLoginShell: /bin/sh
+ipaDefaultPrimaryGroup: ipausers
+ipaMaxUsernameLength: 8
+ipaPwdExpAdvNotify: 4
+ipaGroupObjectClasses: top
+ipaGroupObjectClasses: groupofnames
+ipaGroupObjectClasses: posixGroup
+ipaGroupObjectClasses: inetUser
+ipaUserObjectClasses: top
+ipaUserObjectClasses: person
+ipaUserObjectClasses: organizationalPerson
+ipaUserObjectClasses: inetOrgPerson
+ipaUserObjectClasses: inetUser
+ipaUserObjectClasses: posixAccount
+ipaUserObjectClasses: krbPrincipalAux
+ipaUserObjectClasses: radiusprofile
+ipaDefaultEmailDomain: $DOMAIN
+
+dn: cn=account inactivation,cn=accounts,$SUFFIX
+changetype: add
+description: Lock accounts based on group membership
+objectClass: top
+objectClass: ldapsubentry
+objectClass: cosSuperDefinition
+objectClass: cosClassicDefinition
+cosTemplateDn: cn=cosTemplates,cn=accounts,$SUFFIX
+cosAttribute: nsAccountLock operational
+cosSpecifier: memberOf
+cn: Account Inactivation
+
+dn: cn=cosTemplates,cn=accounts,$SUFFIX
+changetype: add
+objectclass: top
+objectclass: nsContainer
+cn: cosTemplates
+
+dn: cn="cn=inactivated,cn=account inactivation,cn=accounts,$SUFFIX", cn=cosTemplates,cn=accounts,$SUFFIX
+changetype: add
+objectClass: top
+objectClass: cosTemplate
+objectClass: extensibleobject
+nsAccountLock: true
+cosPriority: 1
+
+dn: cn=inactivated,cn=account inactivation,cn=accounts,$SUFFIX
+changetype: add
+objectclass: top
+objectclass: groupofnames
+
+dn: cn="cn=activated,cn=account inactivation,cn=accounts,$SUFFIX", cn=cosTemplates,cn=accounts,$SUFFIX
+changetype: add
+objectClass: top
+objectClass: cosTemplate
+objectClass: extensibleobject
+nsAccountLock: false
+cosPriority: 0
+
+dn: cn=Activated,cn=Account Inactivation,cn=accounts,$SUFFIX
+changetype: add
+objectclass: top
+objectclass: groupofnames
diff --git a/ipa-server/ipa-install/share/certmap.conf.template b/ipa-server/ipa-install/share/certmap.conf.template
new file mode 100644
index 00000000..676d3ef3
--- /dev/null
+++ b/ipa-server/ipa-install/share/certmap.conf.template
@@ -0,0 +1,82 @@
+#
+# BEGIN COPYRIGHT BLOCK
+# This Program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free Software
+# Foundation; version 2 of the License.
+#
+# This Program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this Program; if not, write to the Free Software Foundation, Inc., 59 Temple
+# Place, Suite 330, Boston, MA 02111-1307 USA.
+#
+# In addition, as a special exception, Red Hat, Inc. gives You the additional
+# right to link the code of this Program with code not covered under the GNU
+# General Public License ("Non-GPL Code") and to distribute linked combinations
+# including the two, subject to the limitations in this paragraph. Non-GPL Code
+# permitted under this exception must only link to the code of this Program
+# through those well defined interfaces identified in the file named EXCEPTION
+# found in the source code files (the "Approved Interfaces"). The files of
+# Non-GPL Code may instantiate templates or use macros or inline functions from
+# the Approved Interfaces without causing the resulting work to be covered by
+# the GNU General Public License. Only Red Hat, Inc. may make changes or
+# additions to the list of Approved Interfaces. You must obey the GNU General
+# Public License in all respects for all of the Program code and other code used
+# in conjunction with the Program except the Non-GPL Code covered by this
+# exception. If you modify this file, you may extend this exception to your
+# version of the file, but you are not obligated to do so. If you do not wish to
+# provide this exception without modification, you must delete this exception
+# statement from your version and license this file solely under the GPL without
+# exception.
+#
+#
+# Copyright (C) 2001 Sun Microsystems, Inc. Used by permission.
+# Copyright (C) 2005 Red Hat, Inc.
+# All rights reserved.
+# END COPYRIGHT BLOCK
+#
+#
+# This file configures how a certificate is mapped to an LDAP entry. See the
+# documentation for more information on this file.
+#
+# The format of this file is as follows:
+# certmap <name> <issuerDN>
+# <name>:<prop1> [<val1>]
+# <name>:<prop2> [<val2>]
+#
+# Notes:
+#
+# 1. Mapping can be defined per issuer of a certificate. If mapping doesn't
+# exists for a particular 'issuerDN' then the server uses the default
+# mapping.
+#
+# 2. There must be an entry for <name>=default and issuerDN "default".
+# This mapping is the default mapping.
+#
+# 3. '#' can be used to comment out a line.
+#
+# 4. DNComps & FilterComps are used to form the base DN and filter resp. for
+# performing an LDAP search while mapping the cert to a user entry.
+#
+# 5. DNComps can be one of the following:
+# commented out - take the user's DN from the cert as is
+# empty - search the entire LDAP tree (DN == suffix)
+# attr names - a comma separated list of attributes to form DN
+#
+# 6. FilterComps can be one of the following:
+# commented out - set the filter to "objectclass=*"
+# empty - set the filter to "objectclass=*"
+# attr names - a comma separated list of attributes to form the filter
+#
+
+certmap default default
+#default:DNComps
+#default:FilterComps e, uid
+#default:verifycert on
+#default:CmapLdapAttr certSubjectDN
+#default:library <path_to_shared_lib_or_dll>
+#default:InitFn <Init function's name>
+default:DNComps
+default:FilterComps uid
diff --git a/ipa-server/ipa-install/share/default-aci.ldif b/ipa-server/ipa-install/share/default-aci.ldif
new file mode 100644
index 00000000..25bd3b22
--- /dev/null
+++ b/ipa-server/ipa-install/share/default-aci.ldif
@@ -0,0 +1,38 @@
+# $SUFFIX (base entry)
+# FIXME: We need to allow truly anonymous access only to NIS data for older clients. We need to allow broad access to most attributes only to authenticated users
+dn: $SUFFIX
+changetype: modify
+add: aci
+aci: (targetattr != "userPassword || krbPrincipalKey || sambaLMPassword || sambaNTPassword || passwordHistory || krbMKey")(version 3.0; acl "Enable Anonymous access"; allow (read, search, compare) userdn = "ldap:///anyone";)
+aci: (targetattr != "userPassword || krbPrincipalKey || sambaLMPassword || sambaNTPassword || passwordHistory || krbMKey")(version 3.0; acl "Admin can manage any entry"; allow (all) userdn = "ldap:///uid=admin,cn=users,cn=accounts,$SUFFIX";)
+aci: (targetattr = "userPassword || krbPrincipalKey || sambaLMPassword || sambaNTPassword")(version 3.0; acl "Self can write own password"; allow (write) userdn="ldap:///self";)
+aci: (targetattr = "userPassword || krbPrincipalKey || sambaLMPassword || sambaNTPassword || passwordHistory")(version 3.0; acl "Admins can write passwords"; allow (add,delete,write) groupdn="ldap:///cn=admins,cn=groups,cn=accounts,$SUFFIX";)
+aci: (targetattr = "userPassword || krbPrincipalKey || sambaLMPassword || sambaNTPassword || passwordHistory")(version 3.0; acl "Password change service can read/write passwords"; allow (read, write) userdn="ldap:///krbprincipalname=kadmin/changepw@$REALM,cn=$REALM,cn=kerberos,$SUFFIX";)
+aci: (targetattr = "userPassword || krbPrincipalKey || sambaLMPassword || sambaNTPassword || passwordHistory")(version 3.0; acl "KDC System Account can access passwords"; allow (all) userdn="ldap:///uid=kdc,cn=sysaccounts,cn=etc,$SUFFIX";)
+aci: (targetattr = "krbLastSuccessfulAuth || krbLastFailedAuth || krbLoginFailedCount")(version 3.0; acl "KDC System Account can update some fields"; allow (write) userdn="ldap:///uid=kdc,cn=sysaccounts,cn=etc,$SUFFIX";)
+aci: (targetattr = "krbPrincipalName || krbUPEnabled || krbMKey || krbTicketPolicyReference || krbPrincipalExpiration || krbPasswordExpiration || krbPwdPolicyReference || krbPrincipalType || krbPwdHistory || krbLastPwdChange || krbPrincipalAliases || krbExtraData || krbLastSuccessfulAuth || krbLastFailedAuth || krbLoginFailedCount")(version 3.0; acl "Only the KDC System Account has access to kerberos material"; allow (read, search, compare) userdn="ldap:///uid=kdc,cn=sysaccounts,cn=etc,$SUFFIX";)
+aci: (targetfilter = "(|(objectClass=person)(objectClass=krbPrincipalAux)(objectClass=posixAccount)(objectClass=groupOfNames)(objectClass=posixGroup))")(targetattr != "aci || userPassword || krbPrincipalKey || sambaLMPassword || sambaNTPassword || passwordHistory")(version 3.0; acl "Account Admins can manage Users and Groups"; allow (add, delete, read, write) groupdn = "ldap:///cn=admins,cn=groups,cn=accounts,$SUFFIX";)
+aci: (targetfilter = "(objectClass=krbPwdPolicy)")(targetattr = "krbMaxPwdLife || krbMinPwdLife || krbPwdMinDiffChars || krbPwdMinLength || krbPwdHistoryLength")(version 3.0;acl "Admins can write password policies"; allow (read, search, compare, write) groupdn = "ldap:///cn=admins,cn=groups,cn=accounts,$SUFFIX";)
+aci: (targetattr = "givenName || sn || cn || displayName || title || initials || loginShell || gecos || homePhone || mobile || pager || facsimileTelephoneNumber || telephoneNumber || street || roomNumber || l || st || postalCode || manager || secretary || description || carLicense || labeledURI || inetUserHTTPURL || seeAlso || employeeType || businessCategory || ou")(version 3.0;acl "Self service";allow (write) userdn = "ldap:///self";)
+
+dn: cn=ipaConfig,cn=etc,$SUFFIX
+changetype: modify
+add: aci
+aci: (targetfilter = "(objectClass=ipaGuiConfig)")(targetattr != "aci")(version 3.0;acl "Admins can change GUI config"; allow (read, search, compare, write) groupdn = "ldap:///cn=admins,cn=groups,cn=accounts,$SUFFIX";)
+
+dn: cn=accounts,$SUFFIX
+changetype: modify
+add: aci
+aci: (targetattr = "krbMaxPwdLife || krbMinPwdLife || krbPwdMinDiffChars || krbPwdMinLength || krbPwdHistoryLength")(version 3.0;acl "Admins can write password policy"; allow (write) groupdn="ldap:///cn=admins,cn=groups,cn=accounts,$SUFFIX";)
+aci: (targetattr = "aci")(version 3.0;acl "Admins can manage delegations"; allow (write, delete) groupdn="ldap:///cn=admins,cn=groups,cn=accounts,$SUFFIX";)
+
+dn: cn=radius,$SUFFIX
+changetype: modify
+add: aci
+aci: (targetattr = "*")(version 3.0; acl "Only radius and admin can access radius service data"; deny (all) userdn!="ldap:///uid=admin,cn=users,cn=accounts,$SUFFIX || ldap:///krbprincipalname=radius/$FQDN@$REALM,cn=$REALM,cn=kerberos,$SUFFIX";)
+aci: (targetfilter = "(objectClass=radiusprofile)")(targetattr != "aci || userPassword || krbPrincipalKey || sambaLMPassword || sambaNTPassword || passwordHistory")(version 3.0; acl "Account Admins can manage Users and Groups"; allow (add, delete, read, write) groupdn = "ldap:///cn=admins,cn=groups,cn=accounts,$SUFFIX";)
+
+dn: cn=services,cn=accounts,$SUFFIX
+changetype: modify
+add: aci
+aci: (targetattr="krbPrincipalName || krbUPEnabled || krbPrincipalKey || krbTicketPolicyReference || krbPrincipalExpiration || krbPasswordExpiration || krbPwdPolicyReference || krbPrincipalType || krbPwdHistory || krbLastPwdChange || krbPrincipalAliases || krbExtraData")(version 3.0; acl "KDC System Account"; allow (read, search, compare, write) userdn="ldap:///uid=kdc,cn=sysaccounts,cn=etc,$SUFFIX";)
diff --git a/ipa-server/ipa-install/share/default-keytypes.ldif b/ipa-server/ipa-install/share/default-keytypes.ldif
new file mode 100644
index 00000000..1d54a059
--- /dev/null
+++ b/ipa-server/ipa-install/share/default-keytypes.ldif
@@ -0,0 +1,25 @@
+#kerberos keytypes
+dn: cn=$REALM,cn=kerberos,$SUFFIX
+changetype: modify
+add: krbSupportedEncSaltTypes
+krbSupportedEncSaltTypes: aes256-cts:normal
+krbSupportedEncSaltTypes: aes128-cts:normal
+krbSupportedEncSaltTypes: des3-hmac-sha1:normal
+krbSupportedEncSaltTypes: arcfour-hmac:normal
+krbSupportedEncSaltTypes: des-hmac-sha1:normal
+krbSupportedEncSaltTypes: des-cbc-md5:normal
+krbSupportedEncSaltTypes: des-cbc-crc:normal
+krbSupportedEncSaltTypes: des-cbc-crc:v4
+krbSupportedEncSaltTypes: des-cbc-crc:afs3
+
+#kerberos keytypes
+dn: cn=$REALM,cn=kerberos,$SUFFIX
+changetype: modify
+add: krbDefaultEncSaltTypes
+krbDefaultEncSaltTypes: aes256-cts:normal
+krbDefaultEncSaltTypes: aes128-cts:normal
+krbDefaultEncSaltTypes: des3-hmac-sha1:normal
+krbDefaultEncSaltTypes: arcfour-hmac:normal
+krbDefaultEncSaltTypes: des-hmac-sha1:normal
+krbDefaultEncSaltTypes: des-cbc-md5:normal
+
diff --git a/ipa-server/ipa-install/share/dna-posix.ldif b/ipa-server/ipa-install/share/dna-posix.ldif
new file mode 100644
index 00000000..a8848545
--- /dev/null
+++ b/ipa-server/ipa-install/share/dna-posix.ldif
@@ -0,0 +1,39 @@
+# add container for posix configuration
+
+dn: cn=Posix,cn=ipa-dna,cn=plugins,cn=config
+changetype: add
+objectclass: top
+objectclass: nsContainer
+objectclass: extensibleObject
+cn: Posix
+
+# add plugin configuration for posix users
+
+dn: cn=Accounts,cn=Posix,cn=ipa-dna,cn=plugins,cn=config
+changetype: add
+objectclass: top
+objectclass: extensibleObject
+cn: Accounts
+dnaType: uidNumber
+dnaNextValue: 1100
+dnaInterval: 1
+dnaMaxValue: 1000000000
+dnaMagicRegen: 999
+dnaFilter: (objectclass=posixAccount)
+dnaScope: $SUFFIX
+
+# add plugin configuration for posix groups
+
+dn: cn=Groups,cn=Posix,cn=ipa-dna,cn=plugins,cn=config
+changetype: add
+objectclass: top
+objectclass: extensibleObject
+cn: Groups
+dnaType: gidNumber
+dnaNextValue: 1100
+dnaInterval: 1
+dnaMaxValue: 1000000000
+dnaMagicRegen: 999
+dnaFilter: (objectclass=posixGroup)
+dnaScope: $SUFFIX
+
diff --git a/ipa-server/ipa-install/share/encrypted_attribute.ldif b/ipa-server/ipa-install/share/encrypted_attribute.ldif
new file mode 100644
index 00000000..3f5e1b43
--- /dev/null
+++ b/ipa-server/ipa-install/share/encrypted_attribute.ldif
@@ -0,0 +1,6 @@
+dn: cn=$ENCRYPTED_ATTRIBUTE, cn=encrypted attributes, cn=userRoot, cn=ldbm database, cn=plugins, cn=config
+changetype: add
+objectClass: top
+objectClass: nsAttributeEncryption
+cn: $ENCRYPTED_ATTRIBUTE
+nsEncryptionAlgorithm: AES
diff --git a/ipa-server/ipa-install/share/fedora-ds.init.patch b/ipa-server/ipa-install/share/fedora-ds.init.patch
new file mode 100644
index 00000000..865611d9
--- /dev/null
+++ b/ipa-server/ipa-install/share/fedora-ds.init.patch
@@ -0,0 +1,12 @@
+--- /etc/init.d/dirsrv.orig 2007-07-06 18:21:30.000000000 -0400
++++ /etc/init.d/dirsrv 2007-05-18 19:36:24.000000000 -0400
+@@ -10,6 +10,9 @@
+ # datadir: /var/lib/dirsrv/slapd-<instance name>
+ #
+
++# Get config.
++[ -r /etc/sysconfig/dirsrv ] && . /etc/sysconfig/dirsrv
++
+ # Source function library.
+ if [ -f /etc/rc.d/init.d/functions ] ; then
+ . /etc/rc.d/init.d/functions
diff --git a/ipa-server/ipa-install/share/indices.ldif b/ipa-server/ipa-install/share/indices.ldif
new file mode 100644
index 00000000..05c27655
--- /dev/null
+++ b/ipa-server/ipa-install/share/indices.ldif
@@ -0,0 +1,93 @@
+dn: cn=krbPrincipalName,cn=index,cn=userRoot,cn=ldbm database,cn=plugins,cn=config
+changetype: add
+objectClass:top
+objectClass:nsIndex
+cn:krbPrincipalName
+nsSystemIndex:false
+nsIndexType:eq
+nsIndexType:sub
+
+dn: cn=ou,cn=index,cn=userRoot,cn=ldbm database,cn=plugins,cn=config
+changetype: add
+objectClass:top
+objectClass:nsIndex
+cn:ou
+nsSystemIndex:false
+nsIndexType:eq
+nsIndexType:sub
+
+dn: cn=carLicense,cn=index,cn=userRoot,cn=ldbm database,cn=plugins,cn=config
+changetype: add
+objectClass:top
+objectClass:nsIndex
+cn:carLicense
+nsSystemIndex:false
+nsIndexType:eq
+nsIndexType:sub
+
+dn: cn=title,cn=index,cn=userRoot,cn=ldbm database,cn=plugins,cn=config
+changetype: add
+objectClass:top
+objectClass:nsIndex
+cn:title
+nsSystemIndex:false
+nsIndexType:eq
+nsIndexType:sub
+
+dn: cn=manager,cn=index,cn=userRoot,cn=ldbm database,cn=plugins,cn=config
+changetype: add
+objectClass:top
+objectClass:nsIndex
+cn:manager
+nsSystemIndex:false
+nsIndexType:eq
+
+dn: cn=secretary,cn=index,cn=userRoot,cn=ldbm database,cn=plugins,cn=config
+changetype: add
+objectClass:top
+objectClass:nsIndex
+cn:secretary
+nsSystemIndex:false
+nsIndexType:eq
+
+dn: cn=displayname,cn=index,cn=userRoot,cn=ldbm database,cn=plugins,cn=config
+changetype: add
+objectClass:top
+objectClass:nsIndex
+cn:displayname
+nsSystemIndex:false
+nsIndexType:eq
+nsIndexType:sub
+
+dn: cn=uid,cn=index,cn=userRoot,cn=ldbm database,cn=plugins,cn=config
+changetype: modify
+add: nsIndexType
+nsIndexType:sub
+
+dn: cn=uidnumber,cn=index,cn=userRoot,cn=ldbm database,cn=plugins,cn=config
+changetype: add
+objectClass:top
+objectClass:nsIndex
+cn:uidnumber
+nsSystemIndex:false
+nsIndexType:eq
+nsMatchingRule: integerOrderingMatch
+
+dn: cn=gidnumber,cn=index,cn=userRoot,cn=ldbm database,cn=plugins,cn=config
+changetype: add
+objectClass:top
+objectClass:nsIndex
+cn:gidnumber
+nsSystemIndex:false
+nsIndexType:eq
+nsMatchingRule: integerOrderingMatch
+
+dn: cn=ntUniqueId,cn=index,cn=userRoot,cn=ldbm database,cn=plugins,cn=config
+changetype: modify
+replace: nsIndexType
+nsIndexType: eq,pres
+
+dn: cn=ntUserDomainId,cn=index,cn=userRoot,cn=ldbm database,cn=plugins,cn=config
+changetype: modify
+replace: nsIndexType
+nsIndexType: eq,pres
diff --git a/ipa-server/ipa-install/share/kdc.conf.template b/ipa-server/ipa-install/share/kdc.conf.template
new file mode 100644
index 00000000..db088840
--- /dev/null
+++ b/ipa-server/ipa-install/share/kdc.conf.template
@@ -0,0 +1,14 @@
+[kdcdefaults]
+ kdc_ports = 88
+
+[realms]
+ $REALM = {
+ master_key_type = des3-hmac-sha1
+ supported_enctypes = aes256-cts:normal aes128-cts:normal des3-hmac-sha1:normal arcfour-hmac:normal des-hmac-sha1:normal des-cbc-md5:normal des-cbc-crc:normal des-cbc-crc:v4 des-cbc-crc:afs3
+ max_life = 7d
+ max_renewable_life = 14d
+ acl_file = /var/kerberos/krb5kdc/kadm5.acl
+ dict_file = /usr/share/dict/words
+ default_principal_flags = +preauth
+; admin_keytab = /var/kerberos/krb5kdc/kadm5.keytab
+ }
diff --git a/ipa-server/ipa-install/share/kerberos.ldif b/ipa-server/ipa-install/share/kerberos.ldif
new file mode 100644
index 00000000..f1b651d5
--- /dev/null
+++ b/ipa-server/ipa-install/share/kerberos.ldif
@@ -0,0 +1,16 @@
+#kerberos user
+dn: uid=kdc,cn=sysaccounts,cn=etc,$SUFFIX
+changetype: add
+objectclass: account
+objectclass: simplesecurityobject
+uid: kdc
+userPassword: $PASSWORD
+
+#kerberos base object
+dn: cn=kerberos,$SUFFIX
+changetype: add
+objectClass: krbContainer
+objectClass: top
+cn: kerberos
+aci: (targetattr="*")(version 3.0; acl "KDC System Account"; allow (all) userdn= "ldap:///uid=kdc,cn=sysaccounts,cn=etc,$SUFFIX";)
+
diff --git a/ipa-server/ipa-install/share/krb.con.template b/ipa-server/ipa-install/share/krb.con.template
new file mode 100644
index 00000000..d75a8f60
--- /dev/null
+++ b/ipa-server/ipa-install/share/krb.con.template
@@ -0,0 +1,2 @@
+$REALM $DOMAIN
+$REALM $DOMAIN admin server
diff --git a/ipa-server/ipa-install/share/krb5.conf.template b/ipa-server/ipa-install/share/krb5.conf.template
new file mode 100644
index 00000000..b81cedfe
--- /dev/null
+++ b/ipa-server/ipa-install/share/krb5.conf.template
@@ -0,0 +1,42 @@
+[logging]
+ default = FILE:/var/log/krb5libs.log
+ kdc = FILE:/var/log/krb5kdc.log
+ admin_server = FILE:/var/log/kadmind.log
+
+[libdefaults]
+ default_realm = $REALM
+ dns_lookup_realm = true
+ dns_lookup_kdc = true
+ ticket_lifetime = 24h
+ forwardable = yes
+
+[realms]
+ $REALM = {
+ kdc = $FQDN:88
+ admin_server = $FQDN:749
+ default_domain = $DOMAIN
+}
+
+[domain_realm]
+ .$DOMAIN = $REALM
+ $DOMAIN = $REALM
+
+[appdefaults]
+ pam = {
+ debug = false
+ ticket_lifetime = 36000
+ renew_lifetime = 36000
+ forwardable = true
+ krb4_convert = false
+ }
+
+[dbmodules]
+ $REALM = {
+ db_library = kldap
+ ldap_servers = ldap://127.0.0.1/
+ ldap_kerberos_container_dn = cn=kerberos,$SUFFIX
+ ldap_kdc_dn = uid=kdc,cn=sysaccounts,cn=etc,$SUFFIX
+ ldap_kadmind_dn = uid=kdc,cn=sysaccounts,cn=etc,$SUFFIX
+ ldap_service_password_file = /var/kerberos/krb5kdc/ldappwd
+ }
+
diff --git a/ipa-server/ipa-install/share/krb5.ini.template b/ipa-server/ipa-install/share/krb5.ini.template
new file mode 100644
index 00000000..89f4a370
--- /dev/null
+++ b/ipa-server/ipa-install/share/krb5.ini.template
@@ -0,0 +1,19 @@
+[libdefaults]
+ default_realm = $REALM
+ krb4_config = /usr/kerberos/lib/krb.conf
+ krb4_realms = /usr/kerberos/lib/krb.realms
+ dns_lookup_kdc = true
+
+[realms]
+ $REALM = {
+ admin_server = $FQDN
+ kdc = $FQDN
+ default_domain = $REALM
+ }
+
+[domain_realm]
+ .$DOMAIN = $REALM
+ $DOMAIN = $REALM
+
+[logging]
+# kdc = CONSOLE
diff --git a/ipa-server/ipa-install/share/krbrealm.con.template b/ipa-server/ipa-install/share/krbrealm.con.template
new file mode 100644
index 00000000..c6781386
--- /dev/null
+++ b/ipa-server/ipa-install/share/krbrealm.con.template
@@ -0,0 +1,3 @@
+.$REALM $REALM
+.$REALM. $REALM
+$REALM $REALM
diff --git a/ipa-server/ipa-install/share/master-entry.ldif b/ipa-server/ipa-install/share/master-entry.ldif
new file mode 100644
index 00000000..09c1d44f
--- /dev/null
+++ b/ipa-server/ipa-install/share/master-entry.ldif
@@ -0,0 +1,7 @@
+dn: cn=$FQHN,cn=masters,cn=ipa,cn=etc,$SUFFIX
+changetype: add
+objectclass: top
+objectclass: extensibleObject
+cn: $FQHN
+dnabase: 1100
+dnainterval: 4
diff --git a/ipa-server/ipa-install/share/memberof-task.ldif b/ipa-server/ipa-install/share/memberof-task.ldif
new file mode 100644
index 00000000..827949e3
--- /dev/null
+++ b/ipa-server/ipa-install/share/memberof-task.ldif
@@ -0,0 +1,8 @@
+dn: cn=IPA install $TIME, cn=memberof task, cn=tasks, cn=config
+changetype: add
+objectClass: top
+objectClass: extensibleObject
+cn: IPA install
+basedn: $SUFFIX
+filter: (objectclass=*)
+ttl: 10
diff --git a/ipa-server/ipa-install/share/ntp.conf.server.template b/ipa-server/ipa-install/share/ntp.conf.server.template
new file mode 100644
index 00000000..09149dfc
--- /dev/null
+++ b/ipa-server/ipa-install/share/ntp.conf.server.template
@@ -0,0 +1,50 @@
+# Permit time synchronization with our time source, but do not
+# permit the source to query or modify the service on this system.
+restrict default kod nomodify notrap
+restrict -6 default kod nomodify notrap
+
+# Permit all access over the loopback interface. This could
+# be tightened as well, but to do so would effect some of
+# the administrative functions.
+restrict 127.0.0.1
+restrict -6 ::1
+
+# Hosts on local network are less restricted.
+#restrict 192.168.1.0 mask 255.255.255.0 nomodify notrap
+
+# Use public servers from the pool.ntp.org project.
+# Please consider joining the pool (http://www.pool.ntp.org/join.html).
+server $SERVERA
+server $SERVERB
+server $SERVERC
+
+#broadcast 192.168.1.255 key 42 # broadcast server
+#broadcastclient # broadcast client
+#broadcast 224.0.1.1 key 42 # multicast server
+#multicastclient 224.0.1.1 # multicast client
+#manycastserver 239.255.254.254 # manycast server
+#manycastclient 239.255.254.254 key 42 # manycast client
+
+# Undisciplined Local Clock. This is a fake driver intended for backup
+# and when no outside source of synchronized time is available.
+server 127.127.1.0 # local clock
+#fudge 127.127.1.0 stratum 10
+
+# Drift file. Put this in a directory which the daemon can write to.
+# No symbolic links allowed, either, since the daemon updates the file
+# by creating a temporary in the same directory and then rename()'ing
+# it to the file.
+driftfile /var/lib/ntp/drift
+
+# Key file containing the keys and key identifiers used when operating
+# with symmetric key cryptography.
+keys /etc/ntp/keys
+
+# Specify the key identifiers which are trusted.
+#trustedkey 4 8 42
+
+# Specify the key identifier to use with the ntpdc utility.
+#requestkey 8
+
+# Specify the key identifier to use with the ntpq utility.
+#controlkey 8
diff --git a/ipa-server/ipa-install/share/ntpd.sysconfig.template b/ipa-server/ipa-install/share/ntpd.sysconfig.template
new file mode 100644
index 00000000..3412a0e8
--- /dev/null
+++ b/ipa-server/ipa-install/share/ntpd.sysconfig.template
@@ -0,0 +1,8 @@
+# Drop root to id 'ntp:ntp' by default.
+OPTIONS="-x -u ntp:ntp -p /var/run/ntpd.pid"
+
+# Set to 'yes' to sync hw clock after successful ntpdate
+SYNC_HWCLOCK=yes
+
+# Additional options for ntpdate
+NTPDATE_OPTIONS=""
diff --git a/ipa-server/ipa-install/share/preferences.html.template b/ipa-server/ipa-install/share/preferences.html.template
new file mode 100644
index 00000000..2d3684dc
--- /dev/null
+++ b/ipa-server/ipa-install/share/preferences.html.template
@@ -0,0 +1,33 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
+<html>
+<head>
+ <title>Automatically set browser preferences</title>
+</head>
+<body>
+<form action="undefined" method="get">
+<input type=button onclick="setPreferences()" name="prefs" value="Configure Firefox">
+</form>
+
+<script type="text/javascript">
+function setPreferences() {
+ try {
+ netscape.security.PrivilegeManager.enablePrivilege("UniversalPreferencesWrite");
+ try {
+ navigator.preference("network.negotiate-auth.using-native-gsslib", true)
+ navigator.preference("network.negotiate-auth.delegation-uris", ".$DOMAIN")
+ navigator.preference("network.negotiate-auth.trusted-uris", ".$DOMAIN")
+ navigator.preference("network.negotiate-auth.allow-proxies", true)
+ } catch (e) {
+ alert("Unable to store preferences: " + e)
+ }
+ netscape.security.PrivilegeManager.disablePrivilege("UniversalPreferencesWrite");
+ alert("Successfully configured Firefox for single sign on.")
+ } catch (e) {
+ alert("Unable to apply recommended settings.\n\nClick on the Certificate Authority link and select trust for all, then reload this page and try again.\n\nThe error returned was: " + e);
+ return;
+ }
+}
+</script>
+
+</body>
+</html>
diff --git a/ipa-server/ipa-install/share/referint-conf.ldif b/ipa-server/ipa-install/share/referint-conf.ldif
new file mode 100644
index 00000000..533b97de
--- /dev/null
+++ b/ipa-server/ipa-install/share/referint-conf.ldif
@@ -0,0 +1,11 @@
+dn: cn=referential integrity postoperation,cn=plugins,cn=config
+changetype: modify
+replace: nsslapd-pluginenabled
+nsslapd-pluginenabled: on
+-
+add: nsslapd-pluginArg7
+nsslapd-pluginArg7: manager
+-
+add: nsslapd-pluginArg8
+nsslapd-pluginArg8: secretary
+
diff --git a/ipa-server/ipa-install/share/schema_compat.uldif b/ipa-server/ipa-install/share/schema_compat.uldif
new file mode 100644
index 00000000..71732c99
--- /dev/null
+++ b/ipa-server/ipa-install/share/schema_compat.uldif
@@ -0,0 +1,50 @@
+#
+# Enable the Schema Compatibility plugin provided by slapi-nis.
+#
+# http://slapi-nis.fedorahosted.org/
+#
+dn: cn=Schema Compatibility, cn=plugins, cn=config
+default:objectclass: top
+default:objectclass: nsSlapdPlugin
+default:objectclass: extensibleObject
+default:cn: Schema Compatibility
+default:nsslapd-pluginpath: /usr/lib$LIBARCH/dirsrv/plugins/schemacompat-plugin.so
+default:nsslapd-plugininitfunc: schema_compat_plugin_init
+default:nsslapd-plugintype: object
+default:nsslapd-pluginenabled: on
+default:nsslapd-pluginid: schema-compat-plugin
+default:nsslapd-pluginversion: 0.8
+default:nsslapd-pluginvendor: redhat.com
+default:nsslapd-plugindescription: Schema Compatibility Plugin
+
+dn: cn=users, cn=Schema Compatibility, cn=plugins, cn=config
+default:objectClass: top
+default:objectClass: extensibleObject
+default:cn: users
+default:schema-compat-container-group: cn=compat, $SUFFIX
+default:schema-compat-container-rdn: cn=users
+default:schema-compat-search-base: cn=users, cn=accounts, $SUFFIX
+default:schema-compat-search-filter: objectclass=posixAccount
+default:schema-compat-entry-rdn: uid=%{uid}
+default:schema-compat-entry-attribute: objectclass=posixAccount
+default:schema-compat-entry-attribute: gecos=%{cn}
+default:schema-compat-entry-attribute: cn=%{cn}
+default:schema-compat-entry-attribute: uidNumber=%{uidNumber}
+default:schema-compat-entry-attribute: gidNumber=%{gidNumber}
+default:schema-compat-entry-attribute: loginShell=%{loginShell}
+default:schema-compat-entry-attribute: homeDirectory=%{homeDirectory}
+
+dn: cn=groups, cn=Schema Compatibility, cn=plugins, cn=config
+default:objectClass: top
+default:objectClass: extensibleObject
+default:cn: groups
+default:schema-compat-container-group: cn=compat, $SUFFIX
+default:schema-compat-container-rdn: cn=groups
+default:schema-compat-search-base: cn=groups, cn=accounts, $SUFFIX
+default:schema-compat-search-filter: objectclass=posixGroup
+default:schema-compat-entry-rdn: cn=%{cn}
+default:schema-compat-entry-attribute: objectclass=posixGroup
+default:schema-compat-entry-attribute: gidNumber=%{gidNumber}
+default:schema-compat-entry-attribute: memberUid=%{memberUid}
+default:schema-compat-entry-attribute: memberUid=%deref("member","uid")
+default:schema-compat-entry-attribute: memberUid=%referred("cn=users","memberOf","uid")
diff --git a/ipa-server/ipa-install/share/unique-attributes.ldif b/ipa-server/ipa-install/share/unique-attributes.ldif
new file mode 100644
index 00000000..82ec52d1
--- /dev/null
+++ b/ipa-server/ipa-install/share/unique-attributes.ldif
@@ -0,0 +1,35 @@
+dn: cn=krbPrincipalName uniqueness,cn=plugins,cn=config
+changetype: add
+objectClass: top
+objectClass: nsSlapdPlugin
+objectClass: extensibleObject
+cn: krbPrincipalName uniqueness
+nsslapd-pluginPath: libattr-unique-plugin
+nsslapd-pluginInitfunc: NSUniqueAttr_Init
+nsslapd-pluginType: preoperation
+nsslapd-pluginEnabled: on
+nsslapd-pluginarg0: krbPrincipalName
+nsslapd-pluginarg1: $SUFFIX
+nsslapd-plugin-depends-on-type: database
+nsslapd-pluginId: NSUniqueAttr
+nsslapd-pluginVersion: 1.1.0
+nsslapd-pluginVendor: Fedora Project
+nsslapd-pluginDescription: Enforce unique attribute values
+
+#dn: cn=uid uniqueness,cn=plugins,cn=config
+#objectClass: top
+#objectClass: nsSlapdPlugin
+#objectClass: extensibleObject
+#cn: uid uniqueness
+#nsslapd-pluginPath: libattr-unique-plugin
+#nsslapd-pluginInitfunc: NSUniqueAttr_Init
+#nsslapd-pluginType: preoperation
+#nsslapd-pluginEnabled: on
+#nsslapd-pluginarg0: uid
+#nsslapd-pluginarg1: cn=accounts,$SUFFIX
+#nsslapd-plugin-depends-on-type: database
+#nsslapd-pluginId: NSUniqueAttr
+#nsslapd-pluginVersion: 1.1.0
+#nsslapd-pluginVendor: Fedora Project
+#nsslapd-pluginDescription: Enforce unique attribute values
+#
diff --git a/ipa-server/ipa-install/updates/Makefile.am b/ipa-server/ipa-install/updates/Makefile.am
new file mode 100644
index 00000000..11d20ddd
--- /dev/null
+++ b/ipa-server/ipa-install/updates/Makefile.am
@@ -0,0 +1,19 @@
+NULL =
+
+appdir = $(IPA_DATA_DIR)/updates
+app_DATA = \
+ RFC4876.update \
+ RFC2307bis.update \
+ nss_ldap.update \
+ winsync_index.update \
+ replication.update \
+ indices.update \
+ $(NULL)
+
+EXTRA_DIST = \
+ $(app_DATA) \
+ $(NULL)
+
+MAINTAINERCLEANFILES = \
+ *~ \
+ Makefile.in
diff --git a/ipa-server/ipa-install/updates/RFC2307bis.update b/ipa-server/ipa-install/updates/RFC2307bis.update
new file mode 100644
index 00000000..1ddebc1a
--- /dev/null
+++ b/ipa-server/ipa-install/updates/RFC2307bis.update
@@ -0,0 +1,65 @@
+#
+# Schema derived from RFC 2307bis:
+# "An Approach for Using LDAP as a Network Information Service"
+#
+dn: cn=schema
+add: attributeTypes:
+ ( 1.3.6.1.1.1.1.28 NAME 'nisPublickey'
+ DESC 'nisPublickey'
+ EQUALITY caseIgnoreIA5Match
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
+ X-ORIGIN 'RFC2307bis' )
+add:attributeTypes:
+ ( 1.3.6.1.1.1.1.29 NAME 'nisSecretkey'
+ DESC 'nisSecretkey'
+ EQUALITY caseIgnoreIA5Match
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
+ X-ORIGIN 'RFC2307bis' )
+add:attributeTypes:
+ ( 1.3.6.1.4.1.1.1.1.12 NAME 'nisDomain'
+ DESC 'NIS domain'
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
+ X-ORIGIN 'RFC2307bis' )
+add:attributeTypes:
+ ( 2.16.840.1.113730.3.1.30 NAME 'mgrpRFC822MailMember'
+ DESC 'mgrpRFC822MailMember'
+ EQUALITY caseIgnoreIA5Match
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
+ X-ORIGIN 'RFC2307bis' )
+add:attributeTypes:
+ ( 1.3.6.1.4.1.42.2.27.1.1.12 NAME 'nisNetIdUser'
+ DESC 'nisNetIdUser'
+ EQUALITY caseExactIA5Match
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
+ X-ORIGIN 'RFC2307bis' )
+add:attributeTypes:
+ ( 1.3.6.1.4.1.42.2.27.1.1.13 NAME 'nisNetIdGroup'
+ DESC 'nisNetIdGroup'
+ EQUALITY caseExactIA5Match
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
+ X-ORIGIN 'RFC2307bis' )
+add:attributeTypes:
+ ( 1.3.6.1.4.1.42.2.27.1.1.14 NAME 'nisNetIdHost'
+ DESC 'nisNetIdHost'
+ EQUALITY caseExactIA5Match
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
+ X-ORIGIN 'RFC2307bis' )
+add:objectClasses:
+ ( 1.3.6.1.1.1.2.14 NAME 'nisKeyObject'
+ DESC 'nisKeyObject' SUP top
+ MUST ( cn $ nisPublickey $ nisSecretkey )
+ MAY ( uidNumber $ description ) )
+add:objectClasses:
+ ( 1.3.1.6.1.1.1.2.15 NAME 'nisDomainObject'
+ DESC 'nisDomainObject' SUP top AUXILIARY
+ MUST ( nisDomain ) )
+add:objectClasses:
+ ( 2.16.840.1.113730.3.2.4 NAME 'mailGroup'
+ DESC 'mailGroup' SUP top
+ MUST ( mail )
+ MAY ( cn $ mgrpRFC822MailMember ) )
+add:objectClasses:
+ ( 1.3.6.1.4.1.42.2.27.1.2.6 NAME 'nisNetId'
+ DESC 'nisNetId' SUP top
+ MUST ( cn )
+ MAY ( nisNetIdUser $ nisNetIdGroup $ nisNetIdHost ) )
diff --git a/ipa-server/ipa-install/updates/RFC4876.update b/ipa-server/ipa-install/updates/RFC4876.update
new file mode 100644
index 00000000..5a372c20
--- /dev/null
+++ b/ipa-server/ipa-install/updates/RFC4876.update
@@ -0,0 +1,146 @@
+#
+# Schema more or less verbatim from RFC 4876:
+# "A Configuration Profile Schema for Lightweight Directory Access
+# Protocol (LDAP)-Based Agents"
+#
+dn: cn=schema
+add:attributeTypes:
+ ( 1.3.6.1.4.1.11.1.3.1.1.0 NAME 'defaultServerList'
+ DESC 'List of default servers'
+ EQUALITY caseIgnoreMatch
+ SUBSTR caseIgnoreSubstringsMatch
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
+ SINGLE-VALUE
+ X-ORIGIN 'RFC4876' )
+add:attributeTypes:
+ ( 1.3.6.1.4.1.11.1.3.1.1.1 NAME 'defaultSearchBase'
+ DESC 'Default base for searches'
+ EQUALITY distinguishedNameMatch
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.12
+ SINGLE-VALUE
+ X-ORIGIN 'RFC4876' )
+add:attributeTypes:
+ ( 1.3.6.1.4.1.11.1.3.1.1.2 NAME 'preferredServerList'
+ DESC 'List of preferred servers'
+ EQUALITY caseIgnoreMatch
+ SUBSTR caseIgnoreSubstringsMatch
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
+ SINGLE-VALUE
+ X-ORIGIN 'RFC4876' )
+add:attributeTypes:
+ ( 1.3.6.1.4.1.11.1.3.1.1.3 NAME 'searchTimeLimit'
+ DESC 'Maximum time an agent or service allows for a
+ search to complete'
+ EQUALITY integerMatch
+ ORDERING integerOrderingMatch
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.27
+ SINGLE-VALUE
+ X-ORIGIN 'RFC4876' )
+add:attributeTypes:
+ ( 1.3.6.1.4.1.11.1.3.1.1.4 NAME 'bindTimeLimit'
+ DESC 'Maximum time an agent or service allows for a
+ bind operation to complete'
+ EQUALITY integerMatch
+ ORDERING integerOrderingMatch
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.27
+ SINGLE-VALUE
+ X-ORIGIN 'RFC4876' )
+add:attributeTypes:
+ ( 1.3.6.1.4.1.11.1.3.1.1.5 NAME 'followReferrals'
+ DESC 'An agent or service does or should follow referrals'
+ EQUALITY booleanMatch
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.7
+ SINGLE-VALUE
+ X-ORIGIN 'RFC4876' )
+add:attributeTypes:
+ ( 1.3.6.1.4.1.11.1.3.1.1.6 NAME 'authenticationMethod'
+ DESC 'Identifies the types of authentication methods either
+ used, required, or provided by a service or peer'
+ EQUALITY caseIgnoreMatch
+ SUBSTR caseIgnoreSubstringsMatch
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
+ SINGLE-VALUE
+ X-ORIGIN 'RFC4876' )
+add:attributeTypes:
+ ( 1.3.6.1.4.1.11.1.3.1.1.7 NAME 'profileTTL'
+ DESC 'Time to live, in seconds, before a profile is
+ considered stale'
+ EQUALITY integerMatch
+ ORDERING integerOrderingMatch
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.27
+ SINGLE-VALUE
+ X-ORIGIN 'RFC4876' )
+add:attributeTypes:
+ ( 1.3.6.1.4.1.11.1.3.1.1.9 NAME 'attributeMap'
+ DESC 'Attribute mappings used, required, or supported by an
+ agent or service'
+ EQUALITY caseIgnoreIA5Match
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
+ X-ORIGIN 'RFC4876' )
+add:attributeTypes:
+ ( 1.3.6.1.4.1.11.1.3.1.1.10 NAME 'credentialLevel'
+ DESC 'Identifies type of credentials either used, required,
+ or supported by an agent or service'
+ EQUALITY caseIgnoreIA5Match
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
+ SINGLE-VALUE
+ X-ORIGIN 'RFC4876' )
+add:attributeTypes:
+ ( 1.3.6.1.4.1.11.1.3.1.1.11 NAME 'objectclassMap'
+ DESC 'Object class mappings used, required, or supported by
+ an agent or service'
+ EQUALITY caseIgnoreIA5Match
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
+ X-ORIGIN 'RFC4876' )
+add:attributeTypes:
+ ( 1.3.6.1.4.1.11.1.3.1.1.12 NAME 'defaultSearchScope'
+ DESC 'Default scope used when performing a search'
+ EQUALITY caseIgnoreIA5Match
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
+ SINGLE-VALUE
+ X-ORIGIN 'RFC4876' )
+add:attributeTypes:
+ ( 1.3.6.1.4.1.11.1.3.1.1.13 NAME 'serviceCredentialLevel'
+ DESC 'Specifies the type of credentials either used, required,
+ or supported by a specific service'
+ EQUALITY caseIgnoreIA5Match
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
+ X-ORIGIN 'RFC4876' )
+add:attributeTypes:
+ ( 1.3.6.1.4.1.11.1.3.1.1.14 NAME 'serviceSearchDescriptor'
+ DESC 'Specifies search descriptors required, used, or
+ supported by a particular service or agent'
+ EQUALITY caseExactMatch
+ SUBSTR caseExactSubstringsMatch
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
+ X-ORIGIN 'RFC4876' )
+add:attributeTypes:
+ ( 1.3.6.1.4.1.11.1.3.1.1.15 NAME 'serviceAuthenticationMethod'
+ DESC 'Specifies types authentication methods either
+ used, required, or supported by a particular service'
+ EQUALITY caseIgnoreMatch
+ SUBSTR caseIgnoreSubstringsMatch
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
+ X-ORIGIN 'RFC4876' )
+add:attributeTypes:
+ ( 1.3.6.1.4.1.11.1.3.1.1.16 NAME 'dereferenceAliases'
+ DESC 'Specifies if a service or agent either requires,
+ supports, or uses dereferencing of aliases.'
+ EQUALITY booleanMatch
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.7
+ SINGLE-VALUE
+ X-ORIGIN 'RFC4876' )
+add:objectClasses:
+ ( 1.3.6.1.4.1.11.1.3.1.2.5 NAME 'DUAConfigProfile'
+ SUP top STRUCTURAL
+ DESC 'Abstraction of a base configuration for a DUA'
+ MUST ( cn )
+ MAY ( defaultServerList $ preferredServerList $
+ defaultSearchBase $ defaultSearchScope $
+ searchTimeLimit $ bindTimeLimit $
+ credentialLevel $ authenticationMethod $
+ followReferrals $ dereferenceAliases $
+ serviceSearchDescriptor $ serviceCredentialLevel $
+ serviceAuthenticationMethod $ objectclassMap $
+ attributeMap $ profileTTL )
+ X-ORIGIN 'RFC4876' )
diff --git a/ipa-server/ipa-install/updates/indices.update b/ipa-server/ipa-install/updates/indices.update
new file mode 100644
index 00000000..3d0e42af
--- /dev/null
+++ b/ipa-server/ipa-install/updates/indices.update
@@ -0,0 +1,18 @@
+#
+# Some nss_ldap implementations will always ask for memberuid so we must
+# have an index for it.
+#
+dn: cn=memberuid,cn=index,cn=userRoot,cn=ldbm database,cn=plugins,cn=config
+default:cn: memberuid
+default:ObjectClass: top
+default:ObjectClass: nsIndex
+default:nsSystemIndex: false
+default:nsIndexType: eq,pres
+
+dn: cn=memberof,cn=index,cn=userRoot,cn=ldbm database,cn=plugins,cn=config
+default:cn: memberof
+default:ObjectClass: top
+default:ObjectClass: nsIndex
+default:nsSystemIndex: false
+default:nsIndexType: eq
+
diff --git a/ipa-server/ipa-install/updates/nss_ldap.update b/ipa-server/ipa-install/updates/nss_ldap.update
new file mode 100644
index 00000000..e8c1e00f
--- /dev/null
+++ b/ipa-server/ipa-install/updates/nss_ldap.update
@@ -0,0 +1,33 @@
+#
+# Add profile for RFC 4876 agents (Solaris and HP/ux)
+#
+
+# Update the top-level entry
+dn: $SUFFIX
+add:objectClass: domain
+add:objectClass: domainRelatedObject
+add:objectClass: nisDomainObject
+add:associatedDomain: $DOMAIN
+add:nisDomain: $DOMAIN
+
+# Add a place to store the nss_ldap default profile
+dn: ou=profile,$SUFFIX
+add: objectClass: top
+add: objectClass: organizationalUnit
+add: ou: profiles
+
+# The DUA profile. On Solaris one can run:
+# ldap_client init ipa.example.com
+dn: cn=default,ou=profile,$SUFFIX
+default:ObjectClass: top
+default:ObjectClass: DUAConfigProfile
+default:defaultServerList: $FQDN
+default:defaultSearchBase: $SUFFIX
+default:authenticationMethod: none
+default:searchTimeLimit: 15
+default:cn: default
+default:serviceSearchDescriptor: passwd:cn=users,cn=accounts,$SUFFIX
+default:serviceSearchDescriptor: group:cn=groups,cn=compat,$SUFFIX
+default:bindTimeLimit: 5
+default:objectClassMap: shadow:shadowAccount=posixAccount
+default:followReferrals:TRUE
diff --git a/ipa-server/ipa-install/updates/replication.update b/ipa-server/ipa-install/updates/replication.update
new file mode 100644
index 00000000..29823a6f
--- /dev/null
+++ b/ipa-server/ipa-install/updates/replication.update
@@ -0,0 +1,9 @@
+#
+# Counter used to store the next replica id
+#
+# Start at 3 to avoid conflicts with v1.0 replica ids. The value itself
+# isn't important but each replica needs a unique id.
+dn: cn=replication,cn=etc,$SUFFIX
+add: objectclass: nsDS5Replica
+add: nsDS5ReplicaId: 3
+add: nsDS5ReplicaRoot: '$SUFFIX'
diff --git a/ipa-server/ipa-install/updates/winsync_index.update b/ipa-server/ipa-install/updates/winsync_index.update
new file mode 100644
index 00000000..f24bdf8b
--- /dev/null
+++ b/ipa-server/ipa-install/updates/winsync_index.update
@@ -0,0 +1,10 @@
+#
+# Make sure winsync attributes have the correct indexing
+#
+
+dn: cn=ntUniqueId,cn=index,cn=userRoot,cn=ldbm database,cn=plugins,cn=config
+only: nsIndexType: eq,pres
+
+dn: cn=ntUserDomainId,cn=index,cn=userRoot,cn=ldbm database,cn=plugins,cn=config
+only: nsIndexType: eq,pres
+
diff --git a/ipa-server/ipa-kpasswd/Makefile.am b/ipa-server/ipa-kpasswd/Makefile.am
new file mode 100644
index 00000000..5f95fdef
--- /dev/null
+++ b/ipa-server/ipa-kpasswd/Makefile.am
@@ -0,0 +1,58 @@
+NULL =
+
+INCLUDES = \
+ -I. \
+ -I$(srcdir) \
+ -DPREFIX=\""$(prefix)"\" \
+ -DBINDIR=\""$(bindir)"\" \
+ -DLIBDIR=\""$(libdir)"\" \
+ -DLIBEXECDIR=\""$(libexecdir)"\" \
+ -DDATADIR=\""$(datadir)"\" \
+ $(LDAP_CFLAGS) \
+ $(KRB5_CFLAGS) \
+ $(WARN_CFLAGS) \
+ $(NULL)
+
+sbin_PROGRAMS = \
+ ipa_kpasswd \
+ $(NULL)
+
+ipa_kpasswd_SOURCES = \
+ ipa_kpasswd.c \
+ $(NULL)
+
+ipa_kpasswd_LDADD = \
+ $(LDAP_LIBS) \
+ $(KRB5_LIBS) \
+ $(NULL)
+
+install-exec-local:
+ mkdir -p $(DESTDIR)$(localstatedir)/cache/ipa/kpasswd
+ chmod 700 $(DESTDIR)$(localstatedir)/cache/ipa/kpasswd
+
+uninstall-local:
+ -rmdir $(DESTDIR)$(localstatedir)/cache/ipa/kpasswd
+ -rmdir $(DESTDIR)$(localstatedir)/cache/ipa
+
+EXTRA_DIST = \
+ README \
+ ipa_kpasswd.init \
+ $(NULL)
+
+MAINTAINERCLEANFILES = \
+ *~ \
+ Makefile.in
+
+initdir=$(sysconfdir)/rc.d/init.d
+
+install-data-hook: ipa_kpasswd.init
+
+ if test '!' -d $(DESTDIR)$(initdir); then \
+ $(mkinstalldirs) $(DESTDIR)$(initdir); \
+ chmod 755 $(DESTDIR)$(initdir); \
+ fi
+
+ $(INSTALL_SCRIPT) $(srcdir)/ipa_kpasswd.init $(DESTDIR)$(initdir)/ipa_kpasswd
+
+uninstall-hook:
+ rm -f $(DESTDIR)$(initdir)/ipa_kpasswd
diff --git a/ipa-server/ipa-kpasswd/README b/ipa-server/ipa-kpasswd/README
new file mode 100644
index 00000000..c0a2767a
--- /dev/null
+++ b/ipa-server/ipa-kpasswd/README
@@ -0,0 +1,2 @@
+This is an implementation of the RFC3244 kpasswd protocol.
+It is used to proxy password change operations to Directory Server.
diff --git a/ipa-server/ipa-kpasswd/ipa_kpasswd.c b/ipa-server/ipa-kpasswd/ipa_kpasswd.c
new file mode 100644
index 00000000..f2d3490f
--- /dev/null
+++ b/ipa-server/ipa-kpasswd/ipa_kpasswd.c
@@ -0,0 +1,1388 @@
+
+/* Kpasswd-LDAP proxy */
+
+/* Authors: Simo Sorce <ssorce@redhat.com>
+ *
+ * Copyright (C) 2007, 2008 Red Hat
+ * see file 'COPYING' for use and warranty information
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; version 2 only
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ */
+
+#define _GNU_SOURCE
+
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <sys/time.h>
+#include <sys/wait.h>
+#include <sys/poll.h>
+#include <unistd.h>
+#include <stdio.h>
+#include <stdarg.h>
+#include <stdlib.h>
+#include <string.h>
+#include <errno.h>
+#include <netdb.h>
+#include <syslog.h>
+#include <netinet/in.h>
+#include <arpa/inet.h>
+#include <time.h>
+#include <krb5.h>
+#ifdef WITH_MOZLDAP
+#include <mozldap/ldap.h>
+#else
+#define LDAP_DEPRECATED 1
+#include <ldap.h>
+#endif
+#include <sasl/sasl.h>
+#include <ifaddrs.h>
+
+#define DEFAULT_KEYTAB "FILE:/var/kerberos/krb5kdc/kpasswd.keytab"
+#define TMP_TEMPLATE "/var/cache/ipa/kpasswd/krb5_cc.XXXXXX"
+#define KPASSWD_PORT 464
+
+#ifdef WITH_MOZLDAP
+/* From OpenLDAP's ldap.h */
+#define LDAP_TAG_EXOP_MODIFY_PASSWD_ID ((ber_tag_t) 0x80U)
+#define LDAP_TAG_EXOP_MODIFY_PASSWD_NEW ((ber_tag_t) 0x82U)
+#endif
+
+/* blacklist entries are released only BLCAKLIST_TIMEOUT seconds
+ * after the children performing the noperation has finished.
+ * this is to avoid races */
+
+#define BLACKLIST_TIMEOUT 5
+
+struct blacklist {
+ struct blacklist *next;
+ char *address;
+ pid_t pid;
+ time_t expire;
+};
+
+static struct blacklist *global_blacklist = NULL;
+
+struct socklist {
+ int fd;
+ int socktype;
+ int dest_addr_len;
+ struct sockaddr_storage dest_addr;
+ struct socklist *next;
+};
+
+int check_blacklist(char *address)
+{
+ struct blacklist *bl, *prev_bl;
+ time_t now = time(NULL);
+
+ if (!global_blacklist) {
+ return 0;
+ }
+
+ prev_bl = NULL;
+ bl = global_blacklist;
+ while (bl) {
+ if (bl->expire && (bl->expire < now)) {
+ if (prev_bl) {
+ prev_bl->next = bl->next;
+ free(bl->address);
+ free(bl);
+ bl = prev_bl->next;
+ } else {
+ global_blacklist = bl->next;
+ free(bl->address);
+ free(bl);
+ bl = global_blacklist;
+ }
+ continue;
+ }
+
+ if (strcmp(address, bl->address) == 0) {
+ return 1;
+ }
+
+ prev_bl = bl;
+ bl = bl->next;
+ }
+
+ return 0;
+}
+
+int add_blacklist(pid_t pid, char *address)
+{
+ struct blacklist *bl, *gbl;
+
+ bl = malloc(sizeof(struct blacklist));
+ if (!bl) return -1;
+
+ bl->next = NULL;
+ bl->pid = pid;
+ bl->expire = 0;
+ bl->address = strdup(address);
+ if (!bl->address) {
+ free(bl);
+ return -1;
+ }
+
+ if (!global_blacklist) {
+ global_blacklist = bl;
+ return 0;
+ }
+
+ gbl = global_blacklist;
+ while (gbl->next) {
+ gbl = gbl->next;
+ }
+ gbl->next = bl;
+ return 0;
+}
+
+int remove_blacklist(pid_t pid)
+{
+ struct blacklist *bl;
+
+ if (!global_blacklist) {
+ return -1;
+ }
+
+ bl = global_blacklist;
+ while (bl) {
+ if (pid == bl->pid) {
+ bl->expire = time(NULL) + BLACKLIST_TIMEOUT;
+ return 0;
+ }
+ bl = bl->next;
+ }
+ return -1;
+}
+
+int debug = 0;
+char *srv_pri_name = "kadmin/changepw";
+char *keytab_name = NULL;
+
+static int get_krb5_ticket(char *tmp_file)
+{
+ char *ccname;
+ char *realm_name = NULL;
+ krb5_context context = NULL;
+ krb5_keytab keytab = NULL;
+ krb5_ccache ccache = NULL;
+ krb5_principal kprincpw;
+ krb5_creds my_creds;
+ krb5_get_init_creds_opt options;
+ int krberr, ret;
+
+ krberr = krb5_init_context(&context);
+ if (krberr) {
+ syslog(LOG_ERR, "Failed to init kerberos context");
+ return -1;
+ }
+
+ krberr = krb5_get_default_realm(context, &realm_name);
+ if (krberr) {
+ syslog(LOG_ERR, "Failed to get default realm name: %s",
+ krb5_get_error_message(context, krberr));
+ ret = -1;
+ goto done;
+ }
+
+ krberr = krb5_build_principal(context, &kprincpw,
+ strlen(realm_name), realm_name,
+ "kadmin", "changepw", NULL);
+ if (krberr) {
+ syslog(LOG_ERR, "Unable to build principal: %s",
+ krb5_get_error_message(context, krberr));
+ ret = -1;
+ goto done;
+ }
+
+ krberr = krb5_kt_resolve(context, keytab_name, &keytab);
+ if (krberr) {
+ syslog(LOG_ERR, "Failed to read keytab file: %s",
+ krb5_get_error_message(context, krberr));
+ ret = -1;
+ goto done;
+ }
+
+ ret = asprintf(&ccname, "FILE:%s", tmp_file);
+ if (ret == -1) {
+ syslog(LOG_ERR, "Out of memory!");
+ goto done;
+ }
+
+ ret = setenv("KRB5CCNAME", ccname, 1);
+ if (ret == -1) {
+ syslog(LOG_ERR, "Unable to set env. variable KRB5CCNAME!");
+ goto done;
+ }
+
+ krberr = krb5_cc_resolve(context, ccname, &ccache);
+ if (krberr) {
+ syslog(LOG_ERR, "Failed to set cache name: %s",
+ krb5_get_error_message(context, krberr));
+ ret = -1;
+ goto done;
+ }
+
+ memset(&my_creds, 0, sizeof(my_creds));
+ memset(&options, 0, sizeof(options));
+
+ krb5_get_init_creds_opt_set_address_list(&options, NULL);
+ krb5_get_init_creds_opt_set_forwardable(&options, 0);
+ krb5_get_init_creds_opt_set_proxiable(&options, 0);
+ /* set a very short lifetime, we don't keep the ticket around */
+ krb5_get_init_creds_opt_set_tkt_life(&options, 300);
+
+ krberr = krb5_get_init_creds_keytab(context, &my_creds, kprincpw,
+ keytab, 0, NULL,
+ &options);
+
+ if (krberr) {
+ syslog(LOG_ERR, "Failed to init credentials: %s",
+ krb5_get_error_message(context, krberr));
+ ret = -1;
+ goto done;
+ }
+
+ krb5_cc_initialize(context, ccache, kprincpw);
+ if (krberr) {
+ syslog(LOG_ERR, "Failed to init ccache: %s",
+ krb5_get_error_message(context, krberr));
+ ret = -1;
+ goto done;
+ }
+
+ krberr = krb5_cc_store_cred(context, ccache, &my_creds);
+ if (krberr) {
+ syslog(LOG_ERR, "Failed to store creds: %s",
+ krb5_get_error_message(context, krberr));
+ ret = -1;
+ goto done;
+ }
+
+ ret = 0;
+
+done:
+ /* TODO: mem cleanup */
+ if (keytab) krb5_kt_close(context, keytab);
+ if (context) krb5_free_context(context);
+ return ret;
+}
+
+int ldap_sasl_interact(LDAP *ld, unsigned flags, void *priv_data, void *sit)
+{
+ sasl_interact_t *in = NULL;
+ int ret = LDAP_OTHER;
+ char *realm_name = (char *)priv_data;
+
+ if (!ld) return LDAP_PARAM_ERROR;
+
+ for (in = sit; in && in->id != SASL_CB_LIST_END; in++) {
+ switch(in->id) {
+ case SASL_CB_USER:
+ in->result = srv_pri_name;
+ in->len = strlen(srv_pri_name);
+ ret = LDAP_SUCCESS;
+ break;
+ case SASL_CB_GETREALM:
+ in->result = realm_name;
+ in->len = strlen(realm_name);
+ ret = LDAP_SUCCESS;
+ break;
+ default:
+ if (debug > 0) {
+ syslog(LOG_ERR,
+ "Unhandled SASL int. option %ld",
+ in->id);
+ }
+ in->result = NULL;
+ in->len = 0;
+ ret = LDAP_OTHER;
+ }
+ }
+ return ret;
+}
+
+/* from DS ldaprot.h */
+#define LDAP_TAG_PWP_WARNING 0xA0 /* context specific + constructed + 0 */
+#define LDAP_TAG_PWP_SECSLEFT 0x80L /* context specific + primitive */
+#define LDAP_TAG_PWP_GRCLOGINS 0x81L /* context specific + primitive + 1 */
+#define LDAP_TAG_PWP_ERROR 0x81L /* context specific + primitive + 1 */
+
+int ldap_pwd_change(char *client_name, char *realm_name, krb5_data pwd, char **errstr)
+{
+ char *tmp_file = NULL;
+ int version;
+ LDAP *ld = NULL;
+ BerElement *ctrl = NULL;
+ BerElement *sctrl = NULL;
+ struct berval *control = NULL;
+ struct berval newpw;
+ char hostname[1024];
+ struct berval **ncvals;
+ char *ldap_base = NULL;
+ char *filter;
+ char *attrs[] = {"krbprincipalname", NULL};
+ char *root_attrs[] = {"namingContexts", NULL};
+ char *userdn = NULL;
+ char *retoid = NULL;
+ struct berval *retdata = NULL;
+ struct timeval tv;
+ LDAPMessage *entry, *res = NULL;
+ LDAPControl **srvctrl = NULL;
+ char *exterr0 = NULL;
+ char *exterr1 = NULL;
+ char *exterr2 = NULL;
+ char *err = NULL;
+ int msgid;
+ int ret, rc;
+ int fd;
+ int kpwd_err = KRB5_KPASSWD_HARDERROR;
+
+ tmp_file = strdup(TMP_TEMPLATE);
+ if (!tmp_file) {
+ syslog(LOG_ERR, "Out of memory!");
+ goto done;
+ }
+
+ fd = mkstemp(tmp_file);
+ if (fd == -1) {
+ syslog(LOG_ERR,
+ "Failed to create tmp file with errno: %d", errno);
+ goto done;
+ }
+ /* close mimmediately, we don't need to keep the file open,
+ * just that it exist and has a unique name */
+ close(fd);
+
+ /* In the long term we may want to do this in the main daemon
+ * and just renew when needed.
+ * Right now do it at every password change for robustness */
+ ret = get_krb5_ticket(tmp_file);
+ if (ret) {
+ syslog(LOG_ERR, "Unable to kinit!");
+ goto done;
+ }
+
+ newpw.bv_len = pwd.length;
+ newpw.bv_val = pwd.data;
+
+ /* retrieve server name and build uri */
+ ret = gethostname(hostname, 1023);
+ if (ret == -1) {
+ syslog(LOG_ERR, "Unable to get the hostname!");
+ goto done;
+ }
+
+ /* connect to ldap server */
+ /* TODO: support referrals ? */
+ ld = ldap_init(hostname, 389);
+ if(ld == NULL) {
+ syslog(LOG_ERR, "Unable to connect to ldap server");
+ goto done;
+ }
+
+ version = LDAP_VERSION3;
+ ret = ldap_set_option(ld, LDAP_OPT_PROTOCOL_VERSION, &version);
+ if (ret != LDAP_SUCCESS) {
+ syslog(LOG_ERR, "Unable to set ldap protocol version");
+ goto done;
+ }
+
+ ret = ldap_sasl_interactive_bind_s(ld,
+ NULL, "GSSAPI",
+ NULL, NULL,
+ LDAP_SASL_AUTOMATIC,
+ ldap_sasl_interact, realm_name);
+ if (ret != LDAP_SUCCESS) {
+ syslog(LOG_ERR, "Unable to bind to ldap server");
+ goto done;
+ }
+
+ /* find base dn */
+ /* TODO: address the case where we have multiple naming contexts */
+ tv.tv_sec = 10;
+ tv.tv_usec = 0;
+
+ ret = ldap_search_ext_s(ld, "", LDAP_SCOPE_BASE,
+ "objectclass=*", root_attrs, 0,
+ NULL, NULL, &tv, 0, &res);
+
+ if (ret != LDAP_SUCCESS) {
+ syslog(LOG_ERR,
+ "Search for %s on rootdse failed with error %d",
+ root_attrs[0], ret);
+ goto done;
+ }
+
+ /* for now just use the first result we get */
+ entry = ldap_first_entry(ld, res);
+ ncvals = ldap_get_values_len(ld, entry, root_attrs[0]);
+ if (!ncvals) {
+ syslog(LOG_ERR, "No values for %s", root_attrs[0]);
+ goto done;
+ }
+
+ ldap_base = strdup(ncvals[0]->bv_val);
+
+ ldap_value_free_len(ncvals);
+ ldap_msgfree(res);
+
+ /* find user dn */
+ ret = asprintf(&filter, "krbPrincipalName=%s", client_name);
+ if (ret == -1) {
+ syslog(LOG_ERR, "Out of memory!");
+ goto done;
+ }
+
+ tv.tv_sec = 10;
+ tv.tv_usec = 0;
+
+ ret = ldap_search_ext_s(ld, ldap_base, LDAP_SCOPE_SUBTREE,
+ filter, attrs, 1, NULL, NULL, &tv, 0, &res);
+
+ if (ret != LDAP_SUCCESS) {
+ syslog(LOG_ERR, "Search for %s failed with error %d",
+ filter, ret);
+ if (ret == LDAP_CONSTRAINT_VIOLATION) {
+ *errstr = strdup("Password Change Failed");
+ kpwd_err = KRB5_KPASSWD_SOFTERROR;
+ }
+ goto done;
+ }
+ free(filter);
+
+ /* for now just use the first result we get */
+ entry = ldap_first_entry(ld, res);
+ userdn = ldap_get_dn(ld, entry);
+
+ ldap_msgfree(res);
+ res = NULL;
+
+ if (!userdn) {
+ syslog(LOG_ERR, "No userdn, can't change password!");
+ goto done;
+ }
+
+ /* build password change control */
+ ctrl = ber_alloc_t(LBER_USE_DER);
+ if (!ctrl) {
+ syslog(LOG_ERR, "Out of memory!");
+ goto done;
+ }
+
+ ber_printf(ctrl, "{tstO}",
+ LDAP_TAG_EXOP_MODIFY_PASSWD_ID, userdn,
+ LDAP_TAG_EXOP_MODIFY_PASSWD_NEW, &newpw);
+
+ ret = ber_flatten(ctrl, &control);
+ if (ret < 0) {
+ syslog(LOG_ERR, "ber flattening failed!");
+ goto done;
+ }
+
+ /* perform password change */
+ ret = ldap_extended_operation(ld,
+ LDAP_EXOP_MODIFY_PASSWD,
+ control, NULL, NULL,
+ &msgid);
+ if (ret != LDAP_SUCCESS) {
+ syslog(LOG_ERR, "ldap_extended_operation() failed. (%d)", ret);
+ goto done;
+ }
+
+ tv.tv_sec = 10;
+ tv.tv_usec = 0;
+
+ ret = ldap_result(ld, msgid, 1, &tv, &res);
+ if (ret == -1) {
+ ldap_get_option(ld, LDAP_OPT_ERROR_NUMBER, &rc);
+ syslog(LOG_ERR, "ldap_result() failed. (%d)", rc);
+ goto done;
+ }
+
+ ret = ldap_parse_extended_result(ld, res, &retoid, &retdata, 0);
+ if(ret != LDAP_SUCCESS) {
+ syslog(LOG_ERR, "ldap_parse_extended_result() failed.");
+ ldap_msgfree(res);
+ goto done;
+ }
+ if (retoid || retdata) {
+ syslog(LOG_ERR, "ldap_parse_extended_result() returned data, but we don't handle it yet.");
+ }
+
+ ret = ldap_parse_result(ld, res, &rc, NULL, &err, NULL, &srvctrl, 0);
+ if(ret != LDAP_SUCCESS) {
+ syslog(LOG_ERR, "ldap_parse_result() failed.");
+ goto done;
+ }
+ if (rc != LDAP_SUCCESS) {
+ if (rc == LDAP_CONSTRAINT_VIOLATION) {
+ kpwd_err = KRB5_KPASSWD_SOFTERROR;
+ }
+ ret = LDAP_OPERATIONS_ERROR;
+ }
+ if (err) {
+ syslog(LOG_ERR, "ldap_parse_result(): [%s]", err);
+ ldap_memfree(err);
+ }
+
+ if (srvctrl) {
+
+ LDAPControl *pprc = NULL;
+ int i;
+
+ for (i = 0; srvctrl[i]; i++) {
+ if (0 == strcmp(srvctrl[i]->ldctl_oid, LDAP_CONTROL_PASSWORDPOLICYRESPONSE)) {
+ pprc = srvctrl[i];
+ }
+ }
+ if (pprc) {
+ sctrl = ber_init(&pprc->ldctl_value);
+ }
+
+ if (sctrl) {
+ /*
+ * PasswordPolicyResponseValue ::= SEQUENCE {
+ * warning [0] CHOICE OPTIONAL {
+ * timeBeforeExpiration [0] INTEGER (0 .. maxInt),
+ * graceLoginsRemaining [1] INTEGER (0 .. maxInt) }
+ * error [1] ENUMERATED OPTIONAL {
+ * passwordExpired (0),
+ * accountLocked (1),
+ * changeAfterReset (2),
+ * passwordModNotAllowed (3),
+ * mustSupplyOldPassword (4),
+ * invalidPasswordSyntax (5),
+ * passwordTooShort (6),
+ * passwordTooYoung (7),
+ * passwordInHistory (8) } }
+ */
+
+ ber_tag_t rtag, btag;
+ ber_int_t bint;
+ rtag = ber_scanf(sctrl, "{t", &btag);
+ if (btag == LDAP_TAG_PWP_WARNING) {
+ rtag = ber_scanf(sctrl, "{ti}", &btag, &bint);
+ if (btag == LDAP_TAG_PWP_SECSLEFT) {
+ ret = asprintf(&exterr2, " (%d seconds left before password expires)", bint);
+ } else {
+ ret = asprintf(&exterr2, " (%d grace logins remaining)", bint);
+ }
+ if (ret == -1) {
+ syslog(LOG_ERR, "OOM while creating error message ...");
+ exterr2 = NULL;
+ }
+ rtag = ber_scanf(sctrl, "t", &btag);
+ }
+ if (btag == LDAP_TAG_PWP_ERROR) {
+ rtag = ber_scanf(sctrl, "e", &bint);
+ switch(bint) {
+ case 0:
+ ret = asprintf(&exterr1, " Err%d: Password Expired.", bint);
+ break;
+ case 1:
+ ret = asprintf(&exterr1, " Err%d: Account locked.", bint);
+ break;
+ case 2:
+ ret = asprintf(&exterr1, " Err%d: Password changed after reset.", bint);
+ break;
+ case 3:
+ ret = asprintf(&exterr1, " Err%d: Password change not allowed.", bint);
+ break;
+ case 4:
+ ret = asprintf(&exterr1, " Err%d: [Shouldn't happen].", bint);
+ break;
+ case 5:
+ ret = asprintf(&exterr1, " Err%d: Password too simple.", bint);
+ break;
+ case 6:
+ ret = asprintf(&exterr1, " Err%d: Password too short.", bint);
+ break;
+ case 7:
+ ret = asprintf(&exterr1, " Err%d: Too soon to change password.", bint);
+ break;
+ case 8:
+ ret = asprintf(&exterr1, " Err%d: Password reuse not permitted.", bint);
+ break;
+ default:
+ ret = asprintf(&exterr1, " Err%d: Unknown Errorcode.", bint);
+ break;
+ }
+ if (ret == -1) {
+ syslog(LOG_ERR, "OOM while creating error message ...");
+ exterr1 = NULL;
+ }
+ }
+ }
+ }
+
+ if (ret == LDAP_SUCCESS) {
+ kpwd_err = KRB5_KPASSWD_SUCCESS;
+ exterr0 = "Password change succeeded";
+ } else {
+ exterr0 = "Password change failed";
+ }
+ ret = asprintf(errstr, "%s%s%s", exterr0, exterr1?exterr1:"", exterr2?exterr2:"");
+ if (ret == -1) {
+ syslog(LOG_ERR, "OOM while creating error message ...");
+ *errstr = NULL;
+ }
+
+done:
+ if (ctrl) ber_free(ctrl, 1);
+ if (sctrl) ber_free(sctrl, 1);
+ if (srvctrl) ldap_controls_free(srvctrl);
+ if (res) ldap_msgfree(res);
+ if (control) ber_bvfree(control);
+ free(exterr1);
+ free(exterr2);
+ free(userdn);
+ if (ld) ldap_unbind_ext(ld, NULL, NULL);
+ if (tmp_file) {
+ unlink(tmp_file);
+ free(tmp_file);
+ }
+ return kpwd_err;
+}
+
+void handle_krb_packets(uint8_t *buf, ssize_t buflen,
+ struct socklist *sd,
+ struct sockaddr_storage *from,
+ uint8_t **repbuf, ssize_t *replen)
+{
+ krb5_auth_context auth_context;
+ krb5_context context;
+ krb5_keytab keytab;
+ krb5_principal kprincpw;
+ krb5_ticket *ticket;
+ krb5_address lkaddr, rkaddr;
+ krb5_data kreq, krep, kenc, kdec;
+ krb5_replay_data replay;
+ krb5_error krb5err;
+ int krberr;
+ size_t reqlen;
+ size_t verno;
+ char *client_name, *realm_name;
+ char *result_string;
+ int result_err;
+ uint8_t *reply;
+ ssize_t replylen;
+
+ *replen = 0;
+
+ result_string = NULL;
+ auth_context = NULL;
+ krep.length = 0;
+ krep.data = NULL;
+ kdec.length = 0;
+ kdec.data = NULL;
+ kprincpw = NULL;
+ context = NULL;
+ ticket = NULL;
+
+ switch(((struct sockaddr *)from)->sa_family) {
+ case AF_INET:
+ lkaddr.addrtype = ADDRTYPE_INET;
+ lkaddr.length = sizeof(((struct sockaddr_in *)&sd->dest_addr)->sin_addr);
+ lkaddr.contents = (krb5_octet *) &(((struct sockaddr_in *)&sd->dest_addr)->sin_addr);
+
+ rkaddr.addrtype = ADDRTYPE_INET;
+ rkaddr.length = sizeof(((struct sockaddr_in *)from)->sin_addr);
+ rkaddr.contents = (krb5_octet *) &(((struct sockaddr_in *)from)->sin_addr);
+ break;
+ case AF_INET6:
+ if (IN6_IS_ADDR_V4MAPPED (&((struct sockaddr_in6 *)from)->sin6_addr)) {
+ lkaddr.addrtype = ADDRTYPE_INET;
+ lkaddr.length = 4;
+ lkaddr.contents = 12 + (krb5_octet *) &(((struct sockaddr_in6 *)&sd->dest_addr)->sin6_addr);
+
+ rkaddr.addrtype = ADDRTYPE_INET;
+ rkaddr.length = 4;
+ rkaddr.contents = 12 + (krb5_octet *) &(((struct sockaddr_in6 *)from)->sin6_addr);
+ } else {
+ lkaddr.addrtype = ADDRTYPE_INET6;
+ lkaddr.length = sizeof(((struct sockaddr_in6 *)&sd->dest_addr)->sin6_addr);
+ lkaddr.contents = (krb5_octet *) &(((struct sockaddr_in6 *)&sd->dest_addr)->sin6_addr);
+
+ rkaddr.addrtype = ADDRTYPE_INET6;
+ rkaddr.length = sizeof(((struct sockaddr_in6 *)from)->sin6_addr);
+ rkaddr.contents = (krb5_octet *) &(((struct sockaddr_in6 *)from)->sin6_addr);
+ }
+ break;
+ default:
+ result_string = strdup("Invalid remopte IP address");
+ result_err = KRB5_KPASSWD_MALFORMED;
+ syslog(LOG_ERR, "%s", result_string);
+ goto done;
+ }
+
+ if (buflen < 4) {
+ result_string = strdup("Request truncated");
+ result_err = KRB5_KPASSWD_MALFORMED;
+ syslog(LOG_ERR, "%s", result_string);
+ goto done;
+ }
+
+ reqlen = (buf[0] << 8) + buf[1];
+
+ if (reqlen != buflen) {
+ result_string = strdup("Unmatching request length");
+ result_err = KRB5_KPASSWD_MALFORMED;
+ syslog(LOG_ERR, "%s", result_string);
+ goto done;
+ }
+
+ verno = (buf[2] << 8) + buf[3];
+
+ if (verno != 1) {
+ result_string = strdup("Unsupported version");
+ result_err = KRB5_KPASSWD_BAD_VERSION;
+ syslog(LOG_ERR, "%s", result_string);
+ goto done;
+ }
+
+ kreq.length = (buf[4] << 8) + buf[5];
+ if (kreq.length > (buflen - 6)) {
+ result_string = strdup("Request truncated");
+ result_err = KRB5_KPASSWD_MALFORMED;
+ syslog(LOG_ERR, "%s", result_string);
+ goto done;
+ }
+ kreq.data = (char *)&buf[6];
+
+ krberr = krb5_init_context(&context);
+ if (krberr) {
+ result_string = strdup("Failed to init kerberos context");
+ result_err = KRB5_KPASSWD_HARDERROR;
+ syslog(LOG_ERR, "%s", result_string);
+ goto done;
+ }
+
+ krberr = krb5_get_default_realm(context, &realm_name);
+ if (krberr) {
+ result_string = strdup("Failed to get default realm name");
+ result_err = KRB5_KPASSWD_HARDERROR;
+ syslog(LOG_ERR, "%s", result_string);
+ goto done;
+ }
+
+ krberr = krb5_auth_con_init(context, &auth_context);
+ if (krberr) {
+ result_string = strdup("Unable to init auth context");
+ result_err = KRB5_KPASSWD_HARDERROR;
+ syslog(LOG_ERR, "%s: %s", result_string,
+ krb5_get_error_message(context, krberr));
+ goto done;
+ }
+
+ krberr = krb5_auth_con_setflags(context, auth_context,
+ KRB5_AUTH_CONTEXT_DO_SEQUENCE);
+ if (krberr) {
+ result_string = strdup("Unable to init auth context");
+ result_err = KRB5_KPASSWD_HARDERROR;
+ syslog(LOG_ERR, "%s: %s", result_string,
+ krb5_get_error_message(context, krberr));
+ goto done;
+ }
+
+ krberr = krb5_build_principal(context, &kprincpw,
+ strlen(realm_name), realm_name,
+ "kadmin", "changepw", NULL);
+ if (krberr) {
+ result_string = strdup("Unable to build principal");
+ result_err = KRB5_KPASSWD_HARDERROR;
+ syslog(LOG_ERR, "%s: %s", result_string,
+ krb5_get_error_message(context, krberr));
+ goto done;
+ }
+
+ krberr = krb5_kt_resolve(context, keytab_name, &keytab);
+ if (krberr) {
+ result_string = strdup("Unable to retrieve keytab");
+ result_err = KRB5_KPASSWD_HARDERROR;
+ syslog(LOG_ERR, "%s: %s", result_string,
+ krb5_get_error_message(context, krberr));
+ goto done;
+ }
+
+ krberr = krb5_rd_req(context, &auth_context, &kreq,
+ kprincpw, keytab, NULL, &ticket);
+ if (krberr) {
+ result_string = strdup("Unable to read request");
+ result_err = KRB5_KPASSWD_AUTHERROR;
+ syslog(LOG_ERR, "%s: %s", result_string,
+ krb5_get_error_message(context, krberr));
+ goto done;
+ }
+
+ /* build the AP Reply before actually changing the password
+ * this minimize the risk of a fatal error occurring _after_
+ * the password have been successfully changed */
+ krberr = krb5_mk_rep(context, auth_context, &krep);
+ if (krberr) {
+ result_string = strdup("Failed to to build reply");
+ result_err = KRB5_KPASSWD_HARDERROR;
+ syslog(LOG_ERR, "%s: %s", result_string,
+ krb5_get_error_message(context, krberr));
+ goto done;
+ }
+
+ /* verify that this is an AS_REQ ticket */
+ if (!(ticket->enc_part2->flags & TKT_FLG_INITIAL)) {
+ result_string = strdup("Ticket must be derived from a password");
+ result_err = KRB5_KPASSWD_AUTHERROR;
+ syslog(LOG_ERR, "%s", result_string);
+ goto kpreply;
+ }
+
+ krberr = krb5_unparse_name(context, ticket->enc_part2->client,
+ &client_name);
+ if (krberr) {
+ result_string = strdup("Unable to parse client name");
+ result_err = KRB5_KPASSWD_HARDERROR;
+ syslog(LOG_ERR, "%s", result_string);
+ goto kpreply;
+ }
+
+ krberr = krb5_auth_con_setaddrs(context, auth_context, NULL, &rkaddr);
+ if (krberr) {
+ result_string = strdup("Failed to set client address");
+ result_err = KRB5_KPASSWD_HARDERROR;
+ syslog(LOG_ERR, "%s: %s", result_string,
+ krb5_get_error_message(context, krberr));
+ goto kpreply;
+ }
+
+ /* decrypt the new password */
+ kenc.length = reqlen - kreq.length - 6;
+ kenc.data = kreq.data + kreq.length;
+
+ /* rd_priv needs the remote address while mk_priv (used later)
+ * requires the local address (from kadmin code) */
+ krberr = krb5_rd_priv(context, auth_context, &kenc, &kdec, &replay);
+ if (krberr) {
+ result_string = strdup("Failed to decrypt password");
+ result_err = KRB5_KPASSWD_HARDERROR;
+ syslog(LOG_ERR, "%s: %s", result_string,
+ krb5_get_error_message(context, krberr));
+ goto kpreply;
+ }
+
+ if (debug > 100) {
+ syslog(LOG_ERR, "Client %s trying to set password [%*s]",
+ client_name, kdec.length, kdec.data);
+ }
+
+ /* Actually try to change the password */
+ result_err = ldap_pwd_change(client_name, realm_name, kdec, &result_string);
+ if (result_string == NULL) {
+ result_string = strdup("Server Error while performing LDAP password change");
+ }
+ syslog(LOG_ERR, "%s", result_string);
+
+ /* make sure password is cleared off before we free the memory */
+ memset(kdec.data, 0, kdec.length);
+ free(kdec.data);
+ kdec.length = 0;
+
+kpreply:
+
+ /* set-up the the clear text reply */
+ kdec.length = 2 + strlen(result_string);
+ kdec.data = malloc(kdec.length);
+ if (!kdec.data) {
+ syslog(LOG_ERR, "Out of memory!");
+ kdec.length = 0;
+ goto done;
+ }
+
+ kdec.data[0] = (result_err >> 8) & 0xff;
+ kdec.data[1] = result_err & 0xff;
+ memcpy(&kdec.data[2], result_string, strlen(result_string));
+
+ krberr = krb5_auth_con_setaddrs(context, auth_context, &lkaddr, NULL);
+ if (krberr) {
+ result_string = strdup("Failed to set local address");
+ syslog(LOG_ERR, "%s: %s", result_string,
+ krb5_get_error_message(context, krberr));
+ goto done;
+ }
+
+ krberr = krb5_mk_priv(context, auth_context, &kdec, &kenc, &replay);
+ if (krberr) {
+ result_string = strdup("Failed to encrypt reply message");
+ syslog(LOG_ERR, "%s: %s", result_string,
+ krb5_get_error_message(context, krberr));
+ /* encryption was unsuccessful, let's return a krb error */
+
+ /* the ap data is no more useful */
+ free(krep.data);
+ krep.length = 0;
+
+ /* build a krberror encrypted paylod */
+ krb5err.error = KRB5_CHPW_FAIL;
+ krb5err.server = kprincpw;
+ krb5err.client = NULL;
+ krb5err.ctime = 0;
+ krb5err.cusec = 0;
+ krb5err.susec = 0;
+ krberr = krb5_timeofday(context, &krb5err.stime);
+ if (krberr) {
+ result_string = strdup("Failed to set time of day");
+ syslog(LOG_ERR, "%s: %s", result_string,
+ krb5_get_error_message(context, krberr));
+ goto done;
+ }
+
+ krb5err.text.length = 0;
+ krb5err.e_data = kdec;
+ krberr = krb5_mk_error(context, &krb5err, &kenc);
+ if (krberr) {
+ result_string = strdup("Failed to build error message");
+ syslog(LOG_ERR, "%s: %s", result_string,
+ krb5_get_error_message(context, krberr));
+ goto done;
+ }
+ }
+
+ replylen = 6 + krep.length + kenc.length;
+ reply = malloc(replylen);
+ if (!reply) {
+ syslog(LOG_ERR, "Out of memory!");
+ goto done;
+ }
+ *repbuf = reply;
+
+ reply[0] = (replylen >> 8) & 0xff;
+ reply[1] = replylen & 0xff;
+ reply[2] = 0x00;
+ reply[3] = 0x01;
+ reply[4] = (krep.length >> 8) & 0xff;
+ reply[5] = krep.length & 0xff;
+
+ if (krep.length) {
+ memcpy(&reply[6], krep.data, krep.length);
+ }
+ memcpy(&reply[6 + krep.length], kenc.data, kenc.length);
+
+ *replen = replylen;
+
+done:
+ free(result_string);
+ if (auth_context) krb5_auth_con_free(context, auth_context);
+ if (kprincpw) krb5_free_principal(context, kprincpw);
+ if (krep.length) free(krep.data);
+ if (ticket) krb5_free_ticket(context, ticket);
+ if (kdec.length) free(kdec.data);
+ if (context) krb5_free_context(context);
+}
+
+pid_t handle_conn(struct socklist *sd)
+{
+ int mfd, tcp;
+ pid_t pid;
+ char addrto6[INET6_ADDRSTRLEN+1];
+ char address[INET6_ADDRSTRLEN+1];
+ uint8_t request[1500];
+ ssize_t reqlen;
+ uint8_t *reply;
+ ssize_t replen;
+ struct sockaddr_storage from;
+ socklen_t fromlen;
+ ssize_t sendret;
+ int ret;
+
+ fromlen = sizeof(from);
+ mfd = 0;
+ tcp = 0;
+ reqlen = 0;
+
+ /* receive request */
+ if (sd->socktype == SOCK_STREAM) {
+ tcp = 1;
+ mfd = accept(sd->fd, (struct sockaddr *)&from, &fromlen);
+ if (mfd == -1) {
+ syslog(LOG_ERR, "Accept failed with error (%d) %s",
+ errno, strerror(errno));
+ return -1;
+ }
+ } else {
+ /* read first to empty the buffer on udp connections */
+ reqlen = recvfrom(sd->fd, request, sizeof(request), 0,
+ (struct sockaddr *)&from, &fromlen);
+ if (reqlen <= 0) {
+ syslog(LOG_ERR, "Error receiving request (%d) %s",
+ errno, strerror(errno));
+ return -1;
+ }
+
+ }
+
+ ret = getnameinfo((struct sockaddr *)&from, fromlen,
+ addrto6, INET6_ADDRSTRLEN+1,
+ NULL, 0, NI_NUMERICHOST);
+ if (ret) {
+ syslog(LOG_ERR, "Error retrieving host address\n");
+ return -1;
+ }
+
+ if (debug > 0) {
+ syslog(LOG_ERR, "Connection from %s", addrto6);
+ }
+
+ if (strchr(addrto6, ':') == NULL) {
+ char *prefix6 = "::ffff:";
+ /* this is an IPv4 formatted addr
+ * convert to IPv6 mapped addr */
+ memcpy(address, prefix6, 7);
+ memcpy(&address[7], addrto6, INET6_ADDRSTRLEN-7);
+ } else {
+ /* regular IPv6 address, copy as is */
+ memcpy(address, addrto6, INET6_ADDRSTRLEN);
+ }
+ /* make sure we have termination */
+ address[INET6_ADDRSTRLEN] = '\0';
+
+ /* Check blacklist for requests from the same IP until operations
+ * are finished on the active client.
+ * the password change may be slow and pam_krb5 sends up to 3 UDP
+ * requests waiting 1 sec. each time.
+ * We do not want to start 3 password changes at the same time */
+
+ if (check_blacklist(address)) {
+ if (debug > 0) {
+ syslog(LOG_ERR, "[%s] blacklisted", address);
+ }
+ if (tcp) close(mfd);
+ return 0;
+ }
+
+ /* now read data if it was a TCP connection */
+ if (tcp) {
+ reqlen = recvfrom(mfd, request, sizeof(request), 0,
+ (struct sockaddr *)&from, &fromlen);
+ if (reqlen <= 0) {
+ syslog(LOG_ERR, "Error receiving request (%d) %s",
+ errno, strerror(errno));
+ close(mfd);
+ return -1;
+ }
+ }
+#if 1
+ /* handle kerberos and ldap operations in childrens */
+ pid = fork();
+ if (pid == -1) {
+ syslog(LOG_ERR, "Fork failed with error (%d) %s",
+ errno, strerror(errno));
+ if (tcp) close(mfd);
+ return 0;
+ }
+ if (pid != 0) { /* parent */
+ if (tcp) close(mfd);
+ add_blacklist(pid, address);
+ return pid;
+ }
+#endif
+
+ /* children */
+ if (debug > 0) syslog(LOG_ERR, "Servicing %s", address);
+
+ /* TCP packets prepend the lenght as a 32bit network order field,
+ * this information seem to be just redundant, so let's simply
+ * skip it */
+ if (tcp) {
+ handle_krb_packets(request+4, reqlen-4, sd, &from, &reply, &replen);
+ } else {
+ handle_krb_packets(request, reqlen, sd, &from, &reply, &replen);
+ }
+
+ if (replen) { /* we have something to reply */
+ if (tcp) {
+ sendret = sendto(mfd, reply, replen, 0, NULL, 0);
+ } else {
+ sendret = sendto(sd->fd, reply, replen, 0, (struct sockaddr *)&from, fromlen);
+ }
+ if (sendret == -1) {
+ syslog(LOG_ERR, "Error sending reply (%d)", errno);
+ }
+ }
+ if (tcp) close(mfd);
+ exit(0);
+}
+
+static int create_socket(struct addrinfo *ai, struct socklist **_sds,
+ struct pollfd **_pfds, int *_nfds)
+{
+ struct socklist *csd, *tsd;
+ struct pollfd *pfds;
+ int nfds;
+ int ret;
+ int tru = 1;
+
+ pfds = *_pfds;
+ nfds = *_nfds;
+
+ csd = calloc(1, sizeof(struct socklist));
+ if (csd == NULL) {
+ syslog(LOG_ERR, "Out of memory, can't create socklist\n");
+ return 1;
+ }
+ csd->socktype = ai->ai_socktype;
+ csd->dest_addr_len = ai->ai_addrlen;
+ memcpy(&csd->dest_addr, ai->ai_addr, ai->ai_addrlen);
+
+ csd->fd = socket(csd->dest_addr.ss_family, csd->socktype, 0);
+ if (csd->fd == -1) {
+ syslog(LOG_ERR, "Unable to create socket (%s)",
+ strerror(errno));
+ goto errout;
+ }
+ ret = setsockopt(csd->fd, SOL_SOCKET, SO_REUSEADDR,
+ (void *)&tru, sizeof(tru));
+
+ ret = bind(csd->fd, (struct sockaddr *)&csd->dest_addr, csd->dest_addr_len);
+ if (ret) {
+ if (errno != EADDRINUSE) {
+ syslog(LOG_ERR, "Unable to bind to socket");
+ close(csd->fd);
+ goto errout;
+ }
+ /* if EADDRINUSE it means we are on a machine
+ * with a dual ipv4/ipv6 stack that does not
+ * allow to bind on both at the same time as the
+ * ipv6 bind already allows connections on ipv4
+ * Just ignore */
+ close(csd->fd);
+ free(csd);
+ return 0;
+ }
+
+ if (csd->socktype == SOCK_STREAM) {
+ ret = listen(csd->fd, SOMAXCONN);
+ if (ret) {
+ syslog(LOG_ERR, "Unable to listen to TCP socket (%s)",
+ strerror(errno));
+ close(csd->fd);
+ goto errout;
+ }
+ }
+
+ pfds = realloc(pfds, sizeof(struct pollfd) * (nfds +1));
+ if (pfds == NULL) {
+ syslog(LOG_ERR, "Out of memory, can't alloc pollfd array\n");
+ close(csd->fd);
+ goto errout;
+ }
+ pfds[nfds].events = POLLIN;
+ pfds[nfds].fd = csd->fd;
+ nfds++;
+
+ if (*_sds) {
+ for (tsd = *_sds; tsd->next; tsd = tsd->next) /* skip */ ;
+ tsd->next = csd;
+ } else {
+ *_sds = csd;
+ }
+
+ *_pfds = pfds;
+ *_nfds = nfds;
+
+ return 0;
+
+errout:
+ free(csd);
+ return 1;
+}
+
+int main(int argc, char *argv[])
+{
+ pid_t pid;
+ struct ifaddrs *ifa, *tifa;
+ struct addrinfo *ai, *tai;
+ struct addrinfo hints;
+ char host[NI_MAXHOST];
+ struct socklist *sds, *csd;
+ struct pollfd *pfds;
+ int nfds;
+ int ret;
+ char *env;
+
+ /* log to syslog */
+ openlog("kpasswd", LOG_PID, LOG_DAEMON);
+
+ /* do not keep any fs busy */
+ ret = chdir("/");
+ if (ret == -1) {
+ syslog(LOG_ERR, "Unable to change dir to '/'");
+ exit(-1);
+ }
+
+ /* daemonize */
+ pid = fork();
+ if (pid == -1) {
+ syslog(LOG_ERR, "Error fork() failed!");
+ exit(-1);
+ }
+ if (pid != 0) { /* parent */
+ exit(0);
+ }
+
+ /* new session */
+ setsid();
+
+ /* close std* descriptors */
+ close(0);
+ close(1);
+ close(2);
+
+ /* fork again to make sure we completely detach from parent process */
+ pid = fork();
+ if (pid == -1) {
+ syslog(LOG_ERR, "Error fork() failed!");
+ exit(-1);
+ }
+ if (pid != 0) { /* parent */
+ exit(0);
+ }
+
+ /* source env vars */
+ env = getenv("KRB5_KTNAME");
+ if (!env) {
+ env = DEFAULT_KEYTAB;
+ }
+ keytab_name = strdup(env);
+ if (!keytab_name) {
+ syslog(LOG_ERR, "Out of memory!");
+ }
+
+ env = getenv("IPA_KPASSWD_DEBUG");
+ if (env) {
+ debug = strtol(env, NULL, 0);
+ }
+
+ ret = getifaddrs(&ifa);
+ if (ret) {
+ syslog(LOG_ERR, "getifaddrs failed: %s", gai_strerror(ret));
+ exit(1);
+ }
+
+ /* Write out the pid file after the sigterm handler */
+ const char *pid_file = "/var/run/ipa_kpasswd.pid";
+ FILE *f = fopen(pid_file, "w");
+ int fail = 1;
+ if (f) {
+ int n_bytes = fprintf(f, "%ld\n", (long) getpid());
+ if (fclose(f) == 0 && 0 < n_bytes)
+ fail = 0;
+ }
+ if (fail) {
+ syslog(LOG_ERR, "Couldn't create pid file %s: %s",
+ pid_file, strerror(errno));
+ exit(1);
+ }
+
+ nfds = 0;
+ pfds = NULL;
+ sds = NULL;
+
+ for (tifa = ifa; tifa; tifa = tifa->ifa_next) {
+
+ if (NULL == tifa->ifa_addr)
+ /* uhmm no address ?? skip it */
+ continue;
+
+ if (tifa->ifa_addr->sa_family != AF_INET &&
+ tifa->ifa_addr->sa_family != AF_INET6) {
+ /* not interesting for us */
+ continue;
+ }
+
+ ret = getnameinfo(tifa->ifa_addr, sizeof(struct sockaddr_storage),
+ host, sizeof(host), NULL, 0, NI_NUMERICHOST);
+ if (ret) {
+ syslog(LOG_ERR, "Error converting address (%s)",
+ gai_strerror(ret));
+ continue;
+ } else {
+ syslog(LOG_INFO, "Setting up socket for [%s]", host);
+ }
+
+ memset(&hints, 0, sizeof(hints));
+ hints.ai_flags = AI_NUMERICHOST;
+ hints.ai_family = AF_UNSPEC;
+
+ /* this should return 2 entries, one for UDP and one for TCP */
+ ret = getaddrinfo(host, "kpasswd", &hints, &ai);
+ if (ret) {
+ syslog(LOG_ERR, "Error getting address info (%s) for [%s]",
+ gai_strerror(ret), host);
+ continue;
+ }
+
+ for (tai = ai; tai; tai = tai->ai_next) {
+ char *socktype = (tai->ai_socktype==SOCK_STREAM)?"TCP":"UDP";
+ ret = create_socket(tai, &sds, &pfds, &nfds);
+ if (ret) {
+ syslog(LOG_ERR,
+ "Failed to set up %s socket for [%s]",
+ socktype, host);
+ }
+ }
+ }
+
+ if (nfds == 0) {
+ syslog(LOG_ERR, "Failed to setup any socket. Aborting");
+ exit(1);
+ }
+
+ /* now that sockets are set up, enter the poll loop */
+
+ while (1) {
+ int cstatus, cid, i;
+
+ ret = poll(pfds, nfds, 3000);
+
+ switch(ret) {
+ case 0:
+ break;
+ case -1:
+ if (errno != EINTR) {
+ syslog(LOG_ERR,
+ "Unexpected error in poll (%d) %s",
+ errno, strerror(errno));
+ exit(5);
+ }
+ break;
+ default:
+ for (i = 0; i < nfds; i++) {
+ if (pfds[i].revents & POLLIN) {
+ for (csd = sds; csd; csd = csd->next) {
+ if (csd->fd == pfds[i].fd) {
+ handle_conn(csd);
+ }
+ }
+ }
+ }
+ }
+
+ /* check for children exiting */
+ cid = waitpid(-1, &cstatus, WNOHANG);
+ if (cid != -1 && cid != 0) {
+ if (debug > 0)
+ syslog(LOG_ERR, "pid %d completed operations!\n", cid);
+ remove_blacklist(cid);
+ }
+ }
+}
diff --git a/ipa-server/ipa-kpasswd/ipa_kpasswd.init b/ipa-server/ipa-kpasswd/ipa_kpasswd.init
new file mode 100644
index 00000000..d7244bed
--- /dev/null
+++ b/ipa-server/ipa-kpasswd/ipa_kpasswd.init
@@ -0,0 +1,83 @@
+#!/bin/sh
+#
+# ipa_kpasswd This starts and stops ipa_kpasswd
+#
+# chkconfig: - 36 64
+# description: ipa_kpasswd IPA Kpasswd daemon
+# processname: /usr/sbin/ipa_kpasswd
+# configdir: /etc/sysconfig/ipa-kpasswd
+#
+
+# Source function library.
+if [ -f /etc/rc.d/init.d/functions ] ; then
+. /etc/rc.d/init.d/functions
+fi
+# Source networking configuration.
+if [ -f /etc/sysconfig/network ] ; then
+. /etc/sysconfig/network
+fi
+
+# Check that networking is up.
+if [ "${NETWORKING}" = "no" ]
+then
+ echo "Networking is down"
+ exit 0
+fi
+
+# Source networking configuration.
+if [ -f /etc/sysconfig/ipa-kpasswd ] ; then
+. /etc/sysconfig/ipa-kpasswd
+fi
+
+NAME="ipa_kpasswd"
+PROG="/usr/sbin/ipa_kpasswd"
+
+start() {
+ echo -n $"Starting $NAME: "
+ daemon $NAME
+ RETVAL=$?
+ echo
+ [ $RETVAL -eq 0 ] && touch /var/lock/subsys/ipa_kpasswd || \
+ RETVAL=1
+ return $RETVAL
+}
+
+stop() {
+ echo -n $"Shutting down $NAME: "
+ killproc $NAME
+ RETVAL=$?
+ echo
+ [ $RETVAL -eq 0 ] && rm -f /var/lock/subsys/ipa_kpasswd
+ return $RETVAL
+}
+
+restart() {
+ stop
+ start
+}
+
+case "$1" in
+ start)
+ start
+ ;;
+ stop)
+ stop
+ ;;
+ status)
+ status $PROG
+ ;;
+ restart)
+ restart
+ ;;
+ condrestart)
+ [ -f /var/lock/subsys/ipa_kpasswd ] && restart || :
+ ;;
+ reload)
+ exit 3
+ ;;
+ *)
+ echo $"Usage: $0 {start|stop|status|restart|condrestart}"
+ exit 2
+esac
+
+exit $?
diff --git a/ipa-server/ipa-ldap-updater b/ipa-server/ipa-ldap-updater
new file mode 100755
index 00000000..f85148e3
--- /dev/null
+++ b/ipa-server/ipa-ldap-updater
@@ -0,0 +1,126 @@
+#!/usr/bin/env python
+# Authors: Rob Crittenden <rcritten@redhat.com>
+#
+# Copyright (C) 2008 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+# Documentation can be found at http://freeipa.org/page/LdapUpdate
+
+# TODO
+# save undo files?
+
+import sys
+try:
+ from optparse import OptionParser
+ from ipaserver import ipaldap
+ from ipa import entity, ipaerror, ipautil, config
+ from ipaserver import installutils
+ from ipaserver.ldapupdate import LDAPUpdate, BadSyntax, UPDATES_DIR
+ import ldap
+ import logging
+ import re
+ import krbV
+ import platform
+ import shlex
+ import time
+ import random
+except ImportError:
+ print >> sys.stderr, """\
+There was a problem importing one of the required Python modules. The
+error was:
+
+ %s
+""" % sys.exc_value
+ sys.exit(1)
+
+def parse_options():
+ usage = "%prog [options] input_file(s)\n"
+ usage += "%prog [options]\n"
+ parser = OptionParser(usage=usage, formatter=config.IPAFormatter())
+
+ parser.add_option("-d", "--debug", action="store_true", dest="debug",
+ help="Display debugging information about the update(s)")
+ parser.add_option("-t", "--test", action="store_true", dest="test",
+ help="Run through the update without changing anything")
+ parser.add_option("-y", dest="password",
+ help="File containing the Directory Manager password")
+
+ config.add_standard_options(parser)
+ options, args = parser.parse_args()
+
+ config.init_config(options)
+
+ return options, args
+
+def get_dirman_password():
+ """Prompt the user for the Directory Manager password and verify its
+ correctness.
+ """
+ password = installutils.read_password("Directory Manager", confirm=False, validate=False)
+
+ return password
+
+def main():
+ loglevel = logging.INFO
+
+ options, args = parse_options()
+ if options.debug:
+ loglevel = logging.DEBUG
+
+ logging.basicConfig(level=loglevel,
+ format='%(levelname)s %(message)s')
+
+ dirman_password = ""
+ if options.password:
+ pw = read_file(options.password)
+ dirman_password = pw[0].strip()
+ else:
+ dirman_password = get_dirman_password()
+
+ ld = LDAPUpdate(dm_password=dirman_password, sub_dict={}, live_run=not options.test)
+
+ files=[]
+ if len(args) < 1:
+ files = ld.get_all_files(UPDATES_DIR)
+ else:
+ files = args
+
+ modified = ld.update(files)
+
+ if modified and options.test:
+ return 2
+ else:
+ return 0
+
+try:
+ if __name__ == "__main__":
+ sys.exit(main())
+except BadSyntax, e:
+ print "There is a syntax error in this update file:"
+ print " %s" % e
+ sys.exit(1)
+except RuntimeError, e:
+ print "%s" % e
+ sys.exit(1)
+except SystemExit, e:
+ sys.exit(e)
+except KeyboardInterrupt, e:
+ sys.exit(1)
+except config.IPAConfigError, e:
+ print "An IPA server to update cannot be found. Has one been configured yet?"
+ print "The error was: %s" % e
+ sys.exit(1)
diff --git a/ipa-server/ipa-server.spec.in b/ipa-server/ipa-server.spec.in
new file mode 100644
index 00000000..dfadac12
--- /dev/null
+++ b/ipa-server/ipa-server.spec.in
@@ -0,0 +1,290 @@
+Name: ipa-server
+Version: __VERSION__
+Release: __RELEASE__%{?dist}
+Summary: IPA authentication server
+
+Group: System Environment/Base
+License: GPLv2
+URL: http://www.freeipa.org
+Source0: %{name}-%{version}.tgz
+BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n)
+
+BuildRequires: fedora-ds-base-devel >= 1.1
+BuildRequires: mozldap-devel
+BuildRequires: openssl-devel
+BuildRequires: openldap-devel
+BuildRequires: krb5-devel
+BuildRequires: nss-devel
+BuildRequires: libcap-devel
+
+Requires(post): ipa-server-selinux
+Requires: ipa-python
+Requires: ipa-admintools
+Requires: ipa-client
+Requires: fedora-ds-base >= 1.1
+Requires: openldap-clients
+Requires: nss
+Requires: nss-tools
+Requires: krb5-server
+Requires: krb5-server-ldap
+Requires: cyrus-sasl-gssapi
+Requires: ntp
+Requires: httpd
+Requires: mod_python
+Requires: mod_auth_kerb
+Requires: mod_nss >= 1.0.7-2
+Requires: python-ldap
+Requires: python
+Requires: python-krbV
+Requires: TurboGears
+Requires: python-tgexpandingformwidget
+Requires: acl
+Requires: python-pyasn1
+Requires: libcap
+Requires: slapi-nis
+
+Conflicts: mod_ssl
+
+%define httpd_conf /etc/httpd/conf.d
+%define plugin_dir %{_libdir}/dirsrv/plugins
+
+%{!?python_sitelib: %define python_sitelib %(%{__python} -c "from distutils.sysconfig import get_python_lib; print get_python_lib()")}
+
+%description
+IPA is a server for identity, policy, and audit.
+
+%prep
+%setup -q
+./configure --prefix=%{buildroot}/usr --libdir=%{buildroot}/%{_libdir} --sysconfdir=%{buildroot}/etc --localstatedir=%{buildroot}/var --mandir=%{buildroot}/usr/share/man
+
+%build
+
+make
+
+%install
+rm -rf %{buildroot}
+
+make install
+
+# Remove .la files from libtool - we don't want to package
+# these files
+rm %{buildroot}/%{plugin_dir}/libipa_pwd_extop.la
+rm %{buildroot}/%{plugin_dir}/libipa-memberof-plugin.la
+rm %{buildroot}/%{plugin_dir}/libipa-dna-plugin.la
+rm %{buildroot}/%{plugin_dir}/libipa_winsync.la
+
+# Some user-modifiable HTML files are provided. Move these to /etc
+# and link back.
+mkdir -p %{buildroot}/%{_sysconfdir}/ipa/html
+mv %{buildroot}/%{_usr}/share/ipa/html/ssbrowser.html %{buildroot}/%{_sysconfdir}/ipa/html
+mv %{buildroot}/%{_usr}/share/ipa/html/unauthorized.html %{buildroot}/%{_sysconfdir}/ipa/html
+ln -s ../../../..%{_sysconfdir}/ipa/html/ssbrowser.html \
+ %{buildroot}%{_usr}/share/ipa/html/ssbrowser.html
+ln -s ../../../..%{_sysconfdir}/ipa/html/unauthorized.html \
+ %{buildroot}%{_usr}/share/ipa/html/unauthorized.html
+
+%clean
+rm -rf %{buildroot}
+
+%post
+if [ $1 = 1 ]; then
+ /sbin/chkconfig --add ipa_kpasswd
+ /sbin/chkconfig --add ipa_webgui
+fi
+if [ -e /usr/share/ipa/serial ]; then
+ mv /usr/share/ipa/serial /var/lib/ipa/ca_serialno
+fi
+/bin/touch /var/log/ipa_error.log
+/bin/chown apache /var/log/ipa_error.log
+/bin/chmod 600 /var/log/ipa_error.log
+restorecon /var/log/ipa_error.log
+/usr/sbin/ipa-upgradeconfig || :
+
+%preun
+if [ $1 = 0 ]; then
+ /sbin/chkconfig --del ipa_kpasswd
+ /sbin/chkconfig --del ipa_webgui
+ /sbin/service ipa_kpasswd stop >/dev/null 2>&1 || :
+ /sbin/service ipa_webgui stop >/dev/null 2>&1 || :
+fi
+
+%postun
+if [ "$1" -ge "1" ]; then
+ /sbin/service ipa_kpasswd condrestart >/dev/null 2>&1 || :
+ /sbin/service ipa_webgui condrestart >/dev/null 2>&1 || :
+ /sbin/service httpd condrestart >/dev/null 2>&1 || :
+ /sbin/service dirsrv condrestart >/dev/null 2>&1 || :
+fi
+
+%files
+%defattr(-,root,root,-)
+%{_sbindir}/ipa-server-install
+%{_sbindir}/ipa-replica-install
+%{_sbindir}/ipa-replica-prepare
+%{_sbindir}/ipa-replica-manage
+%{_sbindir}/ipa-server-certinstall
+%{_sbindir}/ipactl
+%{_sbindir}/ipa_kpasswd
+%{_sbindir}/ipa_webgui
+%{_sbindir}/ipa-upgradeconfig
+%{_sbindir}/ipa-fix-CVE-2008-3274
+%{_sbindir}/ipa-ldap-updater
+%{_sbindir}/ipa-compat-manage
+%attr(755,root,root) %{_initrddir}/ipa_kpasswd
+%attr(755,root,root) %{_initrddir}/ipa_webgui
+
+%dir %{_usr}/share/ipa
+%{_usr}/share/ipa/*.ldif
+%{_usr}/share/ipa/*.uldif
+%{_usr}/share/ipa/*.template
+%dir %{_usr}/share/ipa/html
+%{_usr}/share/ipa/html/ssbrowser.html
+%{_usr}/share/ipa/html/unauthorized.html
+%dir %{_sysconfdir}/ipa
+%dir %{_sysconfdir}/ipa/html
+%config(noreplace) %{_sysconfdir}/ipa/html/ssbrowser.html
+%config(noreplace) %{_sysconfdir}/ipa/html/unauthorized.html
+%{_usr}/share/ipa/ipa_webgui.cfg
+%{_usr}/share/ipa/ipa.conf
+%{_usr}/share/ipa/ipa-rewrite.conf
+%dir %{_usr}/share/ipa/ipagui
+%{_usr}/share/ipa/ipagui/*
+%dir %{_usr}/share/ipa/ipa_gui.egg-info
+%{_usr}/share/ipa/ipa_gui.egg-info/*
+%dir %{_usr}/share/ipa/ipaserver
+%{_usr}/share/ipa/ipaserver/*
+%dir %{_usr}/share/ipa/locales/
+%{_usr}/share/ipa/locales/*
+%dir %{_usr}/share/ipa/updates/
+%{_usr}/share/ipa/updates/*
+
+%dir %{python_sitelib}/ipaserver
+%{python_sitelib}/ipaserver/*.py*
+
+%attr(755,root,root) %{plugin_dir}/libipa_pwd_extop.so
+%attr(755,root,root) %{plugin_dir}/libipa-memberof-plugin.so
+%attr(755,root,root) %{plugin_dir}/libipa-dna-plugin.so
+%attr(755,root,root) %{plugin_dir}/libipa_winsync.so
+
+%dir %{_localstatedir}/lib/ipa
+%attr(700,root,root) %dir %{_localstatedir}/lib/ipa/sysrestore
+%dir %{_localstatedir}/cache/ipa
+%attr(700,root,root) %dir %{_localstatedir}/cache/ipa/kpasswd
+%attr(700,apache,apache) %dir %{_localstatedir}/cache/ipa/sessions
+
+%{_mandir}/man8/ipactl.8.gz
+%{_mandir}/man8/ipa_kpasswd.8.gz
+%{_mandir}/man8/ipa_webgui.8.gz
+%{_mandir}/man1/ipa-replica-install.1.gz
+%{_mandir}/man1/ipa-replica-manage.1.gz
+%{_mandir}/man1/ipa-replica-prepare.1.gz
+%{_mandir}/man1/ipa-server-certinstall.1.gz
+%{_mandir}/man1/ipa-server-install.1.gz
+%{_mandir}/man1/ipa-ldap-updater.1.gz
+%{_mandir}/man1/ipa-compat-manage.1.gz
+
+%changelog
+* Wed Sep 17 2008 Rob Crittenden <rcritten@redhat.com> - 1.2.0-0
+- Add ipa-upgradeconfig command and run it at post
+- Move location of the self-signed CA serial number
+- Add ipa-ldap-updater
+- Add updates directory
+- Restart httpd and dirsrv services after upgrade
+- Added WinSync Support - winsync plugin, ipa-replica-manage
+
+* Thu Apr 3 2008 Rob Crittenden <rcritten@redhat.com> - 1.0.0-1
+- Version bump for release
+
+* Fri Mar 14 2008 Rob Crittenden <rcritten@redhat.com> - 0.99.0-5
+- Run restorecon on /var/log/ipa_error.log to ensure correct selinux context
+- Add (post) to ipa-server-selinux Requires
+
+* Fri Mar 14 2008 Rob Crittenden <rcritten@redhat.com> - 0.99.0-4
+- Add missing man pages
+- Add Conflicts for mod_ssl
+
+* Thu Feb 26 2008 Rob Crittenden <rcritten@redhat.com> - 0.99.0-3
+- Add ipactl command
+
+* Thu Feb 21 2008 Rob Crittenden <rcritten@redhat.com> - 0.99.0-2
+- package new file ipa-rewrite.conf
+
+* Thu Feb 21 2008 Rob Crittenden <rcritten@redhat.com> - 0.99.0-1
+- Version bump for release
+
+* Thu Feb 7 2008 Masato Taruishi <taruishi@redhat.com> = 0.6.0-9
+- Internationalize the kid templates and include a Japanese translation.
+
+* Thu Feb 6 2008 Karl MacMillan <kmacmill@redhat.com> = 0.6.0-8
+- Add requirement on ipa-server-selinux
+
+* Thu Jan 31 2008 Rob Crittenden <rcritten@redhat.com> = 0.6.0-7
+- Marked with wrong license. IPA is GPLv2.
+
+* Thu Jan 31 2008 Rob Crittenden <rcritten@redhat.com> = 0.6.0-6
+- Ensure that the ipa_webgui error log exists and has correct permissions.
+
+* Tue Jan 29 2008 Rob Crittenden <rcritten@redhat.com> = 0.6.0-5
+- Put user-modifiable files into /etc/ipa so they can be marked as
+ config(noreplace).
+
+* Thu Jan 24 2008 Rob Crittenden <rcritten@redhat.com> = 0.6.0-4
+- Use new name of pyasn1, python-pyasn1, in Requires
+
+* Tue Jan 22 2008 Rob Crittenden <rcritten@redhat.com> = 0.6.0-3
+- add session cache directory
+
+* Thu Jan 17 2008 Rob Crittenden <rcritten@redhat.com> = 0.6.0-2
+- Fixed License in specfile
+- Include files from /usr/lib/python*/site-packages/ipaserver
+
+* Fri Dec 21 2007 Karl MacMillan <kmacmill@redhat.com> - 0.6.0-1
+- Version bump for release
+
+* Wed Nov 21 2007 Karl MacMillan <kmacmill@mentalrootkit.com> - 0.5.0-1
+- Preverse mode on ipa-keytab-util
+- Version bump for relase and rpm name change
+
+* Thu Nov 15 2007 Rob Crittenden <rcritten@redhat.com> - 0.4.1-2
+- Broke invididual Requires and BuildRequires onto separate lines and
+ reordered them
+- Added python-tgexpandingformwidget as a dependency
+- Require at least fedora-ds-base 1.1
+
+* Thu Nov 1 2007 Karl MacMillan <kmacmill@redhat.com> - 0.4.1-1
+- Version bump for release
+
+* Wed Oct 31 2007 Karl MacMillan <kmacmill@redhat.com> - 0.4.0-6
+- Add dep for freeipa-admintools and acl
+
+* Wed Oct 24 2007 Rob Crittenden <rcritten@redhat.com> - 0.4.0-5
+- Add dependency for python-krbV
+
+* Fri Oct 19 2007 Rob Crittenden <rcritten@redhat.com> - 0.4.0-4
+- Require mod_nss-1.0.7-2 for mod_proxy fixes
+
+* Thu Oct 18 2007 Karl MacMillan <kmacmill@redhat.com> - 0.4.0-3
+- Convert to autotools-based build
+
+* Tue Sep 25 2007 Karl MacMillan <kmacmill@redhat.com> - 0.4.0-2
+- Package ipa-webgui
+
+* Fri Sep 7 2007 Karl MacMillan <kmacmill@redhat.com> - 0.3.0-1
+- Added support for libipa-dna-plugin
+
+* Fri Aug 10 2007 Karl MacMillan <kmacmill@redhat.com> - 0.2.0-1
+- Added support for ipa_kpasswd and ipa_pwd_extop
+
+* Mon Aug 5 2007 Rob Crittenden <rcritten@redhat.com> - 0.1.0-3
+- Abstracted client class to work directly or over RPC
+
+* Wed Aug 1 2007 Rob Crittenden <rcritten@redhat.com> - 0.1.0-2
+- Add mod_auth_kerb and cyrus-sasl-gssapi to Requires
+- Remove references to admin server in ipa-server-setupssl
+- Generate a client certificate for the XML-RPC server to connect to LDAP with
+- Create a keytab for Apache
+- Create an ldif with a test user
+- Provide a certmap.conf for doing SSL client authentication
+
+* Fri Jul 27 2007 Karl MacMillan <kmacmill@redhat.com> - 0.1.0-1
+- Initial rpm version
diff --git a/ipa-server/ipa-slapi-plugins/Makefile.am b/ipa-server/ipa-slapi-plugins/Makefile.am
new file mode 100644
index 00000000..f316371c
--- /dev/null
+++ b/ipa-server/ipa-slapi-plugins/Makefile.am
@@ -0,0 +1,16 @@
+NULL =
+
+SUBDIRS = \
+ ipa-pwd-extop \
+ ipa-memberof \
+ dna \
+ ipa-winsync \
+ $(NULL)
+
+EXTRA_DIST = \
+ README \
+ $(NULL)
+
+MAINTAINERCLEANFILES = \
+ *~ \
+ Makefile.in
diff --git a/ipa-server/ipa-slapi-plugins/README b/ipa-server/ipa-slapi-plugins/README
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/ipa-server/ipa-slapi-plugins/README
diff --git a/ipa-server/ipa-slapi-plugins/dna/Makefile.am b/ipa-server/ipa-slapi-plugins/dna/Makefile.am
new file mode 100644
index 00000000..4a54b8d5
--- /dev/null
+++ b/ipa-server/ipa-slapi-plugins/dna/Makefile.am
@@ -0,0 +1,42 @@
+NULL =
+
+INCLUDES = \
+ -I. \
+ -I$(srcdir) \
+ -DPREFIX=\""$(prefix)"\" \
+ -DBINDIR=\""$(bindir)"\" \
+ -DLIBDIR=\""$(libdir)"\" \
+ -DLIBEXECDIR=\""$(libexecdir)"\" \
+ -DDATADIR=\""$(datadir)"\" \
+ $(MOZLDAP_CFLAGS) \
+ $(KRB5_CFLAGS) \
+ $(WARN_CFLAGS) \
+ $(NULL)
+
+plugindir = $(libdir)/dirsrv/plugins
+plugin_LTLIBRARIES = \
+ libipa-dna-plugin.la \
+ $(NULL)
+
+libipa_dna_plugin_la_SOURCES = \
+ dna.c \
+ $(NULL)
+
+libipa_dna_plugin_la_LDFLAGS = -avoid-version
+
+libipa_dna_plugin_la_LIBADD = \
+ $(MOZLDAP_LIBS) \
+ $(NULL)
+
+appdir = $(IPA_DATA_DIR)
+app_DATA = \
+ dna-conf.ldif \
+ $(NULL)
+
+EXTRA_DIST = \
+ $(app_DATA) \
+ $(NULL)
+
+MAINTAINERCLEANFILES = \
+ *~ \
+ Makefile.in
diff --git a/ipa-server/ipa-slapi-plugins/dna/dna-conf.ldif b/ipa-server/ipa-slapi-plugins/dna/dna-conf.ldif
new file mode 100644
index 00000000..02532b4e
--- /dev/null
+++ b/ipa-server/ipa-slapi-plugins/dna/dna-conf.ldif
@@ -0,0 +1,14 @@
+dn: cn=ipa-dna,cn=plugins,cn=config
+changetype: add
+objectclass: top
+objectclass: nsSlapdPlugin
+objectclass: extensibleObject
+cn: ipa-dna
+nsslapd-pluginpath: libipa-dna-plugin
+nsslapd-plugininitfunc: ipa_dna_init
+nsslapd-plugintype: preoperation
+nsslapd-pluginenabled: on
+nsslapd-pluginid: ipa-dna
+nsslapd-pluginversion: 1.0
+nsslapd-pluginvendor: Red Hat
+nsslapd-plugindescription: IPA Distributed numeric assignment plugin
diff --git a/ipa-server/ipa-slapi-plugins/dna/dna.c b/ipa-server/ipa-slapi-plugins/dna/dna.c
new file mode 100644
index 00000000..cb6a0629
--- /dev/null
+++ b/ipa-server/ipa-slapi-plugins/dna/dna.c
@@ -0,0 +1,1462 @@
+/** BEGIN COPYRIGHT BLOCK
+ * This Program is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free Software
+ * Foundation; version 2 of the License.
+ *
+ * This Program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * this Program; if not, write to the Free Software Foundation, Inc., 59 Temple
+ * Place, Suite 330, Boston, MA 02111-1307 USA.
+ *
+ * In addition, as a special exception, Red Hat, Inc. gives You the additional
+ * right to link the code of this Program with code not covered under the GNU
+ * General Public License ("Non-GPL Code") and to distribute linked combinations
+ * including the two, subject to the limitations in this paragraph. Non-GPL Code
+ * permitted under this exception must only link to the code of this Program
+ * through those well defined interfaces identified in the file named EXCEPTION
+ * found in the source code files (the "Approved Interfaces"). The files of
+ * Non-GPL Code may instantiate templates or use macros or inline functions from
+ * the Approved Interfaces without causing the resulting work to be covered by
+ * the GNU General Public License. Only Red Hat, Inc. may make changes or
+ * additions to the list of Approved Interfaces. You must obey the GNU General
+ * Public License in all respects for all of the Program code and other code used
+ * in conjunction with the Program except the Non-GPL Code covered by this
+ * exception. If you modify this file, you may extend this exception to your
+ * version of the file, but you are not obligated to do so. If you do not wish to
+ * provide this exception without modification, you must delete this exception
+ * statement from your version and license this file solely under the GPL without
+ * exception.
+ *
+ *
+ * Author: Pete Rowley
+ *
+ * Copyright (C) 2007 Red Hat, Inc.
+ * All rights reserved.
+ * END COPYRIGHT BLOCK **/
+
+#ifdef HAVE_CONFIG_H
+# include <config.h>
+#endif
+
+
+/**
+ * Distributed Numeric Assignment plug-in
+ */
+
+#include <dirsrv/slapi-plugin.h>
+
+#include <stdio.h>
+#include <ctype.h>
+#include <string.h>
+#include <errno.h>
+/*#include "portable.h"*/
+#include "nspr.h"
+/*#include "slapi-private.h"*/
+/*#include "dirlite_strings.h"*/
+/*#include "dirver.h"*/
+
+#include "prclist.h"
+#include "ldif.h"
+
+/* get file mode flags for unix */
+#ifndef _WIN32
+#include <sys/stat.h>
+#endif
+
+#define DNA_PLUGIN_SUBSYSTEM "ipa-dna-plugin"
+#define DNA_PLUGIN_VERSION 0x00020000
+
+/* temporary */
+#define DNA_DN "cn=ipa-dna,cn=plugins,cn=config"
+
+#define DNA_SUCCESS 0
+#define DNA_FAILURE -1
+
+/**
+ * DNA config types
+ */
+#define DNA_TYPE "dnaType"
+#define DNA_PREFIX "dnaPrefix"
+#define DNA_NEXTVAL "dnaNextValue"
+#define DNA_INTERVAL "dnaInterval"
+#define DNA_GENERATE "dnaMagicRegen"
+#define DNA_FILTER "dnaFilter"
+#define DNA_SCOPE "dnaScope"
+
+/* since v2 */
+#define DNA_MAXVAL "dnaMaxValue"
+#define DNA_SHARED_CFG_DN "dnaSharedCfgDN"
+
+/* Shared Config */
+#define DNA_GLOBAL_RANGE "dnaGlobalRange"
+#define DNA_RANGE "dnaRange"
+#define DNA_MAX_RANGE_SIZE "dnaMaxRangeSize"
+#define DNA_CHUNK_SIZE "dnaChunkSize"
+
+
+
+#define FEATURE_DESC "IPA Distributed Numeric Assignment"
+#define PLUGIN_DESC "IPA Distributed Numeric Assignment plugin"
+#define PLUGIN_DESC_INT_PREOP PLUGIN_DESC " preop internal"
+#define PLUGIN_DESC_POSTOP PLUGIN_DESC " postop"
+#define PLUGIN_DESC_INT_POSTOP PLUGIN_DESC " postop internal"
+
+static Slapi_PluginDesc pdesc = { FEATURE_DESC,
+ "FreeIPA project", "FreeIPA/1.0",
+ PLUGIN_DESC
+};
+
+
+/**
+ * linked list of config entries
+ */
+
+struct configEntry {
+ PRCList list;
+ char *dn;
+ char *type;
+ char *prefix;
+ PRUint64 nextval;
+ PRUint64 interval;
+ PRUint64 maxval;
+ char *filter;
+ struct slapi_filter *slapi_filter;
+ char *generate;
+ char *scope;
+};
+
+static PRCList *dna_global_config = NULL;
+static PRRWLock *g_dna_cache_lock;
+
+static void *_PluginID = NULL;
+static char *_PluginDN = NULL;
+
+static int g_plugin_started = 0;
+
+
+/*
+ * new value lock
+ */
+static Slapi_Mutex *g_new_value_lock;
+
+/**
+ *
+ * DNA plug-in management functions
+ *
+ */
+int ipa_dna_init(Slapi_PBlock * pb);
+static int dna_start(Slapi_PBlock * pb);
+static int dna_close(Slapi_PBlock * pb);
+static int dna_internal_preop_init(Slapi_PBlock *pb);
+static int dna_postop_init(Slapi_PBlock * pb);
+
+/**
+ *
+ * Local operation functions
+ *
+ */
+static int loadPluginConfig();
+static int parseConfigEntry(Slapi_Entry * e);
+static void deleteConfig();
+static void freeConfigEntry(struct configEntry ** entry);
+
+/**
+ *
+ * helpers
+ *
+ */
+static char *dna_get_dn(Slapi_PBlock * pb);
+static int dna_dn_is_config(char *dn);
+static int dna_get_next_value(struct configEntry * config_entry,
+ char **next_value_ret);
+
+/**
+ *
+ * the ops (where the real work is done)
+ *
+ */
+static int dna_config_check_post_op(Slapi_PBlock * pb);
+static int dna_pre_op(Slapi_PBlock * pb, int modtype);
+static int dna_mod_pre_op(Slapi_PBlock * pb);
+static int dna_add_pre_op(Slapi_PBlock * pb);
+
+/**
+ * debug functions - global, for the debugger
+ */
+void dnaDumpConfig();
+void dnaDumpConfigEntry(struct configEntry *);
+
+/**
+ * set the debug level
+ */
+#ifdef _WIN32
+int *module_ldap_debug = 0;
+
+void plugin_init_debug_level(int *level_ptr)
+{
+ module_ldap_debug = level_ptr;
+}
+#endif
+
+/**
+ *
+ * Deal with cache locking
+ *
+ */
+void dna_read_lock()
+{
+ PR_RWLock_Rlock(g_dna_cache_lock);
+}
+
+void dna_write_lock()
+{
+ PR_RWLock_Wlock(g_dna_cache_lock);
+}
+
+void dna_unlock()
+{
+ PR_RWLock_Unlock(g_dna_cache_lock);
+}
+
+/**
+ *
+ * Get the dna plug-in version
+ *
+ */
+int dna_version()
+{
+ return DNA_PLUGIN_VERSION;
+}
+
+/**
+ * Plugin identity mgmt
+ */
+void setPluginID(void *pluginID)
+{
+ _PluginID = pluginID;
+}
+
+void *getPluginID()
+{
+ return _PluginID;
+}
+
+void setPluginDN(char *pluginDN)
+{
+ _PluginDN = pluginDN;
+}
+
+char *getPluginDN()
+{
+ return _PluginDN;
+}
+
+/*
+ dna_init
+ -------------
+ adds our callbacks to the list
+*/
+int ipa_dna_init(Slapi_PBlock * pb)
+{
+ int status = DNA_SUCCESS;
+ char *plugin_identity = NULL;
+
+ slapi_log_error(SLAPI_LOG_TRACE, DNA_PLUGIN_SUBSYSTEM,
+ "--> ipa_dna_init\n");
+
+ /**
+ * Store the plugin identity for later use.
+ * Used for internal operations
+ */
+
+ slapi_pblock_get(pb, SLAPI_PLUGIN_IDENTITY, &plugin_identity);
+ PR_ASSERT(plugin_identity);
+ setPluginID(plugin_identity);
+
+ if (slapi_pblock_set(pb, SLAPI_PLUGIN_VERSION,
+ SLAPI_PLUGIN_VERSION_01) != 0 ||
+ slapi_pblock_set(pb, SLAPI_PLUGIN_START_FN,
+ (void *) dna_start) != 0 ||
+ slapi_pblock_set(pb, SLAPI_PLUGIN_CLOSE_FN,
+ (void *) dna_close) != 0 ||
+ slapi_pblock_set(pb, SLAPI_PLUGIN_DESCRIPTION,
+ (void *) &pdesc) != 0 ||
+ slapi_pblock_set(pb, SLAPI_PLUGIN_PRE_MODIFY_FN,
+ (void *) dna_mod_pre_op) != 0 ||
+ slapi_pblock_set(pb, SLAPI_PLUGIN_PRE_ADD_FN,
+ (void *) dna_add_pre_op) != 0 ||
+ /* internal preoperation */
+ slapi_register_plugin("internalpreoperation", /* op type */
+ 1, /* Enabled */
+ "dna_internal_preop_init", /* this function desc */
+ dna_internal_preop_init, /* init func */
+ PLUGIN_DESC_INT_PREOP, /* plugin desc */
+ NULL, /* ? */
+ plugin_identity /* access control */
+ ) ||
+ /* the config change checking post op */
+ slapi_register_plugin("postoperation", /* op type */
+ 1, /* Enabled */
+ "dna_postop_init", /* this function desc */
+ dna_postop_init, /* init func for post op */
+ PLUGIN_DESC_POSTOP, /* plugin desc */
+ NULL, /* ? */
+ plugin_identity /* access control */
+ )
+ ) {
+ slapi_log_error(SLAPI_LOG_FATAL, DNA_PLUGIN_SUBSYSTEM,
+ "ipa_dna_init: failed to register plugin\n");
+ status = DNA_FAILURE;
+ }
+
+ slapi_log_error(SLAPI_LOG_TRACE, DNA_PLUGIN_SUBSYSTEM,
+ "<-- ipa_dna_init\n");
+ return status;
+}
+
+
+static int
+dna_internal_preop_init(Slapi_PBlock *pb)
+{
+ int status = DNA_SUCCESS;
+
+ if (slapi_pblock_set(pb, SLAPI_PLUGIN_VERSION,
+ SLAPI_PLUGIN_VERSION_01) != 0 ||
+ slapi_pblock_set(pb, SLAPI_PLUGIN_DESCRIPTION,
+ (void *) &pdesc) != 0 ||
+ slapi_pblock_set(pb, SLAPI_PLUGIN_INTERNAL_PRE_MODIFY_FN,
+ (void *) dna_mod_pre_op) != 0 ||
+ slapi_pblock_set(pb, SLAPI_PLUGIN_INTERNAL_PRE_ADD_FN,
+ (void *) dna_add_pre_op) != 0) {
+ status = DNA_FAILURE;
+ }
+
+ return status;
+}
+
+
+static int dna_postop_init(Slapi_PBlock * pb)
+{
+ int status = DNA_SUCCESS;
+
+ if (slapi_pblock_set(pb, SLAPI_PLUGIN_VERSION,
+ SLAPI_PLUGIN_VERSION_01) != 0 ||
+ slapi_pblock_set(pb, SLAPI_PLUGIN_DESCRIPTION,
+ (void *) &pdesc) != 0 ||
+ slapi_pblock_set(pb, SLAPI_PLUGIN_POST_ADD_FN,
+ (void *) dna_config_check_post_op) != 0 ||
+ slapi_pblock_set(pb, SLAPI_PLUGIN_POST_MODRDN_FN,
+ (void *) dna_config_check_post_op) != 0 ||
+ slapi_pblock_set(pb, SLAPI_PLUGIN_POST_DELETE_FN,
+ (void *) dna_config_check_post_op) != 0 ||
+ slapi_pblock_set(pb, SLAPI_PLUGIN_POST_MODIFY_FN,
+ (void *) dna_config_check_post_op) != 0) {
+ slapi_log_error(SLAPI_LOG_FATAL, DNA_PLUGIN_SUBSYSTEM,
+ "dna_postop_init: failed to register plugin\n");
+ status = DNA_FAILURE;
+ }
+
+ return status;
+}
+
+/*
+ dna_start
+ --------------
+ Kicks off the config cache.
+ It is called after dna_init.
+*/
+static int dna_start(Slapi_PBlock * pb)
+{
+ char *plugindn = NULL;
+
+ slapi_log_error(SLAPI_LOG_TRACE, DNA_PLUGIN_SUBSYSTEM,
+ "--> dna_start\n");
+
+ /* Check if we're already started */
+ if (g_plugin_started) {
+ goto done;
+ }
+
+ g_dna_cache_lock = PR_NewRWLock(PR_RWLOCK_RANK_NONE, "dna");
+ g_new_value_lock = slapi_new_mutex();
+
+ if (!g_dna_cache_lock || !g_new_value_lock) {
+ slapi_log_error(SLAPI_LOG_FATAL, DNA_PLUGIN_SUBSYSTEM,
+ "dna_start: lock creation failed\n");
+
+ return DNA_FAILURE;
+ }
+
+ /**
+ * Get the plug-in target dn from the system
+ * and store it for future use. This should avoid
+ * hardcoding of DN's in the code.
+ */
+ slapi_pblock_get(pb, SLAPI_TARGET_DN, &plugindn);
+ if (NULL == plugindn || 0 == strlen(plugindn)) {
+ slapi_log_error(SLAPI_LOG_PLUGIN, DNA_PLUGIN_SUBSYSTEM,
+ "dna_start: had to use hard coded config dn\n");
+ plugindn = DNA_DN;
+ } else {
+ slapi_log_error(SLAPI_LOG_PLUGIN, DNA_PLUGIN_SUBSYSTEM,
+ "dna_start: config at %s\n", plugindn);
+
+ }
+
+ setPluginDN(plugindn);
+
+ /**
+ * Load the config for our plug-in
+ */
+ dna_global_config = (PRCList *)
+ slapi_ch_calloc(1, sizeof(struct configEntry));
+ PR_INIT_CLIST(dna_global_config);
+
+ if (loadPluginConfig() != DNA_SUCCESS) {
+ slapi_log_error(SLAPI_LOG_FATAL, DNA_PLUGIN_SUBSYSTEM,
+ "dna_start: unable to load plug-in configuration\n");
+ return DNA_FAILURE;
+ }
+
+ g_plugin_started = 1;
+ slapi_log_error(SLAPI_LOG_PLUGIN, DNA_PLUGIN_SUBSYSTEM,
+ "dna: ready for service\n");
+ slapi_log_error(SLAPI_LOG_TRACE, DNA_PLUGIN_SUBSYSTEM,
+ "<-- dna_start\n");
+
+done:
+ return DNA_SUCCESS;
+}
+
+/*
+ dna_close
+ --------------
+ closes down the cache
+*/
+static int dna_close(Slapi_PBlock * pb)
+{
+ slapi_log_error(SLAPI_LOG_TRACE, DNA_PLUGIN_SUBSYSTEM,
+ "--> dna_close\n");
+
+ deleteConfig();
+
+ slapi_ch_free((void **)&dna_global_config);
+
+ slapi_log_error(SLAPI_LOG_TRACE, DNA_PLUGIN_SUBSYSTEM,
+ "<-- dna_close\n");
+
+ return DNA_SUCCESS;
+}
+
+/*
+ * config looks like this
+ * - cn=myplugin
+ * --- cn=posix
+ * ------ cn=accounts
+ * ------ cn=groups
+ * --- cn=samba
+ * --- cn=etc
+ * ------ cn=etc etc
+ */
+static int loadPluginConfig()
+{
+ int status = DNA_SUCCESS;
+ int result;
+ int i;
+ Slapi_PBlock *search_pb;
+ Slapi_Entry **entries = NULL;
+
+ slapi_log_error(SLAPI_LOG_TRACE, DNA_PLUGIN_SUBSYSTEM,
+ "--> loadPluginConfig\n");
+
+ dna_write_lock();
+ deleteConfig();
+
+ search_pb = slapi_pblock_new();
+
+ slapi_search_internal_set_pb(search_pb, getPluginDN(),
+ LDAP_SCOPE_SUBTREE, "objectclass=*",
+ NULL, 0, NULL, NULL, getPluginID(), 0);
+ slapi_search_internal_pb(search_pb);
+ slapi_pblock_get(search_pb, SLAPI_PLUGIN_INTOP_RESULT, &result);
+
+ if (LDAP_SUCCESS != result) {
+ status = DNA_FAILURE;
+ goto cleanup;
+ }
+
+ slapi_pblock_get(search_pb, SLAPI_PLUGIN_INTOP_SEARCH_ENTRIES,
+ &entries);
+ if (NULL == entries || NULL == entries[0]) {
+ status = DNA_SUCCESS;
+ goto cleanup;
+ }
+
+ for (i = 0; (entries[i] != NULL); i++) {
+ status = parseConfigEntry(entries[i]);
+ if (DNA_SUCCESS != status)
+ break;
+ }
+
+ cleanup:
+ slapi_free_search_results_internal(search_pb);
+ slapi_pblock_destroy(search_pb);
+ dna_unlock();
+ slapi_log_error(SLAPI_LOG_TRACE, DNA_PLUGIN_SUBSYSTEM,
+ "<-- loadPluginConfig\n");
+
+ return status;
+}
+
+static int parseConfigEntry(Slapi_Entry * e)
+{
+ char *value;
+ struct configEntry *entry;
+ struct configEntry *config_entry;
+ PRCList *list;
+ int entry_added = 0;
+
+ slapi_log_error(SLAPI_LOG_TRACE, DNA_PLUGIN_SUBSYSTEM,
+ "--> parseConfigEntry\n");
+
+ entry = (struct configEntry *)
+ slapi_ch_calloc(1, sizeof(struct configEntry));
+ if (NULL == entry)
+ goto bail;
+
+ value = slapi_entry_get_ndn(e);
+ if (value) {
+ entry->dn = strdup(value);
+ }
+
+ slapi_log_error(SLAPI_LOG_CONFIG, DNA_PLUGIN_SUBSYSTEM,
+ "----------> dn [%s]\n", entry->dn, 0, 0);
+
+ value = slapi_entry_attr_get_charptr(e, DNA_TYPE);
+ if (value) {
+ entry->type = value;
+ } else
+ goto bail;
+
+ slapi_log_error(SLAPI_LOG_CONFIG, DNA_PLUGIN_SUBSYSTEM,
+ "----------> dnaType [%s]\n", entry->type, 0, 0);
+
+ /* FIXME: check the attribute type, it must suport matching rules and be
+ * indexed, these are requirements and failure to meet them should result in
+ * the configuration to be disarded and an ERROR logged prominently */
+
+ value = slapi_entry_attr_get_charptr(e, DNA_NEXTVAL);
+ if (value) {
+ entry->nextval = strtoul(value, 0, 0);
+ slapi_ch_free_string(&value);
+ } else
+ goto bail;
+
+ slapi_log_error(SLAPI_LOG_CONFIG, DNA_PLUGIN_SUBSYSTEM,
+ "----------> dnaNextValue [%d]\n", entry->nextval, 0,
+ 0);
+
+ value = slapi_entry_attr_get_charptr(e, DNA_PREFIX);
+ if (value && value[0]) {
+ entry->prefix = value;
+ }
+
+ slapi_log_error(SLAPI_LOG_CONFIG, DNA_PLUGIN_SUBSYSTEM,
+ "----------> dnaPrefix [%s]\n", entry->prefix, 0, 0);
+
+ value = slapi_entry_attr_get_charptr(e, DNA_INTERVAL);
+ if (value) {
+ entry->interval = strtoul(value, 0, 0);
+ } else
+ goto bail;
+
+ slapi_log_error(SLAPI_LOG_CONFIG, DNA_PLUGIN_SUBSYSTEM,
+ "----------> dnaInterval [%s]\n", value, 0, 0);
+
+ slapi_ch_free_string(&value);
+
+ value = slapi_entry_attr_get_charptr(e, DNA_GENERATE);
+ if (value) {
+ entry->generate = value;
+ }
+
+ slapi_log_error(SLAPI_LOG_CONFIG, DNA_PLUGIN_SUBSYSTEM,
+ "----------> dnaMagicRegen [%s]\n", entry->generate,
+ 0, 0);
+
+ value = slapi_entry_attr_get_charptr(e, DNA_FILTER);
+ if (value) {
+ entry->filter = value;
+ entry->slapi_filter = slapi_str2filter(value);
+ } else
+ goto bail;
+
+ slapi_log_error(SLAPI_LOG_CONFIG, DNA_PLUGIN_SUBSYSTEM,
+ "----------> dnaFilter [%s]\n", value, 0, 0);
+
+ value = slapi_entry_attr_get_charptr(e, DNA_SCOPE);
+ if (value) {
+ entry->scope = slapi_dn_normalize(value);
+ }
+
+ slapi_log_error(SLAPI_LOG_CONFIG, DNA_PLUGIN_SUBSYSTEM,
+ "----------> dnaScope [%s]\n", entry->scope, 0, 0);
+
+ /* optional, if not specified set -1 which is converted to the max unisgnee
+ * value */
+ value = slapi_entry_attr_get_charptr(e, DNA_MAXVAL);
+ if (value) {
+ entry->maxval = strtoul(value, 0, 0);
+
+ slapi_log_error(SLAPI_LOG_CONFIG, DNA_PLUGIN_SUBSYSTEM,
+ "----------> dnaMaxValue [%ld]\n", value, 0, 0);
+
+ slapi_ch_free_string(&value);
+ } else
+ entry->maxval = -1;
+
+
+ /**
+ * Finally add the entry to the list
+ * we group by type then by filter
+ * and finally sort by dn length with longer dn's
+ * first - this allows the scope checking
+ * code to be simple and quick and
+ * cunningly linear
+ */
+ if (!PR_CLIST_IS_EMPTY(dna_global_config)) {
+ list = PR_LIST_HEAD(dna_global_config);
+ while (list != dna_global_config) {
+ config_entry = (struct configEntry *) list;
+
+ if (slapi_attr_type_cmp(config_entry->type, entry->type, 1))
+ goto next;
+
+ if (slapi_filter_compare(config_entry->slapi_filter,
+ entry->slapi_filter))
+ goto next;
+
+ if (slapi_dn_issuffix(entry->scope, config_entry->scope)) {
+ PR_INSERT_BEFORE(&(entry->list), list);
+ slapi_log_error(SLAPI_LOG_CONFIG,
+ DNA_PLUGIN_SUBSYSTEM,
+ "store [%s] before [%s] \n", entry->scope,
+ config_entry->scope, 0);
+ entry_added = 1;
+ break;
+ }
+
+ next:
+ list = PR_NEXT_LINK(list);
+
+ if (dna_global_config == list) {
+ /* add to tail */
+ PR_INSERT_BEFORE(&(entry->list), list);
+ slapi_log_error(SLAPI_LOG_CONFIG, DNA_PLUGIN_SUBSYSTEM,
+ "store [%s] at tail\n", entry->scope, 0,
+ 0);
+ entry_added = 1;
+ break;
+ }
+ }
+ } else {
+ /* first entry */
+ PR_INSERT_LINK(&(entry->list), dna_global_config);
+ slapi_log_error(SLAPI_LOG_CONFIG, DNA_PLUGIN_SUBSYSTEM,
+ "store [%s] at head \n", entry->scope, 0, 0);
+ entry_added = 1;
+ }
+
+ bail:
+ if (0 == entry_added) {
+ slapi_log_error(SLAPI_LOG_CONFIG, DNA_PLUGIN_SUBSYSTEM,
+ "config entry [%s] skipped\n", entry->dn, 0, 0);
+ freeConfigEntry(&entry);
+ }
+
+ slapi_log_error(SLAPI_LOG_TRACE, DNA_PLUGIN_SUBSYSTEM,
+ "<-- parseConfigEntry\n");
+
+ return DNA_SUCCESS;
+}
+
+static void freeConfigEntry(struct configEntry ** entry)
+{
+ struct configEntry *e = *entry;
+
+ if (e->dn) {
+ slapi_log_error(SLAPI_LOG_CONFIG, DNA_PLUGIN_SUBSYSTEM,
+ "freeing config entry [%s]\n", e->dn, 0, 0);
+ slapi_ch_free_string(&e->dn);
+ }
+
+ if (e->type)
+ slapi_ch_free_string(&e->type);
+
+ if (e->prefix)
+ slapi_ch_free_string(&e->prefix);
+
+ if (e->filter)
+ slapi_ch_free_string(&e->filter);
+
+ if (e->slapi_filter)
+ slapi_filter_free(e->slapi_filter, 1);
+
+ if (e->generate)
+ slapi_ch_free_string(&e->generate);
+
+ if (e->scope)
+ slapi_ch_free_string(&e->scope);
+
+ slapi_ch_free((void **) entry);
+}
+
+static void deleteConfigEntry(PRCList * entry)
+{
+ PR_REMOVE_LINK(entry);
+ freeConfigEntry((struct configEntry **) & entry);
+}
+
+static void deleteConfig()
+{
+ PRCList *list;
+
+ while (!PR_CLIST_IS_EMPTY(dna_global_config)) {
+ list = PR_LIST_HEAD(dna_global_config);
+ deleteConfigEntry(list);
+ }
+
+ return;
+}
+
+/****************************************************
+ Distributed ranges Helpers
+****************************************************/
+
+static int dna_fix_maxval(Slapi_DN *dn, PRUint64 *cur, PRUint64 *max)
+{
+ /* TODO: check the main partition to see if another range
+ * is available, and set the new local configuration
+ * accordingly.
+ * If a new range is not available run the retrieval task
+ * and simply return error
+ */
+
+ return LDAP_OPERATIONS_ERROR;
+}
+
+static void dna_notice_allocation(Slapi_DN *dn, PRUint64 new)
+{
+ /* TODO: check if we passed a new chunk threshold and update
+ * the shared configuration on the public partition.
+ */
+
+ return;
+}
+
+/****************************************************
+ Helpers
+****************************************************/
+
+static char *dna_get_dn(Slapi_PBlock * pb)
+{
+ char *dn = 0;
+ slapi_log_error(SLAPI_LOG_TRACE, DNA_PLUGIN_SUBSYSTEM,
+ "--> dna_get_dn\n");
+
+ if (slapi_pblock_get(pb, SLAPI_TARGET_DN, &dn)) {
+ slapi_log_error(SLAPI_LOG_FATAL, DNA_PLUGIN_SUBSYSTEM,
+ "dna_get_dn: failed to get dn of changed entry");
+ goto bail;
+ }
+
+/* slapi_dn_normalize( dn );
+*/
+ bail:
+ slapi_log_error(SLAPI_LOG_TRACE, DNA_PLUGIN_SUBSYSTEM,
+ "<-- dna_get_dn\n");
+
+ return dn;
+}
+
+/* config check
+ matching config dn or a descendent reloads config
+*/
+static int dna_dn_is_config(char *dn)
+{
+ int ret = 0;
+
+ slapi_log_error(SLAPI_LOG_TRACE, DNA_PLUGIN_SUBSYSTEM,
+ "--> dna_is_config\n");
+
+ if (slapi_dn_issuffix(dn, getPluginDN())) {
+ ret = 1;
+ }
+
+ slapi_log_error(SLAPI_LOG_TRACE, DNA_PLUGIN_SUBSYSTEM,
+ "<-- dna_is_config\n");
+
+ return ret;
+}
+
+#define DNA_LDAP_TAG_SK_REVERSE 0x81L
+
+static LDAPControl *dna_build_sort_control(const char *attr)
+{
+ LDAPControl *ctrl;
+ BerElement *ber;
+ int rc;
+
+ ber = ber_alloc();
+ if (NULL == ber)
+ return NULL;
+
+ rc = ber_printf(ber, "{{stb}}", attr, DNA_LDAP_TAG_SK_REVERSE, 1);
+ if (-1 == rc) {
+ ber_free(ber, 1);
+ return NULL;
+ }
+
+ rc = slapi_build_control(LDAP_CONTROL_SORTREQUEST, ber, 1, &ctrl);
+
+ ber_free(ber, 1);
+
+ if (LDAP_SUCCESS != rc)
+ return NULL;
+
+ return ctrl;
+}
+
+/****************************************************
+ Functions that actually do things other
+ than config and startup
+****************************************************/
+
+/* we do search all values between newval and maxval asking the
+ * server to sort them, then we check the first free spot and
+ * use it as newval */
+static int dna_first_free_value(struct configEntry *config_entry,
+ PRUint64 *newval,
+ PRUint64 maxval,
+ PRUint64 increment)
+{
+ Slapi_Entry **entries = NULL;
+ Slapi_PBlock *pb = NULL;
+ LDAPControl **ctrls;
+ char *attrs[2];
+ char *filter;
+ char *prefix;
+ char *type;
+ int preflen;
+ int result, status;
+ PRUint64 tmpval, sval, i;
+ char *strval = NULL;
+
+ prefix = config_entry->prefix;
+ type = config_entry->type;
+ tmpval = *newval;
+
+ attrs[0] = type;
+ attrs[1] = NULL;
+
+ ctrls = (LDAPControl **)slapi_ch_calloc(2, sizeof(LDAPControl));
+ if (NULL == ctrls)
+ return LDAP_OPERATIONS_ERROR;
+
+ ctrls[0] = dna_build_sort_control(config_entry->type);
+ if (NULL == ctrls[0]) {
+ slapi_ch_free((void **)&ctrls);
+ return LDAP_OPERATIONS_ERROR;
+ }
+
+ filter = slapi_ch_smprintf("(&%s(&(%s>=%s%llu)(%s<=%s%llu)))",
+ config_entry->filter,
+ type, prefix?prefix:"", tmpval,
+ type, prefix?prefix:"", maxval);
+ if (NULL == filter) {
+ ldap_control_free(ctrls[0]);
+ slapi_ch_free((void **)&ctrls);
+ return LDAP_OPERATIONS_ERROR;
+ }
+
+ pb = slapi_pblock_new();
+ if (NULL == pb) {
+ ldap_control_free(ctrls[0]);
+ slapi_ch_free((void **)&ctrls);
+ slapi_ch_free_string(&filter);
+ return LDAP_OPERATIONS_ERROR;
+ }
+
+ slapi_search_internal_set_pb(pb, config_entry->scope,
+ LDAP_SCOPE_SUBTREE, filter,
+ attrs, 0, ctrls,
+ NULL, getPluginID(), 0);
+ slapi_search_internal_pb(pb);
+/*
+ ldap_control_free(ctrls[0]);
+*/
+ slapi_ch_free_string(&filter);
+
+ slapi_pblock_get(pb, SLAPI_PLUGIN_INTOP_RESULT, &result);
+ if (LDAP_SUCCESS != result) {
+ status = LDAP_OPERATIONS_ERROR;
+ goto cleanup;
+ }
+
+ slapi_pblock_get(pb, SLAPI_PLUGIN_INTOP_SEARCH_ENTRIES,
+ &entries);
+
+ if (NULL == entries || NULL == entries[0]) {
+ /* no values means we already have a good value */
+ status = LDAP_SUCCESS;
+ goto cleanup;
+ }
+
+ /* entries are sorted and filtered for value >= tval therefore if the
+ * first one does not match tval it means that the value is free,
+ * otherwise we need to cycle through values until we find a mismatch,
+ * the first mismatch is the first free pit */
+
+ preflen = prefix?strlen(prefix):0;
+ sval = 0;
+ for (i = 0; NULL != entries[i]; i++) {
+ strval = slapi_entry_attr_get_charptr(entries[i], type);
+ if (preflen) {
+ if (strlen(strval) <= preflen) {
+ /* something very wrong here ... */
+ status = LDAP_OPERATIONS_ERROR;
+ goto cleanup;
+ }
+ strval = &strval[preflen-1];
+ }
+
+ errno = 0;
+ sval = strtoul(strval, 0, 0);
+ if (errno) {
+ /* something very wrong here ... */
+ status = LDAP_OPERATIONS_ERROR;
+ goto cleanup;
+ }
+ slapi_ch_free_string(&strval);
+
+ if (tmpval != sval)
+ break;
+
+ if (maxval < sval)
+ break;
+
+ tmpval += increment;
+ }
+
+ *newval = tmpval;
+ status = LDAP_SUCCESS;
+
+cleanup:
+ slapi_ch_free_string(&strval);
+ slapi_free_search_results_internal(pb);
+ slapi_pblock_destroy(pb);
+
+ return status;
+}
+
+/*
+ * Perform ldap operationally atomic increment
+ * Return the next value to be assigned
+ * Method:
+ * 1. retrieve entry
+ * 2. do increment operations
+ * 3. remove current value, add new value in one operation
+ * 4. if failed, and less than 3 times, goto 1
+ */
+static int dna_get_next_value(struct configEntry *config_entry,
+ char **next_value_ret)
+{
+ Slapi_PBlock *pb = NULL;
+ char *old_value = NULL;
+ Slapi_Entry *e = NULL;
+ Slapi_DN *dn = NULL;
+ char *attrlist[4];
+ int attempts;
+ int ret;
+
+ slapi_log_error(SLAPI_LOG_TRACE, DNA_PLUGIN_SUBSYSTEM,
+ "--> dna_get_next_value\n");
+
+ /* get pre-requisites to search */
+ dn = slapi_sdn_new_dn_byref(config_entry->dn);
+ attrlist[0] = DNA_NEXTVAL;
+ attrlist[1] = DNA_MAXVAL;
+ attrlist[2] = DNA_INTERVAL;
+ attrlist[3] = NULL;
+
+
+ /* the operation is constructed such that race conditions
+ * to increment the value are detected and avoided - one wins,
+ * one loses - however, there is no need for the server to compete
+ * with itself so we lock here
+ */
+
+ slapi_lock_mutex(g_new_value_lock);
+
+ for (attempts = 0; attempts < 3; attempts++) {
+
+ LDAPMod mod_add;
+ LDAPMod mod_delete;
+ LDAPMod *mods[3];
+ char *delete_val[2];
+ char *add_val[2];
+ char new_value[16];
+ char *interval;
+ char *max_value;
+ PRUint64 increment = 1; /* default increment */
+ PRUint64 setval = 0;
+ PRUint64 newval = 0;
+ PRUint64 maxval = -1;
+
+ /* do update */
+ ret = slapi_search_internal_get_entry(dn, attrlist, &e,
+ getPluginID());
+ if (LDAP_SUCCESS != ret) {
+ ret = LDAP_OPERATIONS_ERROR;
+ goto done;
+ }
+
+ old_value = slapi_entry_attr_get_charptr(e, DNA_NEXTVAL);
+ if (NULL == old_value) {
+ ret = LDAP_OPERATIONS_ERROR;
+ goto done;
+ }
+
+ setval = strtoul(old_value, 0, 0);
+
+ max_value = slapi_entry_attr_get_charptr(e, DNA_MAXVAL);
+ if (max_value) {
+ maxval = strtoul(max_value, 0, 0);
+ slapi_ch_free_string(&max_value);
+ }
+
+ /* if not present the default is 1 */
+ interval = slapi_entry_attr_get_charptr(e, DNA_INTERVAL);
+ if (NULL != interval) {
+ increment = strtoul(interval, 0, 0);
+ }
+
+ slapi_entry_free(e);
+ e = NULL;
+
+ /* check the value is actually in range */
+
+ /* verify the new value is actually free and get the first
+ * one free if not*/
+ ret = dna_first_free_value(config_entry, &setval, maxval, increment);
+ if (LDAP_SUCCESS != ret)
+ goto done;
+
+ /* try for a new range or fail */
+ if (setval > maxval) {
+ ret = dna_fix_maxval(dn, &setval, &maxval);
+ if (LDAP_SUCCESS != ret) {
+ slapi_log_error(SLAPI_LOG_FATAL, DNA_PLUGIN_SUBSYSTEM,
+ "dna_get_next_value: no more IDs available!!\n");
+ goto done;
+ }
+
+ /* verify the new value is actually free and get the first
+ * one free if not */
+ ret = dna_first_free_value(config_entry, &setval, maxval, increment);
+ if (LDAP_SUCCESS != ret)
+ goto done;
+ }
+
+ if (setval > maxval) {
+ ret = LDAP_OPERATIONS_ERROR;
+ goto done;
+ }
+
+ newval = setval + increment;
+
+ /* try for a new range or fail */
+ if (newval > maxval) {
+ ret = dna_fix_maxval(dn, &newval, &maxval);
+ if (LDAP_SUCCESS != ret) {
+ slapi_log_error(SLAPI_LOG_FATAL, DNA_PLUGIN_SUBSYSTEM,
+ "dna_get_next_value: no more IDs available!!\n");
+ goto done;
+ }
+ }
+
+ /* try to set the new value */
+
+ sprintf(new_value, "%llu", newval);
+
+ delete_val[0] = old_value;
+ delete_val[1] = 0;
+
+ mod_delete.mod_op = LDAP_MOD_DELETE;
+ mod_delete.mod_type = DNA_NEXTVAL;
+ mod_delete.mod_values = delete_val;
+
+ add_val[0] = new_value;
+ add_val[1] = 0;
+
+ mod_add.mod_op = LDAP_MOD_ADD;
+ mod_add.mod_type = DNA_NEXTVAL;
+ mod_add.mod_values = add_val;
+
+ mods[0] = &mod_delete;
+ mods[1] = &mod_add;
+ mods[2] = 0;
+
+ pb = slapi_pblock_new();
+ if (NULL == pb) {
+ ret = LDAP_OPERATIONS_ERROR;
+ goto done;
+ }
+
+ slapi_modify_internal_set_pb(pb, config_entry->dn,
+ mods, 0, 0, getPluginID(), 0);
+
+ slapi_modify_internal_pb(pb);
+
+ slapi_pblock_get(pb, SLAPI_PLUGIN_INTOP_RESULT, &ret);
+
+ slapi_pblock_destroy(pb);
+ pb = NULL;
+ slapi_ch_free_string(&interval);
+ slapi_ch_free_string(&old_value);
+
+ if (LDAP_SUCCESS == ret) {
+ *next_value_ret = slapi_ch_smprintf("%llu", setval);
+ if (NULL == *next_value_ret) {
+ ret = LDAP_OPERATIONS_ERROR;
+ goto done;
+ }
+
+ dna_notice_allocation(dn, newval);
+ goto done;
+ }
+
+ if (LDAP_NO_SUCH_ATTRIBUTE != ret) {
+ /* not the result of a race
+ to change the value
+ */
+ goto done;
+ }
+ }
+
+ done:
+
+ slapi_unlock_mutex(g_new_value_lock);
+
+ if (LDAP_SUCCESS != ret)
+ slapi_ch_free_string(&old_value);
+
+ if (dn)
+ slapi_sdn_free(&dn);
+
+ if (e)
+ slapi_entry_free(e);
+
+ if (pb)
+ slapi_pblock_destroy(pb);
+
+ slapi_log_error(SLAPI_LOG_TRACE, DNA_PLUGIN_SUBSYSTEM,
+ "<-- dna_get_next_value\n");
+
+ return ret;
+}
+
+/* for mods and adds:
+ where dn's are supplied, the closest in scope
+ is used as long as the type and filter
+ are identical - otherwise all matches count
+*/
+
+static int dna_pre_op(Slapi_PBlock * pb, int modtype)
+{
+ char *dn = 0;
+ PRCList *list = 0;
+ struct configEntry *config_entry = 0;
+ struct slapi_entry *e = 0;
+ char *last_type = 0;
+ char *value = 0;
+ int generate = 0;
+ Slapi_Mods *smods = 0;
+ Slapi_Mod *smod = 0;
+ LDAPMod **mods;
+ int free_entry = 0;
+ char *errstr = NULL;
+ int ret = 0;
+
+ slapi_log_error(SLAPI_LOG_TRACE, DNA_PLUGIN_SUBSYSTEM,
+ "--> dna_pre_op\n");
+
+ /* Just bail if we aren't ready to service requests yet. */
+ if (!g_plugin_started)
+ goto bail;
+
+ if (0 == (dn = dna_get_dn(pb)))
+ goto bail;
+
+ if (dna_dn_is_config(dn))
+ goto bail;
+
+ if (LDAP_CHANGETYPE_ADD == modtype) {
+ slapi_pblock_get(pb, SLAPI_ADD_ENTRY, &e);
+ } else {
+ /* xxxPAR: Ideally SLAPI_MODIFY_EXISTING_ENTRY should be
+ * available but it turns out that is only true if you are
+ * a dbm backend pre-op plugin - lucky dbm backend pre-op
+ * plugins.
+ * I think that is wrong since the entry is useful for filter
+ * tests and schema checks and this plugin shouldn't be limited
+ * to a single backend type, but I don't want that fight right
+ * now so we go get the entry here
+ *
+ slapi_pblock_get( pb, SLAPI_MODIFY_EXISTING_ENTRY, &e);
+ */
+ Slapi_DN *tmp_dn = slapi_sdn_new_dn_byref(dn);
+ if (tmp_dn) {
+ slapi_search_internal_get_entry(tmp_dn, 0, &e, getPluginID());
+ slapi_sdn_free(&tmp_dn);
+ free_entry = 1;
+ }
+
+ /* grab the mods - we'll put them back later with
+ * our modifications appended
+ */
+ slapi_pblock_get(pb, SLAPI_MODIFY_MODS, &mods);
+ smods = slapi_mods_new();
+ slapi_mods_init_passin(smods, mods);
+ }
+
+ if (0 == e)
+ goto bailmod;
+
+ dna_read_lock();
+
+ if (!PR_CLIST_IS_EMPTY(dna_global_config)) {
+ list = PR_LIST_HEAD(dna_global_config);
+
+ while (list != dna_global_config && LDAP_SUCCESS == ret) {
+ config_entry = (struct configEntry *) list;
+
+ /* did we already service this type? */
+ if (last_type) {
+ if (!slapi_attr_type_cmp(config_entry->type, last_type, 1))
+ goto next;
+ }
+
+ /* is the entry in scope? */
+ if (config_entry->scope) {
+ if (!slapi_dn_issuffix(dn, config_entry->scope))
+ goto next;
+ }
+
+ /* does the entry match the filter? */
+ if (config_entry->slapi_filter) {
+ if (LDAP_SUCCESS != slapi_vattr_filter_test(pb,
+ e,
+ config_entry->
+ slapi_filter, 0))
+ goto next;
+ }
+
+
+ if (LDAP_CHANGETYPE_ADD == modtype) {
+ /* does attribute contain the magic value
+ or is the type not there?
+ */
+ value =
+ slapi_entry_attr_get_charptr(e, config_entry->type);
+ if ((value
+ && !slapi_UTF8CASECMP(config_entry->generate, value))
+ || 0 == value) {
+ generate = 1;
+ }
+ } else {
+ /* check mods for magic value */
+ Slapi_Mod *next_mod = slapi_mod_new();
+ smod = slapi_mods_get_first_smod(smods, next_mod);
+ while (smod) {
+ char *type = (char *)
+ slapi_mod_get_type(smod);
+
+ if (slapi_attr_types_equivalent(type,
+ config_entry->type)) {
+ struct berval *bv =
+ slapi_mod_get_first_value(smod);
+ int len = strlen(config_entry->generate);
+
+
+ if (len == bv->bv_len) {
+ if (!slapi_UTF8NCASECMP(bv->bv_val,
+ config_entry->generate,
+ len))
+
+ generate = 1;
+ break;
+ }
+ }
+
+ slapi_mod_done(next_mod);
+ smod = slapi_mods_get_next_smod(smods, next_mod);
+ }
+
+ slapi_mod_free(&next_mod);
+ }
+
+ if (generate) {
+ char *new_value;
+ int len;
+
+ /* create the value to add */
+ ret = dna_get_next_value(config_entry, &value);
+ if (DNA_SUCCESS != ret) {
+ errstr = slapi_ch_smprintf("Allocation of a new value for"
+ " %s failed! Unable to proceed.",
+ config_entry->type);
+ break;
+ }
+
+ len = strlen(value) + 1;
+ if (config_entry->prefix) {
+ len += strlen(config_entry->prefix);
+ }
+
+ new_value = slapi_ch_malloc(len);
+
+ if (config_entry->prefix) {
+ strcpy(new_value, config_entry->prefix);
+ strcat(new_value, value);
+ } else
+ strcpy(new_value, value);
+
+ /* do the mod */
+ if (LDAP_CHANGETYPE_ADD == modtype) {
+ /* add - add to entry */
+ slapi_entry_attr_set_charptr(e,
+ config_entry->type,
+ new_value);
+ } else {
+ /* mod - add to mods */
+ slapi_mods_add_string(smods,
+ LDAP_MOD_REPLACE,
+ config_entry->type, new_value);
+ }
+
+ /* free up */
+ slapi_ch_free_string(&value);
+ slapi_ch_free_string(&new_value);
+
+ /* make sure we don't generate for this
+ * type again
+ */
+ if (LDAP_SUCCESS == ret) {
+ last_type = config_entry->type;
+ }
+
+ generate = 0;
+ }
+ next:
+ list = PR_NEXT_LINK(list);
+ }
+ }
+
+ dna_unlock();
+
+ bailmod:
+ if (LDAP_CHANGETYPE_MODIFY == modtype) {
+ /* these are the mods you made, really,
+ * I didn't change them, honest, just had a quick look
+ */
+ mods = slapi_mods_get_ldapmods_passout(smods);
+ slapi_pblock_set(pb, SLAPI_MODIFY_MODS, mods);
+ slapi_mods_free(&smods);
+ }
+
+ bail:
+
+ if (free_entry && e)
+ slapi_entry_free(e);
+
+ if (ret) {
+ slapi_log_error(SLAPI_LOG_PLUGIN, DNA_PLUGIN_SUBSYSTEM,
+ "dna_pre_op: operation failure [%d]\n", ret);
+ slapi_send_ldap_result(pb, ret, NULL, errstr, 0, NULL);
+ slapi_ch_free((void **)&errstr);
+ ret = DNA_FAILURE;
+ }
+
+ slapi_log_error(SLAPI_LOG_TRACE, DNA_PLUGIN_SUBSYSTEM,
+ "<-- dna_pre_op\n");
+
+ return ret;
+}
+
+static int dna_add_pre_op(Slapi_PBlock * pb)
+{
+ return dna_pre_op(pb, LDAP_CHANGETYPE_ADD);
+}
+
+static int dna_mod_pre_op(Slapi_PBlock * pb)
+{
+ return dna_pre_op(pb, LDAP_CHANGETYPE_MODIFY);
+}
+
+static int dna_config_check_post_op(Slapi_PBlock * pb)
+{
+ char *dn;
+
+ slapi_log_error(SLAPI_LOG_TRACE, DNA_PLUGIN_SUBSYSTEM,
+ "--> dna_config_check_post_op\n");
+
+ if ((dn = dna_get_dn(pb))) {
+ if (dna_dn_is_config(dn))
+ loadPluginConfig();
+ }
+
+ slapi_log_error(SLAPI_LOG_TRACE, DNA_PLUGIN_SUBSYSTEM,
+ "<-- dna_config_check_post_op\n");
+
+ return 0;
+}
+
+/****************************************************
+ End of
+ Functions that actually do things other
+ than config and startup
+****************************************************/
+
+/**
+ * debug functions to print config
+ */
+void dnaDumpConfig()
+{
+ PRCList *list;
+
+ dna_read_lock();
+
+ if (!PR_CLIST_IS_EMPTY(dna_global_config)) {
+ list = PR_LIST_HEAD(dna_global_config);
+ while (list != dna_global_config) {
+ dnaDumpConfigEntry((struct configEntry *) list);
+ list = PR_NEXT_LINK(list);
+ }
+ }
+
+ dna_unlock();
+}
+
+
+void dnaDumpConfigEntry(struct configEntry * entry)
+{
+ printf("<- type --------------> %s\n", entry->type);
+ printf("<---- prefix ---------> %s\n", entry->prefix);
+ printf("<---- next value -----> %lu\n", entry->nextval);
+ printf("<---- interval -------> %lu\n", entry->interval);
+ printf("<---- generate flag --> %s\n", entry->generate);
+}
diff --git a/ipa-server/ipa-slapi-plugins/ipa-memberof/Makefile.am b/ipa-server/ipa-slapi-plugins/ipa-memberof/Makefile.am
new file mode 100644
index 00000000..d0ac7f93
--- /dev/null
+++ b/ipa-server/ipa-slapi-plugins/ipa-memberof/Makefile.am
@@ -0,0 +1,43 @@
+NULL =
+
+INCLUDES = \
+ -I. \
+ -I$(srcdir) \
+ -DPREFIX=\""$(prefix)"\" \
+ -DBINDIR=\""$(bindir)"\" \
+ -DLIBDIR=\""$(libdir)"\" \
+ -DLIBEXECDIR=\""$(libexecdir)"\" \
+ -DDATADIR=\""$(datadir)"\" \
+ $(MOZLDAP_CFLAGS) \
+ $(KRB5_CFLAGS) \
+ $(WARN_CFLAGS) \
+ $(NULL)
+
+plugindir = $(libdir)/dirsrv/plugins
+plugin_LTLIBRARIES = \
+ libipa-memberof-plugin.la \
+ $(NULL)
+
+libipa_memberof_plugin_la_SOURCES = \
+ ipa-memberof.c \
+ ipa-memberof_config.c \
+ $(NULL)
+
+libipa_memberof_plugin_la_LDFLAGS = -avoid-version
+
+libipa_memberof_plugin_la_LIBADD = \
+ $(MOZLDAP_LIBS) \
+ $(NULL)
+
+appdir = $(IPA_DATA_DIR)
+app_DATA = \
+ memberof-conf.ldif \
+ $(NULL)
+
+EXTRA_DIST = \
+ $(app_DATA) \
+ $(NULL)
+
+MAINTAINERCLEANFILES = \
+ *~ \
+ Makefile.in
diff --git a/ipa-server/ipa-slapi-plugins/ipa-memberof/ipa-memberof.c b/ipa-server/ipa-slapi-plugins/ipa-memberof/ipa-memberof.c
new file mode 100644
index 00000000..3baf2f6c
--- /dev/null
+++ b/ipa-server/ipa-slapi-plugins/ipa-memberof/ipa-memberof.c
@@ -0,0 +1,2244 @@
+/** BEGIN COPYRIGHT BLOCK
+ * This Program is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free Software
+ * Foundation; version 2 of the License.
+ *
+ * This Program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * this Program; if not, write to the Free Software Foundation, Inc., 59 Temple
+ * Place, Suite 330, Boston, MA 02111-1307 USA.
+ *
+ * In addition, as a special exception, Red Hat, Inc. gives You the additional
+ * right to link the code of this Program with code not covered under the GNU
+ * General Public License ("Non-GPL Code") and to distribute linked combinations
+ * including the two, subject to the limitations in this paragraph. Non-GPL Code
+ * permitted under this exception must only link to the code of this Program
+ * through those well defined interfaces identified in the file named EXCEPTION
+ * found in the source code files (the "Approved Interfaces"). The files of
+ * Non-GPL Code may instantiate templates or use macros or inline functions from
+ * the Approved Interfaces without causing the resulting work to be covered by
+ * the GNU General Public License. Only Red Hat, Inc. may make changes or
+ * additions to the list of Approved Interfaces. You must obey the GNU General
+ * Public License in all respects for all of the Program code and other code used
+ * in conjunction with the Program except the Non-GPL Code covered by this
+ * exception. If you modify this file, you may extend this exception to your
+ * version of the file, but you are not obligated to do so. If you do not wish to
+ * provide this exception without modification, you must delete this exception
+ * statement from your version and license this file solely under the GPL without
+ * exception.
+ *
+ * Authors:
+ * Pete Rowley <prowley@redhat.com>
+ *
+ * Copyright (C) 2007 Red Hat, Inc.
+ * All rights reserved.
+ * END COPYRIGHT BLOCK
+ **/
+
+/* The memberof plugin updates the memberof attribute of entries
+ * based on modifications performed on groupofuniquenames entries
+ *
+ * In addition the plugin provides a DS task that may be started
+ * administrative clients and that creates the initial memberof
+ * list for imported entries and/or fixes the memberof list of
+ * existing entries that have inconsistent state (for example,
+ * if the memberof attribute was incorrectly edited directly)
+ *
+ * To start the memberof task add an entry like:
+ *
+ * dn: cn=mytask, cn=memberof task, cn=tasks, cn=config
+ * objectClass: top
+ * objectClass: extensibleObject
+ * cn: mytask
+ * basedn: dc=example, dc=com
+ * filter: (uid=test4)
+ *
+ * where "basedn" is required and refers to the top most node to perform the
+ * task on, and where "filter" is an optional attribute that provides a filter
+ * describing the entries to be worked on
+ */
+
+#ifdef HAVE_CONFIG_H
+# include <config.h>
+#endif
+
+#include <dirsrv/slapi-plugin.h>
+
+#include "string.h"
+#include "nspr.h"
+
+#include "ipa-memberof.h"
+
+static Slapi_PluginDesc pdesc = { "ipamo", "FreeIPA project", "FreeIPA/1.0",
+ "IPA memberof plugin" };
+
+static void* _PluginID = NULL;
+static Slapi_Mutex *memberof_operation_lock = 0;
+MemberOfConfig *qsortConfig = 0;
+
+typedef struct _memberofstringll
+{
+ const char *dn;
+ void *next;
+} memberofstringll;
+
+typedef struct _memberof_get_groups_data
+{
+ MemberOfConfig *config;
+ Slapi_Value *memberdn_val;
+ Slapi_ValueSet **groupvals;
+} memberof_get_groups_data;
+
+/****** secrets *********/
+#ifndef SLAPI_TASK_PUBLIC
+/*from FDS slap.h
+ * until we get a proper api for access
+ */
+#define TASK_RUNNING_AS_TASK 0x0
+
+/******************************************************************************
+ * Online tasks interface (to support import, export, etc)
+ * After some cleanup, we could consider making these public.
+ */
+struct _slapi_task {
+ struct _slapi_task *next;
+ char *task_dn;
+ int task_exitcode; /* for the end user */
+ int task_state; /* (see above) */
+ int task_progress; /* number between 0 and task_work */
+ int task_work; /* "units" of work to be done */
+ int task_flags; /* (see above) */
+
+ /* it is the task's responsibility to allocate this memory & free it: */
+ char *task_status; /* transient status info */
+ char *task_log; /* appended warnings, etc */
+
+ void *task_private; /* for use by backends */
+ TaskCallbackFn cancel; /* task has been cancelled by user */
+ TaskCallbackFn destructor; /* task entry is being destroyed */
+ int task_refcount;
+};
+
+static void slapi_task_set_data(Slapi_Task *task, void *data)
+{
+ if (task) {
+ task->task_private = data;
+ }
+}
+
+/*
+ * Retrieve some opaque task specific data from the task.
+ */
+static void * slapi_task_get_data(Slapi_Task *task)
+{
+ if (task) {
+ return task->task_private;
+ }
+}
+
+static void slapi_task_begin(Slapi_Task *task, int total_work)
+{
+ if (task) {
+ task->task_work = total_work;
+ task->task_progress = 0;
+ task->task_state = SLAPI_TASK_RUNNING;
+ slapi_task_status_changed(task);
+ }
+}
+
+static void slapi_task_inc_progress(Slapi_Task *task)
+{
+ if (task) {
+ task->task_progress++;
+ slapi_task_status_changed(task);
+ }
+}
+
+static void slapi_task_finish(Slapi_Task *task, int rc)
+{
+ if (task) {
+ task->task_exitcode = rc;
+ task->task_state = SLAPI_TASK_FINISHED;
+ slapi_task_status_changed(task);
+ }
+}
+
+static void slapi_task_set_destructor_fn(Slapi_Task *task, TaskCallbackFn func)
+{
+ if (task) {
+ task->destructor = func;
+ }
+}
+
+#endif /* !SLAPI_TASK_PUBLIC */
+/****** secrets ********/
+
+/*** function prototypes ***/
+
+/* exported functions */
+int ipamo_postop_init(Slapi_PBlock *pb );
+
+/* plugin callbacks */
+static int memberof_postop_del(Slapi_PBlock *pb );
+static int memberof_postop_modrdn(Slapi_PBlock *pb );
+static int memberof_postop_modify(Slapi_PBlock *pb );
+static int memberof_postop_add(Slapi_PBlock *pb );
+static int memberof_postop_start(Slapi_PBlock *pb);
+static int memberof_postop_close(Slapi_PBlock *pb);
+
+/* supporting cast */
+static int memberof_oktodo(Slapi_PBlock *pb);
+static char *memberof_getdn(Slapi_PBlock *pb);
+static int memberof_modop_one(Slapi_PBlock *pb, MemberOfConfig *config, int mod_op,
+ char *op_this, char *op_to);
+static int memberof_modop_one_r(Slapi_PBlock *pb, MemberOfConfig *config, int mod_op,
+ char *group_dn, char *op_this, char *op_to, memberofstringll *stack);
+static int memberof_add_one(Slapi_PBlock *pb, MemberOfConfig *config, char *addthis,
+ char *addto);
+static int memberof_del_one(Slapi_PBlock *pb, MemberOfConfig *config, char *delthis,
+ char *delfrom);
+static int memberof_mod_smod_list(Slapi_PBlock *pb, MemberOfConfig *config, int mod,
+ char *groupdn, Slapi_Mod *smod);
+static int memberof_add_smod_list(Slapi_PBlock *pb, MemberOfConfig *config,
+ char *groupdn, Slapi_Mod *smod);
+static int memberof_del_smod_list(Slapi_PBlock *pb, MemberOfConfig *config,
+ char *groupdn, Slapi_Mod *smod);
+static int memberof_mod_attr_list(Slapi_PBlock *pb, MemberOfConfig *config, int mod,
+ char *groupdn, Slapi_Attr *attr);
+static int memberof_mod_attr_list_r(Slapi_PBlock *pb, MemberOfConfig *config,
+ int mod, char *group_dn, char *op_this, Slapi_Attr *attr, memberofstringll *stack);
+static int memberof_add_attr_list(Slapi_PBlock *pb, MemberOfConfig *config,
+ char *groupdn, Slapi_Attr *attr);
+static int memberof_del_attr_list(Slapi_PBlock *pb, MemberOfConfig *config,
+ char *groupdn, Slapi_Attr *attr);
+static int memberof_moddn_attr_list(Slapi_PBlock *pb, MemberOfConfig *config,
+ char *pre_dn, char *post_dn, Slapi_Attr *attr);
+static int memberof_replace_list(Slapi_PBlock *pb, MemberOfConfig *config, char *group_dn);
+static void memberof_set_plugin_id(void * plugin_id);
+static void *memberof_get_plugin_id();
+static int memberof_compare(MemberOfConfig *config, const void *a, const void *b);
+static int memberof_qsort_compare(const void *a, const void *b);
+static void memberof_load_array(Slapi_Value **array, Slapi_Attr *attr);
+static int memberof_del_dn_from_groups(Slapi_PBlock *pb, MemberOfConfig *config, char *dn);
+static int memberof_call_foreach_dn(Slapi_PBlock *pb, char *dn,
+ char *type, plugin_search_entry_callback callback, void *callback_data);
+static int memberof_is_direct_member(MemberOfConfig *config, Slapi_Value *groupdn,
+ Slapi_Value *memberdn);
+static Slapi_ValueSet *memberof_get_groups(MemberOfConfig *config, char *memberdn);
+static int memberof_get_groups_r(MemberOfConfig *config, char *memberdn,
+ memberof_get_groups_data *data);
+static int memberof_get_groups_callback(Slapi_Entry *e, void *callback_data);
+static int memberof_test_membership(Slapi_PBlock *pb, MemberOfConfig *config,
+ char *group_dn);
+static int memberof_test_membership_callback(Slapi_Entry *e, void *callback_data);
+static int memberof_del_dn_type_callback(Slapi_Entry *e, void *callback_data);
+static int memberof_replace_dn_type_callback(Slapi_Entry *e, void *callback_data);
+static int memberof_replace_dn_from_groups(Slapi_PBlock *pb, MemberOfConfig *config,
+ char *pre_dn, char *post_dn);
+static int memberof_modop_one_replace_r(Slapi_PBlock *pb, MemberOfConfig *config,
+ int mod_op, char *group_dn, char *op_this, char *replace_with, char *op_to,
+ memberofstringll *stack);
+static int memberof_task_add(Slapi_PBlock *pb, Slapi_Entry *e,
+ Slapi_Entry *eAfter, int *returncode, char *returntext,
+ void *arg);
+static void memberof_task_destructor(Slapi_Task *task);
+static const char *fetch_attr(Slapi_Entry *e, const char *attrname,
+ const char *default_val);
+static void memberof_fixup_task_thread(void *arg);
+static int memberof_fix_memberof(MemberOfConfig *config, char *dn, char *filter_str);
+static int memberof_fix_memberof_callback(Slapi_Entry *e, void *callback_data);
+
+
+/*** implementation ***/
+
+
+/*** exported functions ***/
+
+/*
+ * ipamo_postop_init()
+ *
+ * Register plugin call backs
+ *
+ */
+int
+ipamo_postop_init(Slapi_PBlock *pb)
+{
+ int ret = 0;
+ char *memberof_plugin_identity = 0;
+
+ slapi_log_error( SLAPI_LOG_TRACE, MEMBEROF_PLUGIN_SUBSYSTEM,
+ "--> ipamo_postop_init\n" );
+ /*
+ * Get plugin identity and stored it for later use
+ * Used for internal operations
+ */
+
+ slapi_pblock_get (pb, SLAPI_PLUGIN_IDENTITY, &memberof_plugin_identity);
+ PR_ASSERT (memberof_plugin_identity);
+ memberof_set_plugin_id(memberof_plugin_identity);
+
+ if ( slapi_pblock_set( pb, SLAPI_PLUGIN_VERSION,
+ SLAPI_PLUGIN_VERSION_01 ) != 0 ||
+ slapi_pblock_set( pb, SLAPI_PLUGIN_DESCRIPTION,
+ (void *)&pdesc ) != 0 ||
+ slapi_pblock_set( pb, SLAPI_PLUGIN_POST_DELETE_FN,
+ (void *) memberof_postop_del ) != 0 ||
+ slapi_pblock_set( pb, SLAPI_PLUGIN_POST_MODRDN_FN,
+ (void *) memberof_postop_modrdn ) != 0 ||
+ slapi_pblock_set( pb, SLAPI_PLUGIN_POST_MODIFY_FN,
+ (void *) memberof_postop_modify ) != 0 ||
+ slapi_pblock_set( pb, SLAPI_PLUGIN_POST_ADD_FN,
+ (void *) memberof_postop_add ) != 0 ||
+ slapi_pblock_set(pb, SLAPI_PLUGIN_START_FN,
+ (void *) memberof_postop_start ) != 0 ||
+ slapi_pblock_set(pb, SLAPI_PLUGIN_CLOSE_FN,
+ (void *) memberof_postop_close ) != 0)
+ {
+ slapi_log_error( SLAPI_LOG_FATAL, MEMBEROF_PLUGIN_SUBSYSTEM,
+ "ipamo_postop_init failed\n" );
+ ret = -1;
+ }
+
+ slapi_log_error( SLAPI_LOG_TRACE, MEMBEROF_PLUGIN_SUBSYSTEM,
+ "<-- ipamo_postop_init\n" );
+ return ret;
+}
+
+/*
+ * memberof_postop_start()
+ *
+ * Do plugin start up stuff
+ *
+ */
+int memberof_postop_start(Slapi_PBlock *pb)
+{
+ int rc = 0;
+ Slapi_Entry *config_e = NULL; /* entry containing plugin config */
+
+ slapi_log_error( SLAPI_LOG_TRACE, MEMBEROF_PLUGIN_SUBSYSTEM,
+ "--> memberof_postop_start\n" );
+
+ memberof_operation_lock = slapi_new_mutex();
+ if(0 == memberof_operation_lock)
+ {
+ rc = -1;
+ goto bail;
+ }
+
+ if ( slapi_pblock_get( pb, SLAPI_ADD_ENTRY, &config_e ) != 0 ) {
+ slapi_log_error( SLAPI_LOG_FATAL, MEMBEROF_PLUGIN_SUBSYSTEM,
+ "missing config entry\n" );
+ rc = -1;
+ goto bail;
+ }
+
+ if (( rc = memberof_config( config_e )) != LDAP_SUCCESS ) {
+ slapi_log_error( SLAPI_LOG_FATAL, MEMBEROF_PLUGIN_SUBSYSTEM,
+ "configuration failed (%s)\n", ldap_err2string( rc ));
+ return( -1 );
+ }
+
+ rc = slapi_task_register_handler("memberof task", memberof_task_add);
+ if(rc)
+ {
+ goto bail;
+ }
+
+ /*
+ * TODO: start up operation actor thread
+ * need to get to a point where server failure
+ * or shutdown doesn't hose our operations
+ * so we should create a task entry that contains
+ * all required information to complete the operation
+ * then the tasks can be restarted safely if
+ * interrupted
+ */
+
+bail:
+ slapi_log_error( SLAPI_LOG_TRACE, MEMBEROF_PLUGIN_SUBSYSTEM,
+ "<-- memberof_postop_start\n" );
+
+ return rc;
+}
+
+/*
+ * memberof_postop_close()
+ *
+ * Do plugin shut down stuff
+ *
+ */
+int memberof_postop_close(Slapi_PBlock *pb)
+{
+ slapi_log_error( SLAPI_LOG_TRACE, MEMBEROF_PLUGIN_SUBSYSTEM,
+ "--> memberof_postop_close\n" );
+
+
+
+ slapi_log_error( SLAPI_LOG_TRACE, MEMBEROF_PLUGIN_SUBSYSTEM,
+ "<-- memberof_postop_close\n" );
+ return 0;
+}
+
+/*
+ * memberof_postop_del()
+ *
+ * All entries with a memberOf attribute that contains the group DN get retrieved
+ * and have the their memberOf attribute regenerated (it is far too complex and
+ * error prone to attempt to change only those dn values involved in this case -
+ * mainly because the deleted group may itself be a member of other groups which
+ * may be members of other groups etc. in a big recursive mess involving dependency
+ * chains that must be created and traversed in order to decide if an entry should
+ * really have those groups removed too)
+ */
+int memberof_postop_del(Slapi_PBlock *pb)
+{
+ int ret = 0;
+ MemberOfConfig configCopy = {0, 0, 0, 0};
+ char *dn;
+
+ slapi_log_error( SLAPI_LOG_TRACE, MEMBEROF_PLUGIN_SUBSYSTEM,
+ "--> memberof_postop_del\n" );
+
+ if(memberof_oktodo(pb) && (dn = memberof_getdn(pb)))
+ {
+ struct slapi_entry *e = NULL;
+
+ slapi_pblock_get( pb, SLAPI_ENTRY_PRE_OP, &e );
+
+ /* We need to get the config lock first. Trying to get the
+ * config lock after we already hold the op lock can cause
+ * a deadlock. */
+ memberof_rlock_config();
+ /* copy config so it doesn't change out from under us */
+ memberof_copy_config(&configCopy, memberof_get_config());
+ memberof_unlock_config();
+
+ /* get the memberOf operation lock */
+ memberof_lock();
+
+ /* remove this group DN from the
+ * membership lists of groups
+ */
+ memberof_del_dn_from_groups(pb, &configCopy, dn);
+
+ /* is the entry of interest as a group? */
+ if(e && !slapi_filter_test_simple(e, configCopy.group_filter))
+ {
+ Slapi_Attr *attr = 0;
+
+ if(0 == slapi_entry_attr_find(e, configCopy.groupattr, &attr))
+ {
+ memberof_del_attr_list(pb, &configCopy, dn, attr);
+ }
+ }
+
+ memberof_unlock();
+
+ memberof_free_config(&configCopy);
+ }
+
+ slapi_log_error( SLAPI_LOG_TRACE, MEMBEROF_PLUGIN_SUBSYSTEM,
+ "<-- memberof_postop_del\n" );
+ return ret;
+}
+
+typedef struct _memberof_del_dn_data
+{
+ char *dn;
+ char *type;
+} memberof_del_dn_data;
+
+int memberof_del_dn_from_groups(Slapi_PBlock *pb, MemberOfConfig *config, char *dn)
+{
+ memberof_del_dn_data data = {dn, config->groupattr};
+
+ return memberof_call_foreach_dn(pb, dn,
+ config->groupattr, memberof_del_dn_type_callback, &data);
+}
+
+int memberof_del_dn_type_callback(Slapi_Entry *e, void *callback_data)
+{
+ int rc = 0;
+ LDAPMod mod;
+ LDAPMod *mods[2];
+ char *val[2];
+ Slapi_PBlock *mod_pb = 0;
+
+ mod_pb = slapi_pblock_new();
+
+ mods[0] = &mod;
+ mods[1] = 0;
+
+ val[0] = ((memberof_del_dn_data *)callback_data)->dn;
+ val[1] = 0;
+
+ mod.mod_op = LDAP_MOD_DELETE;
+ mod.mod_type = ((memberof_del_dn_data *)callback_data)->type;
+ mod.mod_values = val;
+
+ slapi_modify_internal_set_pb(
+ mod_pb, slapi_entry_get_dn(e),
+ mods, 0, 0,
+ memberof_get_plugin_id(), 0);
+
+ slapi_modify_internal_pb(mod_pb);
+
+ slapi_pblock_get(mod_pb,
+ SLAPI_PLUGIN_INTOP_RESULT,
+ &rc);
+
+ slapi_pblock_destroy(mod_pb);
+
+ return rc;
+}
+
+/*
+ * Does a callback search of "type=dn" under the db suffix that "dn" is in.
+ * If "dn" is a user, you'd want "type" to be "member". If "dn" is a group,
+ * you could want type to be either "member" or "memberOf" depending on the
+ * case.
+ */
+int memberof_call_foreach_dn(Slapi_PBlock *pb, char *dn,
+ char *type, plugin_search_entry_callback callback, void *callback_data)
+{
+ int rc = 0;
+ Slapi_PBlock *search_pb = slapi_pblock_new();
+ Slapi_Backend *be = 0;
+ Slapi_DN *sdn = 0;
+ Slapi_DN *base_sdn = 0;
+ char *filter_str = 0;
+
+ /* get the base dn for the backend we are in
+ (we don't support having members and groups in
+ different backends - issues with offline / read only backends)
+ */
+ sdn = slapi_sdn_new_dn_byref(dn);
+ be = slapi_be_select(sdn);
+ if(be)
+ {
+ base_sdn = (Slapi_DN*)slapi_be_getsuffix(be,0);
+ }
+
+ if(base_sdn)
+ {
+ filter_str = slapi_ch_smprintf("(%s=%s)", type, dn);
+ }
+
+ if(filter_str)
+ {
+ slapi_search_internal_set_pb(search_pb, slapi_sdn_get_dn(base_sdn),
+ LDAP_SCOPE_SUBTREE, filter_str, 0, 0,
+ 0, 0,
+ memberof_get_plugin_id(),
+ 0);
+
+ slapi_search_internal_callback_pb(search_pb,
+ callback_data,
+ 0, callback,
+ 0);
+ }
+
+ slapi_sdn_free(&sdn);
+ slapi_pblock_destroy(search_pb);
+ slapi_ch_free_string(&filter_str);
+ return rc;
+}
+
+/*
+ * memberof_postop_modrdn()
+ *
+ * All entries with a memberOf attribute that contains the old group DN get retrieved
+ * and have the old group DN deleted and the new group DN added to their memberOf attribute
+ */
+int memberof_postop_modrdn(Slapi_PBlock *pb)
+{
+ int ret = 0;
+
+ slapi_log_error( SLAPI_LOG_TRACE, MEMBEROF_PLUGIN_SUBSYSTEM,
+ "--> memberof_postop_modrdn\n" );
+
+ if(memberof_oktodo(pb))
+ {
+ MemberOfConfig *mainConfig = 0;
+ MemberOfConfig configCopy = {0, 0, 0, 0};
+ struct slapi_entry *pre_e = NULL;
+ struct slapi_entry *post_e = NULL;
+ char *pre_dn = 0;
+ char *post_dn = 0;
+ int interested = 0;
+
+ slapi_pblock_get( pb, SLAPI_ENTRY_PRE_OP, &pre_e );
+ slapi_pblock_get( pb, SLAPI_ENTRY_POST_OP, &post_e );
+
+ if(pre_e && post_e)
+ {
+ pre_dn = slapi_entry_get_ndn(pre_e);
+ post_dn = slapi_entry_get_ndn(post_e);
+ }
+
+ /* is the entry of interest? */
+ memberof_rlock_config();
+ mainConfig = memberof_get_config();
+ if(pre_dn && post_dn &&
+ !slapi_filter_test_simple(post_e, mainConfig->group_filter))
+ {
+ interested = 1;
+ /* copy config so it doesn't change out from under us */
+ memberof_copy_config(&configCopy, mainConfig);
+ }
+ memberof_unlock_config();
+
+ if(interested)
+ {
+ Slapi_Attr *attr = 0;
+
+ memberof_lock();
+
+ /* get a list of member attributes present in the group
+ * entry that is being renamed. */
+ if(0 == slapi_entry_attr_find(post_e, configCopy.groupattr, &attr))
+ {
+ memberof_moddn_attr_list(pb, &configCopy, pre_dn, post_dn, attr);
+ }
+
+ /* modrdn must change the dns in groups that have
+ * this group as a member.
+ */
+ memberof_replace_dn_from_groups(pb, &configCopy, pre_dn, post_dn);
+
+ memberof_unlock();
+
+ memberof_free_config(&configCopy);
+ }
+ }
+
+
+ slapi_log_error( SLAPI_LOG_TRACE, MEMBEROF_PLUGIN_SUBSYSTEM,
+ "<-- memberof_postop_modrdn\n" );
+ return ret;
+}
+
+typedef struct _replace_dn_data
+{
+ char *pre_dn;
+ char *post_dn;
+ char *type;
+} replace_dn_data;
+
+int memberof_replace_dn_from_groups(Slapi_PBlock *pb, MemberOfConfig *config,
+ char *pre_dn, char *post_dn)
+{
+ replace_dn_data data = {pre_dn, post_dn, config->groupattr};
+
+ return memberof_call_foreach_dn(pb, pre_dn, config->groupattr,
+ memberof_replace_dn_type_callback, &data);
+}
+
+
+int memberof_replace_dn_type_callback(Slapi_Entry *e, void *callback_data)
+{
+ int rc = 0;
+ LDAPMod delmod;
+ LDAPMod addmod;
+ LDAPMod *mods[3];
+ char *delval[2];
+ char *addval[2];
+ Slapi_PBlock *mod_pb = 0;
+
+ mod_pb = slapi_pblock_new();
+
+ mods[0] = &delmod;
+ mods[1] = &addmod;
+ mods[2] = 0;
+
+ delval[0] = ((replace_dn_data *)callback_data)->pre_dn;
+ delval[1] = 0;
+
+ delmod.mod_op = LDAP_MOD_DELETE;
+ delmod.mod_type = ((replace_dn_data *)callback_data)->type;
+ delmod.mod_values = delval;
+
+ addval[0] = ((replace_dn_data *)callback_data)->post_dn;
+ addval[1] = 0;
+
+ addmod.mod_op = LDAP_MOD_ADD;
+ addmod.mod_type = ((replace_dn_data *)callback_data)->type;
+ addmod.mod_values = addval;
+
+ slapi_modify_internal_set_pb(
+ mod_pb, slapi_entry_get_dn(e),
+ mods, 0, 0,
+ memberof_get_plugin_id(), 0);
+
+ slapi_modify_internal_pb(mod_pb);
+
+ slapi_pblock_get(mod_pb,
+ SLAPI_PLUGIN_INTOP_RESULT,
+ &rc);
+
+ slapi_pblock_destroy(mod_pb);
+
+ return rc;
+}
+
+/*
+ * memberof_postop_modify()
+ *
+ * Added members are retrieved and have the group DN added to their memberOf attribute
+ * Deleted members are retrieved and have the group DN deleted from their memberOf attribute
+ * On replace of the membership attribute values:
+ * 1. Sort old and new values
+ * 2. Iterate through both lists at same time
+ * 3. Any value not in old list but in new list - add group DN to memberOf attribute
+ * 4. Any value in old list but not in new list - remove group DN from memberOf attribute
+ *
+ * Note: this will suck for large groups but nonetheless is optimal (it's linear) given
+ * current restrictions i.e. originally adding members in sorted order would allow
+ * us to sort one list only (the new one) but that is under server control, not this plugin
+ */
+int memberof_postop_modify(Slapi_PBlock *pb)
+{
+ int ret = 0;
+ char *dn = 0;
+ Slapi_Mods *smods = 0;
+ Slapi_Mod *smod = 0;
+ LDAPMod **mods;
+ Slapi_Mod *next_mod = 0;
+
+ slapi_log_error( SLAPI_LOG_TRACE, MEMBEROF_PLUGIN_SUBSYSTEM,
+ "--> memberof_postop_modify\n" );
+
+ if(memberof_oktodo(pb) &&
+ (dn = memberof_getdn(pb)))
+ {
+ int config_copied = 0;
+ MemberOfConfig *mainConfig = 0;
+ MemberOfConfig configCopy = {0, 0, 0, 0};
+
+ /* get the mod set */
+ slapi_pblock_get(pb, SLAPI_MODIFY_MODS, &mods);
+ smods = slapi_mods_new();
+ slapi_mods_init_byref(smods, mods);
+
+ next_mod = slapi_mod_new();
+ smod = slapi_mods_get_first_smod(smods, next_mod);
+ while(smod)
+ {
+ int interested = 0;
+ char *type = (char *)slapi_mod_get_type(smod);
+
+ /* We only want to copy the config if we encounter an
+ * operation that we need to act on. We also want to
+ * only copy the config the first time it's needed so
+ * it remains the same for all mods in the operation,
+ * despite any config changes that may be made. */
+ if (!config_copied)
+ {
+ memberof_rlock_config();
+ mainConfig = memberof_get_config();
+
+ if(slapi_attr_types_equivalent(type, mainConfig->groupattr))
+ {
+ interested = 1;
+ /* copy config so it doesn't change out from under us */
+ memberof_copy_config(&configCopy, mainConfig);
+ config_copied = 1;
+ }
+
+ memberof_unlock_config();
+ } else {
+ if(slapi_attr_types_equivalent(type, configCopy.groupattr))
+ {
+ interested = 1;
+ }
+ }
+
+ if(interested)
+ {
+ int op = slapi_mod_get_operation(smod);
+
+ memberof_lock();
+
+ /* the modify op decides the function */
+ switch(op & ~LDAP_MOD_BVALUES)
+ {
+ case LDAP_MOD_ADD:
+ {
+ /* add group DN to targets */
+ memberof_add_smod_list(pb, &configCopy, dn, smod);
+ break;
+ }
+
+ case LDAP_MOD_DELETE:
+ {
+ /* If there are no values in the smod, we should
+ * just do a replace instead. The user is just
+ * trying to delete all members from this group
+ * entry, which the replace code deals with. */
+ if (slapi_mod_get_num_values(smod) == 0)
+ {
+ memberof_replace_list(pb, &configCopy, dn);
+ }
+ else
+ {
+ /* remove group DN from target values in smod*/
+ memberof_del_smod_list(pb, &configCopy, dn, smod);
+ }
+ break;
+ }
+
+ case LDAP_MOD_REPLACE:
+ {
+ /* replace current values */
+ memberof_replace_list(pb, &configCopy, dn);
+ break;
+ }
+
+ default:
+ {
+ slapi_log_error(
+ SLAPI_LOG_PLUGIN,
+ MEMBEROF_PLUGIN_SUBSYSTEM,
+ "memberof_postop_modify: unknown mod type\n" );
+ break;
+ }
+ }
+
+ memberof_unlock();
+ }
+
+ slapi_mod_done(next_mod);
+ smod = slapi_mods_get_next_smod(smods, next_mod);
+ }
+
+ if (config_copied)
+ {
+ memberof_free_config(&configCopy);
+ }
+
+ slapi_mod_free(&next_mod);
+ slapi_mods_free(&smods);
+ }
+
+ slapi_log_error( SLAPI_LOG_TRACE, MEMBEROF_PLUGIN_SUBSYSTEM,
+ "<-- memberof_postop_modify\n" );
+ return ret;
+}
+
+
+/*
+ * memberof_postop_add()
+ *
+ * All members in the membership attribute of the new entry get retrieved
+ * and have the group DN added to their memberOf attribute
+ */
+int memberof_postop_add(Slapi_PBlock *pb)
+{
+ int ret = 0;
+ int interested = 0;
+ char *dn = 0;
+
+ slapi_log_error( SLAPI_LOG_TRACE, MEMBEROF_PLUGIN_SUBSYSTEM,
+ "--> memberof_postop_add\n" );
+
+ if(memberof_oktodo(pb) && (dn = memberof_getdn(pb)))
+ {
+ MemberOfConfig *mainConfig = 0;
+ MemberOfConfig configCopy = {0, 0, 0, 0};
+ struct slapi_entry *e = NULL;
+
+ slapi_pblock_get( pb, SLAPI_ENTRY_POST_OP, &e );
+
+
+ /* is the entry of interest? */
+ memberof_rlock_config();
+ mainConfig = memberof_get_config();
+ if(e && !slapi_filter_test_simple(e, mainConfig->group_filter))
+ {
+ interested = 1;
+ /* copy config so it doesn't change out from under us */
+ memberof_copy_config(&configCopy, mainConfig);
+ }
+ memberof_unlock_config();
+
+ if(interested)
+ {
+ Slapi_Attr *attr = 0;
+
+ memberof_lock();
+
+ if(0 == slapi_entry_attr_find(e, configCopy.groupattr, &attr))
+ {
+ memberof_add_attr_list(pb, &configCopy, dn, attr);
+ }
+
+ memberof_unlock();
+
+ memberof_free_config(&configCopy);
+ }
+ }
+
+ slapi_log_error( SLAPI_LOG_TRACE, MEMBEROF_PLUGIN_SUBSYSTEM,
+ "<-- memberof_postop_add\n" );
+ return ret;
+}
+
+/*** Support functions ***/
+
+/*
+ * memberof_oktodo()
+ *
+ * Check that the op succeeded
+ * Note: we also respond to replicated ops so we don't test for that
+ * this does require that the memberOf attribute not be replicated
+ * and this means that memberof is consistent with local state
+ * not the network system state
+ *
+ */
+int memberof_oktodo(Slapi_PBlock *pb)
+{
+ int ret = 1;
+ int oprc = 0;
+
+ slapi_log_error( SLAPI_LOG_TRACE, MEMBEROF_PLUGIN_SUBSYSTEM,
+ "--> memberof_postop_oktodo\n" );
+
+ if(slapi_pblock_get(pb, SLAPI_PLUGIN_OPRETURN, &oprc) != 0)
+ {
+ slapi_log_error( SLAPI_LOG_FATAL, MEMBEROF_PLUGIN_SUBSYSTEM,
+ "memberof_postop_oktodo: could not get parameters\n" );
+ ret = -1;
+ }
+
+ /* this plugin should only execute if the operation succeeded
+ */
+ if(oprc != 0)
+ {
+ ret = 0;
+ }
+
+ slapi_log_error( SLAPI_LOG_TRACE, MEMBEROF_PLUGIN_SUBSYSTEM,
+ "<-- memberof_postop_oktodo\n" );
+
+ return ret;
+}
+
+/*
+ * memberof_getdn()
+ *
+ * Get dn of target entry
+ *
+ */
+char *memberof_getdn(Slapi_PBlock *pb)
+{
+ char *dn = 0;
+
+ slapi_pblock_get(pb, SLAPI_TARGET_DN, &dn);
+
+ return dn;
+}
+
+/*
+ * memberof_modop_one()
+ *
+ * Perform op on memberof attribute of op_to using op_this as the value
+ * However, if op_to happens to be a group, we must arrange for the group
+ * members to have the mod performed on them instead, and we must take
+ * care to not recurse when we have visted a group before
+ *
+ * Also, we must not delete entries that are a member of the group
+ */
+int memberof_modop_one(Slapi_PBlock *pb, MemberOfConfig *config, int mod_op,
+ char *op_this, char *op_to)
+{
+ return memberof_modop_one_r(pb, config, mod_op, op_this, op_this, op_to, 0);
+}
+
+/* memberof_modop_one_r()
+ *
+ * recursive function to perform above (most things don't need the replace arg)
+ */
+
+int memberof_modop_one_r(Slapi_PBlock *pb, MemberOfConfig *config, int mod_op,
+ char *group_dn, char *op_this, char *op_to, memberofstringll *stack)
+{
+ return memberof_modop_one_replace_r(
+ pb, config, mod_op, group_dn, op_this, 0, op_to, stack);
+}
+
+/* memberof_modop_one_replace_r()
+ *
+ * recursive function to perform above (with added replace arg)
+ */
+int memberof_modop_one_replace_r(Slapi_PBlock *pb, MemberOfConfig *config,
+ int mod_op, char *group_dn, char *op_this, char *replace_with,
+ char *op_to, memberofstringll *stack)
+{
+ int rc = 0;
+ LDAPMod mod;
+ LDAPMod replace_mod;
+ LDAPMod *mods[3];
+ char *val[2];
+ char *replace_val[2];
+ Slapi_PBlock *mod_pb = 0;
+ char *attrlist[2] = {config->groupattr,0};
+ Slapi_DN *op_to_sdn = 0;
+ Slapi_Entry *e = 0;
+ memberofstringll *ll = 0;
+ char *op_str = 0;
+ Slapi_Value *to_dn_val = slapi_value_new_string(op_to);
+ Slapi_Value *this_dn_val = slapi_value_new_string(op_this);
+
+ /* determine if this is a group op or single entry */
+ op_to_sdn = slapi_sdn_new_dn_byref(op_to);
+ slapi_search_internal_get_entry( op_to_sdn, attrlist,
+ &e, memberof_get_plugin_id());
+ if(!e)
+ {
+ /* In the case of a delete, we need to worry about the
+ * missing entry being a nested group. There's a small
+ * window where another thread may have deleted a nested
+ * group that our group_dn entry refers to. This has the
+ * potential of us missing some indirect member entries
+ * that need to be updated. */
+ if(LDAP_MOD_DELETE == mod_op)
+ {
+ Slapi_PBlock *search_pb = slapi_pblock_new();
+ Slapi_DN *base_sdn = 0;
+ Slapi_Backend *be = 0;
+ char *filter_str = 0;
+ int n_entries = 0;
+
+ /* We can't tell for sure if the op_to entry is a
+ * user or a group since the entry doesn't exist
+ * anymore. We can safely ignore the missing entry
+ * if no other entries have a memberOf attribute that
+ * points to the missing entry. */
+ be = slapi_be_select(op_to_sdn);
+ if(be)
+ {
+ base_sdn = (Slapi_DN*)slapi_be_getsuffix(be,0);
+ }
+
+ if(base_sdn)
+ {
+ filter_str = slapi_ch_smprintf("(%s=%s)",
+ config->memberof_attr, op_to);
+ }
+
+ if(filter_str)
+ {
+ slapi_search_internal_set_pb(search_pb, slapi_sdn_get_dn(base_sdn),
+ LDAP_SCOPE_SUBTREE, filter_str, 0, 0, 0, 0,
+ memberof_get_plugin_id(), 0);
+
+ if (slapi_search_internal_pb(search_pb))
+ {
+ /* get result and log an error */
+ int res = 0;
+ slapi_pblock_get(search_pb, SLAPI_PLUGIN_INTOP_RESULT, &res);
+ slapi_log_error( SLAPI_LOG_FATAL, MEMBEROF_PLUGIN_SUBSYSTEM,
+ "memberof_modop_one_replace_r: error searching for members: "
+ "%d", res);
+ } else {
+ slapi_pblock_get(search_pb, SLAPI_NENTRIES, &n_entries);
+
+ if(n_entries > 0)
+ {
+ /* We want to fixup the membership for the
+ * entries that referred to the missing group
+ * entry. This will fix the references to
+ * the missing group as well as the group
+ * represented by op_this. */
+ memberof_test_membership(pb, config, op_to);
+ }
+ }
+
+ slapi_free_search_results_internal(search_pb);
+ slapi_ch_free_string(&filter_str);
+ }
+
+ slapi_pblock_destroy(search_pb);
+ }
+
+ goto bail;
+ }
+
+ if(LDAP_MOD_DELETE == mod_op)
+ {
+ op_str = "DELETE";
+ }
+ else if(LDAP_MOD_ADD == mod_op)
+ {
+ op_str = "ADD";
+ }
+ else if(LDAP_MOD_REPLACE == mod_op)
+ {
+ op_str = "REPLACE";
+ }
+ else
+ {
+ op_str = "UNKNOWN";
+ }
+
+ slapi_log_error( SLAPI_LOG_PLUGIN, MEMBEROF_PLUGIN_SUBSYSTEM,
+ "memberof_modop_one_replace_r: %s %s in %s\n"
+ ,op_str, op_this, op_to);
+
+ if(!slapi_filter_test_simple(e, config->group_filter))
+ {
+ /* group */
+ Slapi_Value *ll_dn_val = 0;
+ Slapi_Attr *members = 0;
+
+ ll = stack;
+
+ /* have we been here before? */
+ while(ll)
+ {
+ ll_dn_val = slapi_value_new_string(ll->dn);
+
+ if(0 == memberof_compare(config, &ll_dn_val, &to_dn_val))
+ {
+ slapi_value_free(&ll_dn_val);
+
+ /* someone set up infinitely
+ recursive groups - bail out */
+ slapi_log_error( SLAPI_LOG_PLUGIN,
+ MEMBEROF_PLUGIN_SUBSYSTEM,
+ "memberof_modop_one_replace_r: group recursion"
+ " detected in %s\n"
+ ,op_to);
+ goto bail;
+ }
+
+ slapi_value_free(&ll_dn_val);
+ ll = ll->next;
+ }
+
+ /* do op on group */
+ slapi_log_error( SLAPI_LOG_PLUGIN,
+ MEMBEROF_PLUGIN_SUBSYSTEM,
+ "memberof_modop_one_replace_r: descending into group %s\n",
+ op_to);
+ /* Add the nested group's DN to the stack so we can detect loops later. */
+ ll = (memberofstringll*)slapi_ch_malloc(sizeof(memberofstringll));
+ ll->dn = op_to;
+ ll->next = stack;
+
+ slapi_entry_attr_find( e, config->groupattr, &members );
+ if(members)
+ {
+ memberof_mod_attr_list_r(pb, config, mod_op, group_dn, op_this, members, ll);
+ }
+
+ {
+ /* crazyness follows:
+ * strict-aliasing doesn't like the required cast
+ * to void for slapi_ch_free so we are made to
+ * juggle to get a normal thing done
+ */
+ void *pll = ll;
+ slapi_ch_free(&pll);
+ ll = 0;
+ }
+ }
+ /* continue with operation */
+ {
+ /* We want to avoid listing a group as a memberOf itself
+ * in case someone set up a circular grouping.
+ */
+ if (0 == memberof_compare(config, &this_dn_val, &to_dn_val))
+ {
+ slapi_log_error( SLAPI_LOG_PLUGIN,
+ MEMBEROF_PLUGIN_SUBSYSTEM,
+ "memberof_modop_one_replace_r: not processing memberOf "
+ "operations on self entry: %s\n", this_dn_val);
+ goto bail;
+ }
+
+ /* For add and del modify operations, we just regenerate the
+ * memberOf attribute. */
+ if(LDAP_MOD_DELETE == mod_op || LDAP_MOD_ADD == mod_op)
+ {
+ /* find parent groups and replace our member attr */
+ memberof_fix_memberof_callback(e, config);
+ } else {
+ /* single entry - do mod */
+ mod_pb = slapi_pblock_new();
+
+ mods[0] = &mod;
+ if(LDAP_MOD_REPLACE == mod_op)
+ {
+ mods[1] = &replace_mod;
+ mods[2] = 0;
+ }
+ else
+ {
+ mods[1] = 0;
+ }
+
+ val[0] = op_this;
+ val[1] = 0;
+ mod.mod_op = LDAP_MOD_REPLACE == mod_op?LDAP_MOD_DELETE:mod_op;
+ mod.mod_type = config->memberof_attr;
+ mod.mod_values = val;
+
+ if(LDAP_MOD_REPLACE == mod_op)
+ {
+ replace_val[0] = replace_with;
+ replace_val[1] = 0;
+
+ replace_mod.mod_op = LDAP_MOD_ADD;
+ replace_mod.mod_type = config->memberof_attr;
+ replace_mod.mod_values = replace_val;
+ }
+
+ slapi_modify_internal_set_pb(
+ mod_pb, op_to,
+ mods, 0, 0,
+ memberof_get_plugin_id(), 0);
+
+ slapi_modify_internal_pb(mod_pb);
+
+ slapi_pblock_get(mod_pb,
+ SLAPI_PLUGIN_INTOP_RESULT,
+ &rc);
+
+ slapi_pblock_destroy(mod_pb);
+ }
+ }
+
+bail:
+ slapi_sdn_free(&op_to_sdn);
+ slapi_value_free(&to_dn_val);
+ slapi_value_free(&this_dn_val);
+ slapi_entry_free(e);
+ return rc;
+}
+
+
+/*
+ * memberof_add_one()
+ *
+ * Add addthis DN to the memberof attribute of addto
+ *
+ */
+int memberof_add_one(Slapi_PBlock *pb, MemberOfConfig *config, char *addthis, char *addto)
+{
+ return memberof_modop_one(pb, config, LDAP_MOD_ADD, addthis, addto);
+}
+
+/*
+ * memberof_del_one()
+ *
+ * Delete delthis DN from the memberof attribute of delfrom
+ *
+ */
+int memberof_del_one(Slapi_PBlock *pb, MemberOfConfig *config, char *delthis, char *delfrom)
+{
+ return memberof_modop_one(pb, config, LDAP_MOD_DELETE, delthis, delfrom);
+}
+
+/*
+ * memberof_mod_smod_list()
+ *
+ * Perform mod for group DN to the memberof attribute of the list of targets
+ *
+ */
+int memberof_mod_smod_list(Slapi_PBlock *pb, MemberOfConfig *config, int mod,
+ char *group_dn, Slapi_Mod *smod)
+{
+ int rc = 0;
+ struct berval *bv = slapi_mod_get_first_value(smod);
+ int last_size = 0;
+ char *last_str = 0;
+
+ while(bv)
+ {
+ char *dn_str = 0;
+
+ if(last_size > bv->bv_len)
+ {
+ dn_str = last_str;
+ }
+ else
+ {
+ int the_size = (bv->bv_len * 2) + 1;
+
+ if(last_str)
+ slapi_ch_free_string(&last_str);
+
+ dn_str = (char*)slapi_ch_malloc(the_size);
+
+ last_str = dn_str;
+ last_size = the_size;
+ }
+
+ memset(dn_str, 0, last_size);
+
+ strncpy(dn_str, bv->bv_val, (size_t)bv->bv_len);
+
+ memberof_modop_one(pb, config, mod, group_dn, dn_str);
+
+ bv = slapi_mod_get_next_value(smod);
+ }
+
+ if(last_str)
+ slapi_ch_free_string(&last_str);
+
+ return rc;
+}
+
+/*
+ * memberof_add_smod_list()
+ *
+ * Add group DN to the memberof attribute of the list of targets
+ *
+ */
+int memberof_add_smod_list(Slapi_PBlock *pb, MemberOfConfig *config,
+ char *groupdn, Slapi_Mod *smod)
+{
+ return memberof_mod_smod_list(pb, config, LDAP_MOD_ADD, groupdn, smod);
+}
+
+
+/*
+ * memberof_del_smod_list()
+ *
+ * Remove group DN from the memberof attribute of the list of targets
+ *
+ */
+int memberof_del_smod_list(Slapi_PBlock *pb, MemberOfConfig *config,
+ char *groupdn, Slapi_Mod *smod)
+{
+ return memberof_mod_smod_list(pb, config, LDAP_MOD_DELETE, groupdn, smod);
+}
+
+/**
+ * Plugin identity mgmt
+ */
+void memberof_set_plugin_id(void * plugin_id)
+{
+ _PluginID=plugin_id;
+}
+
+void * memberof_get_plugin_id()
+{
+ return _PluginID;
+}
+
+
+/*
+ * memberof_mod_attr_list()
+ *
+ * Perform mod for group DN to the memberof attribute of the list of targets
+ *
+ */
+int memberof_mod_attr_list(Slapi_PBlock *pb, MemberOfConfig *config, int mod,
+ char *group_dn, Slapi_Attr *attr)
+{
+ return memberof_mod_attr_list_r(pb, config, mod, group_dn, group_dn, attr, 0);
+}
+
+int memberof_mod_attr_list_r(Slapi_PBlock *pb, MemberOfConfig *config, int mod,
+ char *group_dn, char *op_this, Slapi_Attr *attr, memberofstringll *stack)
+{
+ int rc = 0;
+ Slapi_Value *val = 0;
+ Slapi_Value *op_this_val = 0;
+ int last_size = 0;
+ char *last_str = 0;
+ int hint = slapi_attr_first_value(attr, &val);
+
+ op_this_val = slapi_value_new_string(op_this);
+
+ while(val)
+ {
+ char *dn_str = 0;
+ struct berval *bv = 0;
+
+ /* We don't want to process a memberOf operation on ourselves. */
+ if(0 != memberof_compare(config, &val, &op_this_val))
+ {
+ bv = (struct berval *)slapi_value_get_berval(val);
+
+ if(last_size > bv->bv_len)
+ {
+ dn_str = last_str;
+ }
+ else
+ {
+ int the_size = (bv->bv_len * 2) + 1;
+
+ if(last_str)
+ slapi_ch_free_string(&last_str);
+
+ dn_str = (char*)slapi_ch_malloc(the_size);
+
+ last_str = dn_str;
+ last_size = the_size;
+ }
+
+ memset(dn_str, 0, last_size);
+
+ strncpy(dn_str, bv->bv_val, (size_t)bv->bv_len);
+
+ /* If we're doing a replace (as we would in the MODRDN case), we need
+ * to specify the new group DN value */
+ if(mod == LDAP_MOD_REPLACE)
+ {
+ memberof_modop_one_replace_r(pb, config, mod, group_dn, op_this,
+ group_dn, dn_str, stack);
+ }
+ else
+ {
+ memberof_modop_one_r(pb, config, mod, group_dn, op_this, dn_str, stack);
+ }
+ }
+
+ hint = slapi_attr_next_value(attr, hint, &val);
+ }
+
+ slapi_value_free(&op_this_val);
+
+ if(last_str)
+ slapi_ch_free_string(&last_str);
+
+ return rc;
+}
+
+/*
+ * memberof_add_attr_list()
+ *
+ * Add group DN to the memberof attribute of the list of targets
+ *
+ */
+int memberof_add_attr_list(Slapi_PBlock *pb, MemberOfConfig *config, char *groupdn,
+ Slapi_Attr *attr)
+{
+ return memberof_mod_attr_list(pb, config, LDAP_MOD_ADD, groupdn, attr);
+}
+
+/*
+ * memberof_del_attr_list()
+ *
+ * Remove group DN from the memberof attribute of the list of targets
+ *
+ */
+int memberof_del_attr_list(Slapi_PBlock *pb, MemberOfConfig *config, char *groupdn,
+ Slapi_Attr *attr)
+{
+ return memberof_mod_attr_list(pb, config, LDAP_MOD_DELETE, groupdn, attr);
+}
+
+/*
+ * memberof_moddn_attr_list()
+ *
+ * Perform mod for group DN to the memberof attribute of the list of targets
+ *
+ */
+int memberof_moddn_attr_list(Slapi_PBlock *pb, MemberOfConfig *config,
+ char *pre_dn, char *post_dn, Slapi_Attr *attr)
+{
+ int rc = 0;
+ Slapi_Value *val = 0;
+ int last_size = 0;
+ char *last_str = 0;
+ int hint = slapi_attr_first_value(attr, &val);
+
+ while(val)
+ {
+ char *dn_str = 0;
+ struct berval *bv = (struct berval *)slapi_value_get_berval(val);
+
+ if(last_size > bv->bv_len)
+ {
+ dn_str = last_str;
+ }
+ else
+ {
+ int the_size = (bv->bv_len * 2) + 1;
+
+ if(last_str)
+ slapi_ch_free_string(&last_str);
+
+ dn_str = (char*)slapi_ch_malloc(the_size);
+
+ last_str = dn_str;
+ last_size = the_size;
+ }
+
+ memset(dn_str, 0, last_size);
+
+ strncpy(dn_str, bv->bv_val, (size_t)bv->bv_len);
+
+ memberof_modop_one_replace_r(pb, config, LDAP_MOD_REPLACE,
+ post_dn, pre_dn, post_dn, dn_str, 0);
+
+ hint = slapi_attr_next_value(attr, hint, &val);
+ }
+
+ if(last_str)
+ slapi_ch_free_string(&last_str);
+
+ return rc;
+}
+
+/* memberof_get_groups()
+ *
+ * Gets a list of all groups that an entry is a member of.
+ * This is done by looking only at member attribute values.
+ * A Slapi_ValueSet* is returned. It is up to the caller to
+ * free it.
+ */
+Slapi_ValueSet *memberof_get_groups(MemberOfConfig *config, char *memberdn)
+{
+ Slapi_Value *memberdn_val = slapi_value_new_string(memberdn);
+ Slapi_ValueSet *groupvals = slapi_valueset_new();
+ memberof_get_groups_data data = {config, memberdn_val, &groupvals};
+
+ memberof_get_groups_r(config, memberdn, &data);
+
+ slapi_value_free(&memberdn_val);
+
+ return groupvals;
+}
+
+int memberof_get_groups_r(MemberOfConfig *config, char *memberdn, memberof_get_groups_data *data)
+{
+ /* Search for member=<memberdn>
+ * For each match, add it to the list, recurse and do same search */
+ return memberof_call_foreach_dn(NULL, memberdn, config->groupattr,
+ memberof_get_groups_callback, data);
+}
+
+/* memberof_get_groups_callback()
+ *
+ * Callback to perform work of memberof_get_groups()
+ */
+int memberof_get_groups_callback(Slapi_Entry *e, void *callback_data)
+{
+ char *group_dn = slapi_entry_get_dn(e);
+ Slapi_Value *group_dn_val = 0;
+ Slapi_ValueSet *groupvals = *((memberof_get_groups_data*)callback_data)->groupvals;
+
+ /* get the DN of the group */
+ group_dn_val = slapi_value_new_string(group_dn);
+
+ /* check if e is the same as our original member entry */
+ if (0 == memberof_compare(((memberof_get_groups_data*)callback_data)->config,
+ &((memberof_get_groups_data*)callback_data)->memberdn_val, &group_dn_val))
+ {
+ /* A recursive group caused us to find our original
+ * entry we passed to memberof_get_groups(). We just
+ * skip processing this entry. */
+ slapi_log_error( SLAPI_LOG_PLUGIN, MEMBEROF_PLUGIN_SUBSYSTEM,
+ "memberof_get_groups_callback: group recursion"
+ " detected in %s\n" ,group_dn);
+ slapi_value_free(&group_dn_val);
+ goto bail;
+
+ }
+
+ /* have we been here before? */
+ if (groupvals &&
+ slapi_valueset_find(((memberof_get_groups_data*)callback_data)->config->group_slapiattr,
+ groupvals, group_dn_val))
+ {
+ /* we either hit a recursive grouping, or an entry is
+ * a member of a group through multiple paths. Either
+ * way, we can just skip processing this entry since we've
+ * already gone through this part of the grouping hierarchy. */
+ slapi_log_error( SLAPI_LOG_PLUGIN, MEMBEROF_PLUGIN_SUBSYSTEM,
+ "memberof_get_groups_callback: possible group recursion"
+ " detected in %s\n" ,group_dn);
+ slapi_value_free(&group_dn_val);
+ goto bail;
+ }
+
+ /* Push group_dn_val into the valueset. This memory is now owned
+ * by the valueset. */
+ slapi_valueset_add_value_ext(groupvals, group_dn_val, SLAPI_VALUE_FLAG_PASSIN);
+
+ /* now recurse to find parent groups of e */
+ memberof_get_groups_r(((memberof_get_groups_data*)callback_data)->config,
+ group_dn, callback_data);
+
+ bail:
+ return 0;
+}
+
+/* memberof_is_direct_member()
+ *
+ * tests for direct membership of memberdn in group groupdn
+ * returns non-zero when true, zero otherwise
+ */
+int memberof_is_direct_member(MemberOfConfig *config, Slapi_Value *groupdn,
+ Slapi_Value *memberdn)
+{
+ int rc = 0;
+ Slapi_DN *sdn = 0;
+ char *attrlist[2] = {config->groupattr,0};
+ Slapi_Entry *group_e = 0;
+ Slapi_Attr *attr = 0;
+
+ sdn = slapi_sdn_new_dn_byref(slapi_value_get_string(groupdn));
+
+ slapi_search_internal_get_entry(sdn, attrlist,
+ &group_e, memberof_get_plugin_id());
+
+ if(group_e)
+ {
+ slapi_entry_attr_find(group_e, config->groupattr, &attr );
+ if(attr)
+ {
+ rc = 0 == slapi_attr_value_find(
+ attr, slapi_value_get_berval(memberdn));
+ }
+ slapi_entry_free(group_e);
+ }
+
+ slapi_sdn_free(&sdn);
+ return rc;
+}
+
+/* memberof_test_membership()
+ *
+ * Finds all entries who are a "memberOf" the group
+ * represented by "group_dn". For each matching entry, we
+ * call memberof_test_membership_callback().
+ *
+ * for each attribute in the memberof attribute
+ * determine if the entry is still a member.
+ *
+ * test each for direct membership
+ * move groups entry is memberof to member group
+ * test remaining groups for membership in member groups
+ * iterate until a pass fails to move a group over to member groups
+ * remaining groups should be deleted
+ */
+int memberof_test_membership(Slapi_PBlock *pb, MemberOfConfig *config, char *group_dn)
+{
+ return memberof_call_foreach_dn(pb, group_dn, config->memberof_attr,
+ memberof_test_membership_callback , config);
+}
+
+/*
+ * memberof_test_membership_callback()
+ *
+ * A callback function to do the work of memberof_test_membership().
+ * Note that this not only tests membership, but updates the memberOf
+ * attributes in the entry to be correct.
+ */
+int memberof_test_membership_callback(Slapi_Entry *e, void *callback_data)
+{
+ int rc = 0;
+ Slapi_Attr *attr = 0;
+ int total = 0;
+ Slapi_Value **member_array = 0;
+ Slapi_Value **candidate_array = 0;
+ Slapi_Value *entry_dn = 0;
+ MemberOfConfig *config = (MemberOfConfig *)callback_data;
+
+ entry_dn = slapi_value_new_string(slapi_entry_get_dn(e));
+
+ if(0 == entry_dn)
+ {
+ goto bail;
+ }
+
+ /* divide groups into member and non-member lists */
+ slapi_entry_attr_find(e, config->memberof_attr, &attr );
+ if(attr)
+ {
+ slapi_attr_get_numvalues( attr, &total);
+ if(total)
+ {
+ Slapi_Value *val = 0;
+ int hint = 0;
+ int c_index = 0;
+ int m_index = 0;
+ int member_found = 1;
+ int outer_index = 0;
+
+ candidate_array =
+ (Slapi_Value**)
+ slapi_ch_malloc(sizeof(Slapi_Value*)*total);
+ memset(candidate_array, 0, sizeof(Slapi_Value*)*total);
+ member_array =
+ (Slapi_Value**)
+ slapi_ch_malloc(sizeof(Slapi_Value*)*total);
+ memset(member_array, 0, sizeof(Slapi_Value*)*total);
+
+ hint = slapi_attr_first_value(attr, &val);
+
+ while(val)
+ {
+ /* test for direct membership */
+ if(memberof_is_direct_member(config, val, entry_dn))
+ {
+ /* it is a member */
+ member_array[m_index] = val;
+ m_index++;
+ }
+ else
+ {
+ /* not a member, still a candidate */
+ candidate_array[c_index] = val;
+ c_index++;
+ }
+
+ hint = slapi_attr_next_value(attr, hint, &val);
+ }
+
+ /* now iterate over members testing for membership
+ in candidate groups and moving candidates to members
+ when successful, quit when a full iteration adds no
+ new members
+ */
+ while(member_found)
+ {
+ member_found = 0;
+
+ /* For each group that this entry is a verified member of, see if
+ * any of the candidate groups are members. If they are, add them
+ * to the list of verified groups that this entry is a member of.
+ */
+ while(outer_index < m_index)
+ {
+ int inner_index = 0;
+
+ while(inner_index < c_index)
+ {
+ /* Check for a special value in this position
+ * that indicates that the candidate was moved
+ * to the member array. */
+ if((void*)1 ==
+ candidate_array[inner_index])
+ {
+ /* was moved, skip */
+ inner_index++;
+ continue;
+ }
+
+ if(memberof_is_direct_member(
+ config,
+ candidate_array[inner_index],
+ member_array[outer_index]))
+ {
+ member_array[m_index] =
+ candidate_array
+ [inner_index];
+ m_index++;
+
+ candidate_array[inner_index] =
+ (void*)1;
+
+ member_found = 1;
+ }
+
+ inner_index++;
+ }
+
+ outer_index++;
+ }
+ }
+
+ /* here we are left only with values to delete
+ from the memberof attribute in the candidate list
+ */
+ outer_index = 0;
+ while(outer_index < c_index)
+ {
+ /* Check for a special value in this position
+ * that indicates that the candidate was moved
+ * to the member array. */
+ if((void*)1 == candidate_array[outer_index])
+ {
+ /* item moved, skip */
+ outer_index++;
+ continue;
+ }
+
+ memberof_del_one(
+ 0, config,
+ (char*)slapi_value_get_string(
+ candidate_array[outer_index]),
+ (char*)slapi_value_get_string(entry_dn));
+
+ outer_index++;
+ }
+ {
+ /* crazyness follows:
+ * strict-aliasing doesn't like the required cast
+ * to void for slapi_ch_free so we are made to
+ * juggle to get a normal thing done
+ */
+ void *pmember_array = member_array;
+ void *pcandidate_array = candidate_array;
+ slapi_ch_free(&pcandidate_array);
+ slapi_ch_free(&pmember_array);
+ candidate_array = 0;
+ member_array = 0;
+ }
+ }
+ }
+
+bail:
+ slapi_value_free(&entry_dn);
+
+ return rc;
+}
+
+/*
+ * memberof_replace_list()
+ *
+ * Perform replace the group DN list in the memberof attribute of the list of targets
+ *
+ */
+int memberof_replace_list(Slapi_PBlock *pb, MemberOfConfig *config, char *group_dn)
+{
+ struct slapi_entry *pre_e = NULL;
+ struct slapi_entry *post_e = NULL;
+ Slapi_Attr *pre_attr = 0;
+ Slapi_Attr *post_attr = 0;
+
+ slapi_pblock_get( pb, SLAPI_ENTRY_PRE_OP, &pre_e );
+ slapi_pblock_get( pb, SLAPI_ENTRY_POST_OP, &post_e );
+
+ if(pre_e && post_e)
+ {
+ slapi_entry_attr_find( pre_e, config->groupattr, &pre_attr );
+ slapi_entry_attr_find( post_e, config->groupattr, &post_attr );
+ }
+
+ if(pre_attr || post_attr)
+ {
+ int pre_total = 0;
+ int post_total = 0;
+ Slapi_Value **pre_array = 0;
+ Slapi_Value **post_array = 0;
+ int pre_index = 0;
+ int post_index = 0;
+
+ /* create arrays of values */
+ if(pre_attr)
+ {
+ slapi_attr_get_numvalues( pre_attr, &pre_total);
+ }
+
+ if(post_attr)
+ {
+ slapi_attr_get_numvalues( post_attr, &post_total);
+ }
+
+ /* Stash a plugin global pointer here and have memberof_qsort_compare
+ * use it. We have to do this because we use memberof_qsort_compare
+ * as the comparator function for qsort, which requires the function
+ * to only take two void* args. This is thread-safe since we only
+ * store and use the pointer while holding the memberOf operation
+ * lock. */
+ qsortConfig = config;
+
+ if(pre_total)
+ {
+ pre_array =
+ (Slapi_Value**)
+ slapi_ch_malloc(sizeof(Slapi_Value*)*pre_total);
+ memberof_load_array(pre_array, pre_attr);
+ qsort(
+ pre_array,
+ pre_total,
+ sizeof(Slapi_Value*),
+ memberof_qsort_compare);
+ }
+
+ if(post_total)
+ {
+ post_array =
+ (Slapi_Value**)
+ slapi_ch_malloc(sizeof(Slapi_Value*)*post_total);
+ memberof_load_array(post_array, post_attr);
+ qsort(
+ post_array,
+ post_total,
+ sizeof(Slapi_Value*),
+ memberof_qsort_compare);
+ }
+
+ qsortConfig = 0;
+
+
+ /* work through arrays, following these rules:
+ in pre, in post, do nothing
+ in pre, not in post, delete from entry
+ not in pre, in post, add to entry
+ */
+ while(pre_index < pre_total || post_index < post_total)
+ {
+ if(pre_index == pre_total)
+ {
+ /* add the rest of post */
+ memberof_add_one(
+ pb, config,
+ group_dn,
+ (char*)slapi_value_get_string(
+ post_array[post_index]));
+
+ post_index++;
+ }
+ else if(post_index == post_total)
+ {
+ /* delete the rest of pre */
+ memberof_del_one(
+ pb, config,
+ group_dn,
+ (char*)slapi_value_get_string(
+ pre_array[pre_index]));
+
+ pre_index++;
+ }
+ else
+ {
+ /* decide what to do */
+ int cmp = memberof_compare(
+ config,
+ &(pre_array[pre_index]),
+ &(post_array[post_index]));
+
+ if(cmp < 0)
+ {
+ /* delete pre array */
+ memberof_del_one(
+ pb, config,
+ group_dn,
+ (char*)slapi_value_get_string(
+ pre_array[pre_index]));
+
+ pre_index++;
+ }
+ else if(cmp > 0)
+ {
+ /* add post array */
+ memberof_add_one(
+ pb, config,
+ group_dn,
+ (char*)slapi_value_get_string(
+ post_array[post_index]));
+
+ post_index++;
+ }
+ else
+ {
+ /* do nothing, advance */
+ pre_index++;
+ post_index++;
+ }
+ }
+ }
+ slapi_ch_free((void **)&pre_array);
+ slapi_ch_free((void **)&post_array);
+ }
+
+ return 0;
+}
+
+/* memberof_load_array()
+ *
+ * put attribute values in array structure
+ */
+void memberof_load_array(Slapi_Value **array, Slapi_Attr *attr)
+{
+ Slapi_Value *val = 0;
+ int hint = slapi_attr_first_value(attr, &val);
+
+ while(val)
+ {
+ *array = val;
+ array++;
+ hint = slapi_attr_next_value(attr, hint, &val);
+ }
+}
+
+/* memberof_compare()
+ *
+ * compare two attr values
+ */
+int memberof_compare(MemberOfConfig *config, const void *a, const void *b)
+{
+ Slapi_Value *val1 = *((Slapi_Value **)a);
+ Slapi_Value *val2 = *((Slapi_Value **)b);
+
+ return slapi_attr_value_cmp(
+ config->group_slapiattr,
+ slapi_value_get_berval(val1),
+ slapi_value_get_berval(val2));
+}
+
+/* memberof_qsort_compare()
+ *
+ * This is a version of memberof_compare that uses a plugin
+ * global copy of the config. We'd prefer to pass in a copy
+ * of config that is local to the running thread, but we can't
+ * do this since qsort is using us as a comparator function.
+ * We should only use this function when using qsort, and only
+ * when the memberOf lock is acquired.
+ */
+int memberof_qsort_compare(const void *a, const void *b)
+{
+ Slapi_Value *val1 = *((Slapi_Value **)a);
+ Slapi_Value *val2 = *((Slapi_Value **)b);
+
+ return slapi_attr_value_cmp(
+ qsortConfig->group_slapiattr,
+ slapi_value_get_berval(val1),
+ slapi_value_get_berval(val2));
+}
+
+void memberof_lock()
+{
+ slapi_lock_mutex(memberof_operation_lock);
+}
+
+void memberof_unlock()
+{
+ slapi_unlock_mutex(memberof_operation_lock);
+}
+
+typedef struct _task_data
+{
+ char *dn;
+ char *filter_str;
+} task_data;
+
+void memberof_fixup_task_thread(void *arg)
+{
+ MemberOfConfig configCopy = {0, 0, 0, 0};
+ Slapi_Task *task = (Slapi_Task *)arg;
+ task_data *td = NULL;
+ int rc = 0;
+
+ /* Fetch our task data from the task */
+ td = (task_data *)slapi_task_get_data(task);
+
+ slapi_task_begin(task, 1);
+ slapi_task_log_notice(task, "Memberof task starts (arg: %s) ...\n",
+ td->filter_str);
+
+ /* We need to get the config lock first. Trying to get the
+ * config lock after we already hold the op lock can cause
+ * a deadlock. */
+ memberof_rlock_config();
+ /* copy config so it doesn't change out from under us */
+ memberof_copy_config(&configCopy, memberof_get_config());
+ memberof_unlock_config();
+
+ /* get the memberOf operation lock */
+ memberof_lock();
+
+ /* do real work */
+ rc = memberof_fix_memberof(&configCopy, td->dn, td->filter_str);
+
+ /* release the memberOf operation lock */
+ memberof_unlock();
+
+ memberof_free_config(&configCopy);
+
+ slapi_task_log_notice(task, "Memberof task finished.");
+ slapi_task_log_status(task, "Memberof task finished.");
+ slapi_task_inc_progress(task);
+
+ /* this will queue the destruction of the task */
+ slapi_task_finish(task, rc);
+}
+
+/* extract a single value from the entry (as a string) -- if it's not in the
+ * entry, the default will be returned (which can be NULL).
+ * you do not need to free anything returned by this.
+ */
+const char *fetch_attr(Slapi_Entry *e, const char *attrname,
+ const char *default_val)
+{
+ Slapi_Attr *attr;
+ Slapi_Value *val = NULL;
+
+ if (slapi_entry_attr_find(e, attrname, &attr) != 0)
+ return default_val;
+ slapi_attr_first_value(attr, &val);
+ return slapi_value_get_string(val);
+}
+
+int memberof_task_add(Slapi_PBlock *pb, Slapi_Entry *e,
+ Slapi_Entry *eAfter, int *returncode, char *returntext,
+ void *arg)
+{
+ PRThread *thread = NULL;
+ int rv = SLAPI_DSE_CALLBACK_OK;
+ task_data *mytaskdata = NULL;
+ Slapi_Task *task = NULL;
+ const char *filter;
+ const char *dn = 0;
+
+ *returncode = LDAP_SUCCESS;
+ /* get arg(s) */
+ if ((dn = fetch_attr(e, "basedn", 0)) == NULL)
+ {
+ *returncode = LDAP_OBJECT_CLASS_VIOLATION;
+ rv = SLAPI_DSE_CALLBACK_ERROR;
+ goto out;
+ }
+
+ if ((filter = fetch_attr(e, "filter", "(objectclass=inetuser)")) == NULL)
+ {
+ *returncode = LDAP_OBJECT_CLASS_VIOLATION;
+ rv = SLAPI_DSE_CALLBACK_ERROR;
+ goto out;
+ }
+
+ /* setup our task data */
+ mytaskdata = (task_data*)slapi_ch_malloc(sizeof(task_data));
+ if (mytaskdata == NULL)
+ {
+ *returncode = LDAP_OPERATIONS_ERROR;
+ rv = SLAPI_DSE_CALLBACK_ERROR;
+ goto out;
+ }
+ mytaskdata->dn = slapi_ch_strdup(dn);
+ mytaskdata->filter_str = slapi_ch_strdup(filter);
+
+ /* allocate new task now */
+ task = slapi_new_task(slapi_entry_get_ndn(e));
+
+ /* register our destructor for cleaning up our private data */
+ slapi_task_set_destructor_fn(task, memberof_task_destructor);
+
+ /* Stash a pointer to our data in the task */
+ slapi_task_set_data(task, mytaskdata);
+
+ /* start the sample task as a separate thread */
+ thread = PR_CreateThread(PR_USER_THREAD, memberof_fixup_task_thread,
+ (void *)task, PR_PRIORITY_NORMAL, PR_GLOBAL_THREAD,
+ PR_UNJOINABLE_THREAD, SLAPD_DEFAULT_THREAD_STACKSIZE);
+ if (thread == NULL)
+ {
+ slapi_log_error( SLAPI_LOG_FATAL, MEMBEROF_PLUGIN_SUBSYSTEM,
+ "unable to create task thread!\n");
+ *returncode = LDAP_OPERATIONS_ERROR;
+ rv = SLAPI_DSE_CALLBACK_ERROR;
+ slapi_task_finish(task, *returncode);
+ } else {
+ rv = SLAPI_DSE_CALLBACK_OK;
+ }
+
+out:
+ return rv;
+}
+
+void
+memberof_task_destructor(Slapi_Task *task)
+{
+ if (task) {
+ task_data *mydata = (task_data *)slapi_task_get_data(task);
+ if (mydata) {
+ slapi_ch_free_string(&mydata->dn);
+ slapi_ch_free_string(&mydata->filter_str);
+ /* Need to cast to avoid a compiler warning */
+ slapi_ch_free((void **)&mydata);
+ }
+ }
+}
+
+int memberof_fix_memberof(MemberOfConfig *config, char *dn, char *filter_str)
+{
+ int rc = 0;
+ Slapi_PBlock *search_pb = slapi_pblock_new();
+
+ slapi_search_internal_set_pb(search_pb, dn,
+ LDAP_SCOPE_SUBTREE, filter_str, 0, 0,
+ 0, 0,
+ memberof_get_plugin_id(),
+ 0);
+
+ rc = slapi_search_internal_callback_pb(search_pb,
+ config,
+ 0, memberof_fix_memberof_callback,
+ 0);
+
+ slapi_pblock_destroy(search_pb);
+
+ return rc;
+}
+
+/* memberof_fix_memberof_callback()
+ * Add initial and/or fix up broken group list in entry
+ *
+ * 1. Remove all present memberOf values
+ * 2. Add direct group membership memberOf values
+ * 3. Add indirect group membership memberOf values
+ */
+int memberof_fix_memberof_callback(Slapi_Entry *e, void *callback_data)
+{
+ int rc = 0;
+ char *dn = slapi_entry_get_dn(e);
+ MemberOfConfig *config = (MemberOfConfig *)callback_data;
+ memberof_del_dn_data del_data = {0, config->memberof_attr};
+ Slapi_ValueSet *groups = 0;
+
+ /* get a list of all of the groups this user belongs to */
+ groups = memberof_get_groups(config, dn);
+
+ /* If we found some groups, replace the existing memberOf attribute
+ * with the found values. */
+ if (groups && slapi_valueset_count(groups))
+ {
+ Slapi_PBlock *mod_pb = slapi_pblock_new();
+ Slapi_Value *val = 0;
+ Slapi_Mod *smod;
+ LDAPMod **mods = (LDAPMod **) slapi_ch_malloc(2 * sizeof(LDAPMod *));
+ int hint = 0;
+
+ /* NGK - need to allocate the smod */
+ smod = slapi_mod_new();
+ slapi_mod_init(smod, 0);
+ slapi_mod_set_operation(smod, LDAP_MOD_REPLACE | LDAP_MOD_BVALUES);
+ slapi_mod_set_type(smod, config->memberof_attr);
+
+ /* Loop through all of our values and add them to smod */
+ hint = slapi_valueset_first_value(groups, &val);
+ while (val)
+ {
+ /* this makes a copy of the berval */
+ slapi_mod_add_value(smod, slapi_value_get_berval(val));
+ hint = slapi_valueset_next_value(groups, hint, &val);
+ }
+
+ mods[0] = slapi_mod_get_ldapmod_passout(smod);
+ mods[1] = 0;
+
+ slapi_modify_internal_set_pb(
+ mod_pb, dn, mods, 0, 0,
+ memberof_get_plugin_id(), 0);
+
+ slapi_modify_internal_pb(mod_pb);
+
+ slapi_pblock_get(mod_pb, SLAPI_PLUGIN_INTOP_RESULT, &rc);
+
+ ldap_mods_free(mods, 1);
+ slapi_mod_free(&smod);
+ /* NGK - need to free the smod */
+ slapi_pblock_destroy(mod_pb);
+ } else {
+ /* No groups were found, so remove the memberOf attribute
+ * from this entry. */
+ memberof_del_dn_type_callback(e, &del_data);
+ }
+
+ slapi_valueset_free(groups);
+
+ return rc;
+}
+
diff --git a/ipa-server/ipa-slapi-plugins/ipa-memberof/ipa-memberof.h b/ipa-server/ipa-slapi-plugins/ipa-memberof/ipa-memberof.h
new file mode 100644
index 00000000..3e7b5cf4
--- /dev/null
+++ b/ipa-server/ipa-slapi-plugins/ipa-memberof/ipa-memberof.h
@@ -0,0 +1,100 @@
+/** BEGIN COPYRIGHT BLOCK
+ * This Program is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free Software
+ * Foundation; version 2 of the License.
+ *
+ * This Program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * this Program; if not, write to the Free Software Foundation, Inc., 59 Temple
+ * Place, Suite 330, Boston, MA 02111-1307 USA.
+ *
+ * In addition, as a special exception, Red Hat, Inc. gives You the additional
+ * right to link the code of this Program with code not covered under the GNU
+ * General Public License ("Non-GPL Code") and to distribute linked combinations
+ * including the two, subject to the limitations in this paragraph. Non-GPL Code
+ * permitted under this exception must only link to the code of this Program
+ * through those well defined interfaces identified in the file named EXCEPTION
+ * found in the source code files (the "Approved Interfaces"). The files of
+ * Non-GPL Code may instantiate templates or use macros or inline functions from
+ * the Approved Interfaces without causing the resulting work to be covered by
+ * the GNU General Public License. Only Red Hat, Inc. may make changes or
+ * additions to the list of Approved Interfaces. You must obey the GNU General
+ * Public License in all respects for all of the Program code and other code used
+ * in conjunction with the Program except the Non-GPL Code covered by this
+ * exception. If you modify this file, you may extend this exception to your
+ * version of the file, but you are not obligated to do so. If you do not wish to
+ * provide this exception without modification, you must delete this exception
+ * statement from your version and license this file solely under the GPL without
+ * exception.
+ *
+ *
+ * Copyright (C) 2008 Red Hat, Inc.
+ * All rights reserved.
+ * END COPYRIGHT BLOCK **/
+
+#ifdef HAVE_CONFIG_H
+# include <config.h>
+#endif
+
+/*
+ * ipa-memberof.h - memberOf shared definitions
+ *
+ */
+
+#ifndef _MEMBEROF_H_
+#define _MEMBEROF_H_
+
+#include <stdio.h>
+#include <string.h>
+#include <time.h>
+#include <sys/types.h>
+#include <dirsrv/slapi-plugin.h>
+#include <nspr.h>
+
+/****** secrets *********/
+/*from FDS slapi-private.h
+ * until we get a proper api for access
+ */
+#define SLAPI_DSE_CALLBACK_OK (1)
+#define SLAPI_DSE_CALLBACK_ERROR (-1)
+#define SLAPI_DSE_CALLBACK_DO_NOT_APPLY (0)
+#define SLAPI_DSE_RETURNTEXT_SIZE 512
+#define DSE_FLAG_PREOP 0x0002
+/*********** end secrets **********/
+/*
+ * macros
+ */
+#define MEMBEROF_PLUGIN_SUBSYSTEM "ipa-memberof-plugin" /* used for logging */
+#define MEMBEROF_GROUP_ATTR "member"
+#define MEMBEROF_ATTR "memberOf"
+
+
+/*
+ * structs
+ */
+typedef struct memberofconfig {
+ char *groupattr;
+ char *memberof_attr;
+ Slapi_Filter *group_filter;
+ Slapi_Attr *group_slapiattr;
+} MemberOfConfig;
+
+
+/*
+ * functions
+ */
+int memberof_config(Slapi_Entry *config_e);
+void memberof_copy_config(MemberOfConfig *dest, MemberOfConfig *src);
+void memberof_free_config(MemberOfConfig *config);
+MemberOfConfig *memberof_get_config();
+void memberof_lock();
+void memberof_unlock();
+void memberof_rlock_config();
+void memberof_wlock_config();
+void memberof_unlock_config();
+
+
+#endif /* _MEMBEROF_H_ */
diff --git a/ipa-server/ipa-slapi-plugins/ipa-memberof/ipa-memberof_config.c b/ipa-server/ipa-slapi-plugins/ipa-memberof/ipa-memberof_config.c
new file mode 100644
index 00000000..b2bd374a
--- /dev/null
+++ b/ipa-server/ipa-slapi-plugins/ipa-memberof/ipa-memberof_config.c
@@ -0,0 +1,312 @@
+/** BEGIN COPYRIGHT BLOCK
+ * This Program is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free Software
+ * Foundation; version 2 of the License.
+ *
+ * This Program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * this Program; if not, write to the Free Software Foundation, Inc., 59 Temple
+ * Place, Suite 330, Boston, MA 02111-1307 USA.
+ *
+ * In addition, as a special exception, Red Hat, Inc. gives You the additional
+ * right to link the code of this Program with code not covered under the GNU
+ * General Public License ("Non-GPL Code") and to distribute linked combinations
+ * including the two, subject to the limitations in this paragraph. Non-GPL Code
+ * permitted under this exception must only link to the code of this Program
+ * through those well defined interfaces identified in the file named EXCEPTION
+ * found in the source code files (the "Approved Interfaces"). The files of
+ * Non-GPL Code may instantiate templates or use macros or inline functions from
+ * the Approved Interfaces without causing the resulting work to be covered by
+ * the GNU General Public License. Only Red Hat, Inc. may make changes or
+ * additions to the list of Approved Interfaces. You must obey the GNU General
+ * Public License in all respects for all of the Program code and other code used
+ * in conjunction with the Program except the Non-GPL Code covered by this
+ * exception. If you modify this file, you may extend this exception to your
+ * version of the file, but you are not obligated to do so. If you do not wish to
+ * provide this exception without modification, you must delete this exception
+ * statement from your version and license this file solely under the GPL without
+ * exception.
+ *
+ *
+ * Copyright (C) 2008 Red Hat, Inc.
+ * All rights reserved.
+ * END COPYRIGHT BLOCK **/
+
+#ifdef HAVE_CONFIG_H
+# include <config.h>
+#endif
+
+/*
+ * memberof_config.c - configuration-related code for memberOf plug-in
+ *
+ */
+
+#include <plstr.h>
+
+#include "ipa-memberof.h"
+
+#define MEMBEROF_CONFIG_FILTER "(objectclass=*)"
+
+/*
+ * The configuration attributes are contained in the plugin entry e.g.
+ * cn=MemberOf Plugin,cn=plugins,cn=config
+ *
+ * Configuration is a two step process. The first pass is a validation step which
+ * occurs pre-op - check inputs and error out if bad. The second pass actually
+ * applies the changes to the run time config.
+ */
+
+
+/*
+ * function prototypes
+ */
+static int memberof_apply_config (Slapi_PBlock *pb, Slapi_Entry* entryBefore, Slapi_Entry* e,
+ int *returncode, char *returntext, void *arg);
+static int memberof_search (Slapi_PBlock *pb, Slapi_Entry* entryBefore, Slapi_Entry* e,
+ int *returncode, char *returntext, void *arg)
+{
+ return SLAPI_DSE_CALLBACK_OK;
+}
+
+/*
+ * static variables
+ */
+/* This is the main configuration which is updated from dse.ldif. The
+ * config will be copied when it is used by the plug-in to prevent it
+ * being changed out from under a running memberOf operation. */
+static MemberOfConfig theConfig;
+static PRRWLock *memberof_config_lock = 0;
+static int inited = 0;
+
+
+static int dont_allow_that(Slapi_PBlock *pb, Slapi_Entry* entryBefore, Slapi_Entry* e,
+ int *returncode, char *returntext, void *arg)
+{
+ *returncode = LDAP_UNWILLING_TO_PERFORM;
+ return SLAPI_DSE_CALLBACK_ERROR;
+}
+
+/*
+ * memberof_config()
+ *
+ * Read configuration and create a configuration data structure.
+ * This is called after the server has configured itself so we can
+ * perform checks with regards to suffixes if it ever becomes
+ * necessary.
+ * Returns an LDAP error code (LDAP_SUCCESS if all goes well).
+ */
+int
+memberof_config(Slapi_Entry *config_e)
+{
+ int returncode = LDAP_SUCCESS;
+ char returntext[SLAPI_DSE_RETURNTEXT_SIZE];
+
+ if ( inited ) {
+ slapi_log_error( SLAPI_LOG_FATAL, MEMBEROF_PLUGIN_SUBSYSTEM,
+ "only one memberOf plugin instance can be used\n" );
+ return( LDAP_PARAM_ERROR );
+ }
+
+ /* initialize the RW lock to protect the main config */
+ memberof_config_lock = PR_NewRWLock(PR_RWLOCK_RANK_NONE, "memberof_config_lock");
+
+ /* initialize fields */
+ memberof_apply_config(NULL, NULL, config_e,
+ &returncode, returntext, NULL);
+
+ /* config DSE must be initialized before we get here */
+ if (returncode == LDAP_SUCCESS) {
+ const char *config_dn = slapi_entry_get_dn_const(config_e);
+ slapi_config_register_callback(SLAPI_OPERATION_MODIFY, DSE_FLAG_PREOP,
+ config_dn, LDAP_SCOPE_BASE, MEMBEROF_CONFIG_FILTER,
+ dont_allow_that,NULL);
+ slapi_config_register_callback(SLAPI_OPERATION_MODRDN, DSE_FLAG_PREOP,
+ config_dn, LDAP_SCOPE_BASE, MEMBEROF_CONFIG_FILTER,
+ dont_allow_that, NULL);
+ slapi_config_register_callback(SLAPI_OPERATION_DELETE, DSE_FLAG_PREOP,
+ config_dn, LDAP_SCOPE_BASE, MEMBEROF_CONFIG_FILTER,
+ dont_allow_that, NULL);
+ slapi_config_register_callback(SLAPI_OPERATION_SEARCH, DSE_FLAG_PREOP,
+ config_dn, LDAP_SCOPE_BASE, MEMBEROF_CONFIG_FILTER,
+ memberof_search,NULL);
+ }
+
+ inited = 1;
+
+ if (returncode != LDAP_SUCCESS) {
+ slapi_log_error(SLAPI_LOG_FATAL, MEMBEROF_PLUGIN_SUBSYSTEM,
+ "Error %d: %s\n", returncode, returntext);
+ }
+
+ return returncode;
+}
+
+
+/*
+ * memberof_apply_config()
+ *
+ * Just use hardcoded config values.
+ */
+static int
+memberof_apply_config (Slapi_PBlock *pb, Slapi_Entry* entryBefore, Slapi_Entry* e,
+ int *returncode, char *returntext, void *arg)
+{
+ char *groupattr = NULL;
+ char *memberof_attr = NULL;
+ char *filter_str = NULL;
+
+ *returncode = LDAP_SUCCESS;
+
+ groupattr = slapi_ch_strdup(MEMBEROF_GROUP_ATTR);
+ memberof_attr = slapi_ch_strdup(MEMBEROF_ATTR);
+
+ /* We want to be sure we don't change the config in the middle of
+ * a memberOf operation, so we obtain an exclusive lock here */
+ memberof_wlock_config();
+
+ if (!theConfig.groupattr ||
+ (groupattr && PL_strcmp(theConfig.groupattr, groupattr))) {
+ slapi_ch_free_string(&theConfig.groupattr);
+ theConfig.groupattr = groupattr;
+ groupattr = NULL; /* config now owns memory */
+
+ /* We allocate a Slapi_Attr using the groupattr for
+ * convenience in our memberOf comparison functions */
+ slapi_attr_free(&theConfig.group_slapiattr);
+ theConfig.group_slapiattr = slapi_attr_new();
+ slapi_attr_init(theConfig.group_slapiattr, theConfig.groupattr);
+
+ /* The filter is based off of the groupattr, so we
+ * update it here too. */
+ slapi_filter_free(theConfig.group_filter, 1);
+ filter_str = slapi_ch_smprintf("(%s=*)", theConfig.groupattr);
+ theConfig.group_filter = slapi_str2filter(filter_str);
+ slapi_ch_free_string(&filter_str);
+ }
+
+ if (!theConfig.memberof_attr ||
+ (memberof_attr && PL_strcmp(theConfig.memberof_attr, memberof_attr))) {
+ slapi_ch_free_string(&theConfig.memberof_attr);
+ theConfig.memberof_attr = memberof_attr;
+ memberof_attr = NULL; /* config now owns memory */
+ }
+
+ /* release the lock */
+ memberof_unlock_config();
+
+ slapi_ch_free_string(&groupattr);
+ slapi_ch_free_string(&memberof_attr);
+
+ if (*returncode != LDAP_SUCCESS)
+ {
+ return SLAPI_DSE_CALLBACK_ERROR;
+ }
+ else
+ {
+ return SLAPI_DSE_CALLBACK_OK;
+ }
+}
+
+/*
+ * memberof_copy_config()
+ *
+ * Makes a copy of the config in src. This function will free the
+ * elements of dest if they already exist. This should only be called
+ * if you hold the memberof config lock if src was obtained with
+ * memberof_get_config().
+ */
+void
+memberof_copy_config(MemberOfConfig *dest, MemberOfConfig *src)
+{
+ if (dest && src)
+ {
+ /* Check if the copy is already up to date */
+ if (!dest->groupattr || (src->groupattr
+ && PL_strcmp(dest->groupattr, src->groupattr)))
+ {
+ slapi_ch_free_string(&dest->groupattr);
+ dest->groupattr = slapi_ch_strdup(src->groupattr);
+ slapi_filter_free(dest->group_filter, 1);
+ dest->group_filter = slapi_filter_dup(src->group_filter);
+ slapi_attr_free(&dest->group_slapiattr);
+ dest->group_slapiattr = slapi_attr_dup(src->group_slapiattr);
+ }
+
+ if (!dest->memberof_attr || (src->memberof_attr
+ && PL_strcmp(dest->memberof_attr, src->memberof_attr)))
+ {
+ slapi_ch_free_string(&dest->memberof_attr);
+ dest->memberof_attr = slapi_ch_strdup(src->memberof_attr);
+ }
+ }
+}
+
+/*
+ * memberof_free_config()
+ *
+ * Free's the contents of a config structure.
+ */
+void
+memberof_free_config(MemberOfConfig *config)
+{
+ if (config)
+ {
+ slapi_ch_free_string(&config->groupattr);
+ slapi_filter_free(config->group_filter, 1);
+ slapi_attr_free(&config->group_slapiattr);
+ slapi_ch_free_string(&config->memberof_attr);
+ }
+}
+
+/*
+ * memberof_get_config()
+ *
+ * Returns a pointer to the main config. You should call
+ * memberof_rlock_config() first so the main config doesn't
+ * get modified out from under you.
+ */
+MemberOfConfig *
+memberof_get_config()
+{
+ return &theConfig;
+}
+
+/*
+ * memberof_rlock_config()
+ *
+ * Gets a non-exclusive lock on the main config. This will
+ * prevent the config from being changed out from under you
+ * while you read it, but it will still allow other threads
+ * to read the config at the same time.
+ */
+void
+memberof_rlock_config()
+{
+ PR_RWLock_Rlock(memberof_config_lock);
+}
+
+/*
+ * memberof_wlock_config()
+ *
+ * Gets an exclusive lock on the main config. This should
+ * be called if you need to write to the main config.
+ */
+void
+memberof_wlock_config()
+{
+ PR_RWLock_Wlock(memberof_config_lock);
+}
+
+/*
+ * memberof_unlock_config()
+ *
+ * Unlocks the main config.
+ */
+void
+memberof_unlock_config()
+{
+ PR_RWLock_Unlock(memberof_config_lock);
+}
diff --git a/ipa-server/ipa-slapi-plugins/ipa-memberof/memberof-conf.ldif b/ipa-server/ipa-slapi-plugins/ipa-memberof/memberof-conf.ldif
new file mode 100644
index 00000000..1441afea
--- /dev/null
+++ b/ipa-server/ipa-slapi-plugins/ipa-memberof/memberof-conf.ldif
@@ -0,0 +1,14 @@
+dn: cn=ipa-memberof,cn=plugins,cn=config
+changetype: add
+objectclass: top
+objectclass: nsSlapdPlugin
+objectclass: extensibleObject
+cn: ipa-memberof
+nsslapd-pluginpath: libipa-memberof-plugin
+nsslapd-plugininitfunc: ipamo_postop_init
+nsslapd-plugintype: postoperation
+nsslapd-pluginenabled: on
+nsslapd-pluginid: memberof
+nsslapd-pluginversion: 1.0
+nsslapd-pluginvendor: Red Hat
+nsslapd-plugindescription: Memberof plugin
diff --git a/ipa-server/ipa-slapi-plugins/ipa-pwd-extop/Makefile.am b/ipa-server/ipa-slapi-plugins/ipa-pwd-extop/Makefile.am
new file mode 100644
index 00000000..540646f0
--- /dev/null
+++ b/ipa-server/ipa-slapi-plugins/ipa-pwd-extop/Makefile.am
@@ -0,0 +1,46 @@
+NULL =
+
+INCLUDES = \
+ -I. \
+ -I$(srcdir) \
+ -DPREFIX=\""$(prefix)"\" \
+ -DBINDIR=\""$(bindir)"\" \
+ -DLIBDIR=\""$(libdir)"\" \
+ -DLIBEXECDIR=\""$(libexecdir)"\" \
+ -DDATADIR=\""$(datadir)"\" \
+ $(MOZLDAP_CFLAGS) \
+ $(KRB5_CFLAGS) \
+ $(SSL_CFLAGS) \
+ $(WARN_CFLAGS) \
+ $(NULL)
+
+plugindir = $(libdir)/dirsrv/plugins
+plugin_LTLIBRARIES = \
+ libipa_pwd_extop.la \
+ $(NULL)
+
+libipa_pwd_extop_la_SOURCES = \
+ ipa_pwd_extop.c \
+ $(NULL)
+
+libipa_pwd_extop_la_LDFLAGS = -avoid-version
+
+libipa_pwd_extop_la_LIBADD = \
+ $(KRB5_LIBS) \
+ $(SSL_LIBS) \
+ $(MOZLDAP_LIBS) \
+ $(NULL)
+
+appdir = $(IPA_DATA_DIR)
+app_DATA = \
+ pwd-extop-conf.ldif \
+ $(NULL)
+
+EXTRA_DIST = \
+ README \
+ $(app_DATA) \
+ $(NULL)
+
+MAINTAINERCLEANFILES = \
+ *~ \
+ Makefile.in
diff --git a/ipa-server/ipa-slapi-plugins/ipa-pwd-extop/README b/ipa-server/ipa-slapi-plugins/ipa-pwd-extop/README
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/ipa-server/ipa-slapi-plugins/ipa-pwd-extop/README
diff --git a/ipa-server/ipa-slapi-plugins/ipa-pwd-extop/ipa_pwd_extop.c b/ipa-server/ipa-slapi-plugins/ipa-pwd-extop/ipa_pwd_extop.c
new file mode 100644
index 00000000..24acc887
--- /dev/null
+++ b/ipa-server/ipa-slapi-plugins/ipa-pwd-extop/ipa_pwd_extop.c
@@ -0,0 +1,4058 @@
+/** BEGIN COPYRIGHT BLOCK
+ * This Program is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free Software
+ * Foundation; version 2 of the License.
+ *
+ * This Program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * this Program; if not, write to the Free Software Foundation, Inc., 59 Temple
+ * Place, Suite 330, Boston, MA 02111-1307 USA.
+ *
+ * In addition, as a special exception, Red Hat, Inc. gives You the additional
+ * right to link the code of this Program with code not covered under the GNU
+ * General Public License ("Non-GPL Code") and to distribute linked combinations
+ * including the two, subject to the limitations in this paragraph. Non-GPL Code
+ * permitted under this exception must only link to the code of this Program
+ * through those well defined interfaces identified in the file named EXCEPTION
+ * found in the source code files (the "Approved Interfaces"). The files of
+ * Non-GPL Code may instantiate templates or use macros or inline functions from
+ * the Approved Interfaces without causing the resulting work to be covered by
+ * the GNU General Public License. Only Red Hat, Inc. may make changes or
+ * additions to the list of Approved Interfaces. You must obey the GNU General
+ * Public License in all respects for all of the Program code and other code
+ * used in conjunction with the Program except the Non-GPL Code covered by this
+ * exception. If you modify this file, you may extend this exception to your
+ * version of the file, but you are not obligated to do so. If you do not wish
+ * to provide this exception without modification, you must delete this
+ * exception statement from your version and license this file solely under the
+ * GPL without exception.
+ *
+ * Authors:
+ * Simo Sorce <ssorce@redhat.com>
+ *
+ * Copyright (C) 2005 Red Hat, Inc.
+ * All rights reserved.
+ * END COPYRIGHT BLOCK **/
+
+#ifdef HAVE_CONFIG_H
+# include <config.h>
+#endif
+
+/*
+ * Password Modify - LDAP Extended Operation.
+ * RFC 3062
+ *
+ *
+ * This plugin implements the "Password Modify - LDAP3"
+ * extended operation for LDAP. The plugin function is called by
+ * the server if an LDAP client request contains the OID:
+ * "1.3.6.1.4.1.4203.1.11.1".
+ *
+ */
+
+#include <stdio.h>
+#include <string.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+
+#include <prio.h>
+#include <ssl.h>
+#include <dirsrv/slapi-plugin.h>
+#define KRB5_PRIVATE 1
+#include <krb5.h>
+#include <lber.h>
+#include <time.h>
+#include <iconv.h>
+#include <openssl/des.h>
+#include <openssl/md4.h>
+
+/* Type of connection for this operation;*/
+#define LDAP_EXTOP_PASSMOD_CONN_SECURE
+
+/* Uncomment the following #undef FOR TESTING:
+ * allows non-SSL connections to use the password change extended op */
+/* #undef LDAP_EXTOP_PASSMOD_CONN_SECURE */
+
+/* ber tags for the PasswdModifyRequestValue sequence */
+#define LDAP_EXTOP_PASSMOD_TAG_USERID 0x80U
+#define LDAP_EXTOP_PASSMOD_TAG_OLDPWD 0x81U
+#define LDAP_EXTOP_PASSMOD_TAG_NEWPWD 0x82U
+
+/* ber tags for the PasswdModifyResponseValue sequence */
+#define LDAP_EXTOP_PASSMOD_TAG_GENPWD 0x80U
+
+/* OID of the extended operation handled by this plug-in */
+#define EXOP_PASSWD_OID "1.3.6.1.4.1.4203.1.11.1"
+
+/* OID to retrieve keytabs */
+#define KEYTAB_SET_OID "2.16.840.1.113730.3.8.3.1"
+#define KEYTAB_RET_OID "2.16.840.1.113730.3.8.3.2"
+
+/* krbTicketFlags */
+#define KTF_DISALLOW_POSTDATED 0x00000001
+#define KTF_DISALLOW_FORWARDABLE 0x00000002
+#define KTF_DISALLOW_TGT_BASED 0x00000004
+#define KTF_DISALLOW_RENEWABLE 0x00000008
+#define KTF_DISALLOW_PROXIABLE 0x00000010
+#define KTF_DISALLOW_DUP_SKEY 0x00000020
+#define KTF_DISALLOW_ALL_TIX 0x00000040
+#define KTF_REQUIRES_PRE_AUTH 0x00000080
+#define KTF_REQUIRES_HW_AUTH 0x00000100
+#define KTF_REQUIRES_PWCHANGE 0x00000200
+#define KTF_DISALLOW_SVR 0x00001000
+#define KTF_PWCHANGE_SERVICE 0x00002000
+
+/* These are the default enc:salt types if nothing is defined.
+ * TODO: retrieve the configure set of ecntypes either from the
+ * kfc.conf file or by synchronizing the the file content into
+ * the directory */
+
+/* Salt types */
+#define KRB5_KDB_SALTTYPE_NORMAL 0
+#define KRB5_KDB_SALTTYPE_V4 1
+#define KRB5_KDB_SALTTYPE_NOREALM 2
+#define KRB5_KDB_SALTTYPE_ONLYREALM 3
+#define KRB5_KDB_SALTTYPE_SPECIAL 4
+#define KRB5_KDB_SALTTYPE_AFS3 5
+
+#define KRB5P_SALT_SIZE 16
+
+void krb5int_c_free_keyblock_contents(krb5_context context, register krb5_keyblock *key);
+
+static const char *ipapwd_def_encsalts[] = {
+ "des3-hmac-sha1:normal",
+/* "arcfour-hmac:normal",
+ "des-hmac-sha1:normal",
+ "des-cbc-md5:normal", */
+ "des-cbc-crc:normal",
+/* "des-cbc-crc:v4",
+ "des-cbc-crc:afs3", */
+ NULL
+};
+
+struct ipapwd_encsalt {
+ krb5_int32 enc_type;
+ krb5_int32 salt_type;
+};
+
+static const char *ipa_realm_dn;
+static const char *ipa_pwd_config_dn;
+static const char *ipa_changepw_principal_dn;
+
+#define IPAPWD_PLUGIN_NAME "ipa-pwd-extop"
+#define IPAPWD_FEATURE_DESC "IPA Password Manager"
+#define IPAPWD_PLUGIN_DESC "IPA Password Extended Operation plugin"
+
+static Slapi_PluginDesc pdesc = {
+ IPAPWD_FEATURE_DESC,
+ "FreeIPA project",
+ "FreeIPA/1.0",
+ IPAPWD_PLUGIN_DESC
+};
+
+static void *ipapwd_plugin_id;
+
+#define IPA_CHANGETYPE_NORMAL 0
+#define IPA_CHANGETYPE_ADMIN 1
+#define IPA_CHANGETYPE_DSMGR 2
+
+struct ipapwd_krbcfg {
+ krb5_context krbctx;
+ char *realm;
+ krb5_keyblock *kmkey;
+ int num_supp_encsalts;
+ struct ipapwd_encsalt *supp_encsalts;
+ int num_pref_encsalts;
+ struct ipapwd_encsalt *pref_encsalts;
+ char **passsync_mgrs;
+ int num_passsync_mgrs;
+};
+
+static void free_ipapwd_krbcfg(struct ipapwd_krbcfg **cfg)
+{
+ struct ipapwd_krbcfg *c = *cfg;
+
+ if (!c) return;
+
+ krb5_free_default_realm(c->krbctx, c->realm);
+ krb5_free_context(c->krbctx);
+ free(c->kmkey->contents);
+ free(c->kmkey);
+ free(c->supp_encsalts);
+ free(c->pref_encsalts);
+ slapi_ch_array_free(c->passsync_mgrs);
+ free(c);
+ *cfg = NULL;
+};
+
+struct ipapwd_data {
+ Slapi_Entry *target;
+ char *dn;
+ char *password;
+ time_t timeNow;
+ time_t lastPwChange;
+ time_t expireTime;
+ int changetype;
+ int pwHistoryLen;
+};
+
+struct ipapwd_krbkeydata {
+ int32_t type;
+ struct berval value;
+};
+
+struct ipapwd_krbkey {
+ struct ipapwd_krbkeydata *salt;
+ struct ipapwd_krbkeydata *ekey;
+ struct berval s2kparams;
+};
+
+struct ipapwd_keyset {
+ uint16_t major_vno;
+ uint16_t minor_vno;
+ uint32_t kvno;
+ uint32_t mkvno;
+ struct ipapwd_krbkey *keys;
+ int num_keys;
+};
+
+static void ipapwd_keyset_free(struct ipapwd_keyset **pkset)
+{
+ struct ipapwd_keyset *kset = *pkset;
+ int i;
+
+ if (!kset) return;
+
+ for (i = 0; i < kset->num_keys; i++) {
+ if (kset->keys[i].salt) {
+ free(kset->keys[i].salt->value.bv_val);
+ free(kset->keys[i].salt);
+ }
+ if (kset->keys[i].ekey) {
+ free(kset->keys[i].ekey->value.bv_val);
+ free(kset->keys[i].ekey);
+ }
+ free(kset->keys[i].s2kparams.bv_val);
+ }
+ free(kset->keys);
+ free(kset);
+ *pkset = NULL;
+}
+
+static int filter_keys(struct ipapwd_krbcfg *krbcfg, struct ipapwd_keyset *kset)
+{
+ int i, j;
+
+ for (i = 0; i < kset->num_keys; i++) {
+ for (j = 0; j < krbcfg->num_supp_encsalts; j++) {
+ if (kset->keys[i].ekey->type ==
+ krbcfg->supp_encsalts[j].enc_type) {
+ break;
+ }
+ }
+ if (j == krbcfg->num_supp_encsalts) { /* not valid */
+
+ /* free key */
+ if (kset->keys[i].ekey) {
+ free(kset->keys[i].ekey->value.bv_val);
+ free(kset->keys[i].ekey);
+ }
+ if (kset->keys[i].salt) {
+ free(kset->keys[i].salt->value.bv_val);
+ free(kset->keys[i].salt);
+ }
+ free(kset->keys[i].s2kparams.bv_val);
+
+ /* move all remaining keys up by one */
+ kset->num_keys -= 1;
+
+ for (j = i; j < kset->num_keys; j++) {
+ kset->keys[j] = kset->keys[j + 1];
+ }
+
+ /* new key has been moved to this position, make sure
+ * we do not skip it, by neutralizing next increment */
+ i--;
+ }
+ }
+
+ return 0;
+}
+
+/* Novell key-format scheme:
+
+ KrbKeySet ::= SEQUENCE {
+ attribute-major-vno [0] UInt16,
+ attribute-minor-vno [1] UInt16,
+ kvno [2] UInt32,
+ mkvno [3] UInt32 OPTIONAL,
+ keys [4] SEQUENCE OF KrbKey,
+ ...
+ }
+
+ KrbKey ::= SEQUENCE {
+ salt [0] KrbSalt OPTIONAL,
+ key [1] EncryptionKey,
+ s2kparams [2] OCTET STRING OPTIONAL,
+ ...
+ }
+
+ KrbSalt ::= SEQUENCE {
+ type [0] Int32,
+ salt [1] OCTET STRING OPTIONAL
+ }
+
+ EncryptionKey ::= SEQUENCE {
+ keytype [0] Int32,
+ keyvalue [1] OCTET STRING
+ }
+
+ */
+
+static struct berval *encode_keys(struct ipapwd_keyset *kset)
+{
+ BerElement *be = NULL;
+ struct berval *bval = NULL;
+ int ret, i;
+
+ be = ber_alloc_t(LBER_USE_DER);
+
+ if (!be) {
+ slapi_log_error(SLAPI_LOG_FATAL, "ipa_pwd_extop",
+ "memory allocation failed\n");
+ return NULL;
+ }
+
+ ret = ber_printf(be, "{t[i]t[i]t[i]t[i]t[{",
+ (ber_tag_t)(LBER_CONSTRUCTED | LBER_CLASS_CONTEXT | 0), kset->major_vno,
+ (ber_tag_t)(LBER_CONSTRUCTED | LBER_CLASS_CONTEXT | 1), kset->minor_vno,
+ (ber_tag_t)(LBER_CONSTRUCTED | LBER_CLASS_CONTEXT | 2), kset->kvno,
+ (ber_tag_t)(LBER_CONSTRUCTED | LBER_CLASS_CONTEXT | 3), kset->mkvno,
+ (ber_tag_t)(LBER_CONSTRUCTED | LBER_CLASS_CONTEXT | 4));
+ if (ret == -1) {
+ slapi_log_error(SLAPI_LOG_FATAL, "ipa_pwd_extop",
+ "encoding asn1 vno info failed\n");
+ goto done;
+ }
+
+ for (i = 0; i < kset->num_keys; i++) {
+
+ ret = ber_printf(be, "{");
+ if (ret == -1) {
+ slapi_log_error(SLAPI_LOG_FATAL, "ipa_pwd_extop",
+ "encoding asn1 EncryptionKey failed\n");
+ goto done;
+ }
+
+ if (kset->keys[i].salt) {
+ ret = ber_printf(be, "t[{t[i]",
+ (ber_tag_t)(LBER_CONSTRUCTED | LBER_CLASS_CONTEXT | 0),
+ (ber_tag_t)(LBER_CONSTRUCTED | LBER_CLASS_CONTEXT | 0),
+ kset->keys[i].salt->type);
+ if ((ret != -1) && kset->keys[i].salt->value.bv_len) {
+ ret = ber_printf(be, "t[o]",
+ (ber_tag_t)(LBER_CONSTRUCTED | LBER_CLASS_CONTEXT | 1),
+ kset->keys[i].salt->value.bv_val,
+ kset->keys[i].salt->value.bv_len);
+ }
+ if (ret != -1) {
+ ret = ber_printf(be, "}]");
+ }
+ if (ret == -1) {
+ goto done;
+ }
+ }
+
+ ret = ber_printf(be, "t[{t[i]t[o]}]",
+ (ber_tag_t)(LBER_CONSTRUCTED | LBER_CLASS_CONTEXT | 1),
+ (ber_tag_t)(LBER_CONSTRUCTED | LBER_CLASS_CONTEXT | 0),
+ kset->keys[i].ekey->type,
+ (ber_tag_t)(LBER_CONSTRUCTED | LBER_CLASS_CONTEXT | 1),
+ kset->keys[i].ekey->value.bv_val,
+ kset->keys[i].ekey->value.bv_len);
+ if (ret == -1) {
+ slapi_log_error(SLAPI_LOG_FATAL, "ipa_pwd_extop",
+ "encoding asn1 EncryptionKey failed\n");
+ goto done;
+ }
+
+ /* FIXME: s2kparams not supported yet */
+
+ ret = ber_printf(be, "}");
+ if (ret == -1) {
+ slapi_log_error(SLAPI_LOG_FATAL, "ipa_pwd_extop",
+ "encoding asn1 EncryptionKey failed\n");
+ goto done;
+ }
+ }
+
+ ret = ber_printf(be, "}]}");
+ if (ret == -1) {
+ slapi_log_error(SLAPI_LOG_FATAL, "ipa_pwd_extop",
+ "encoding asn1 end of sequences failed\n");
+ goto done;
+ }
+
+ ret = ber_flatten(be, &bval);
+ if (ret == -1) {
+ slapi_log_error(SLAPI_LOG_FATAL, "ipa_pwd_extop",
+ "flattening asn1 failed\n");
+ goto done;
+ }
+done:
+ ber_free(be, 1);
+
+ return bval;
+}
+
+static int ipapwd_get_cur_kvno(Slapi_Entry *target)
+{
+ Slapi_Attr *krbPrincipalKey = NULL;
+ Slapi_ValueSet *svs;
+ Slapi_Value *sv;
+ BerElement *be = NULL;
+ const struct berval *cbval;
+ ber_tag_t tag, tmp;
+ ber_int_t tkvno;
+ int hint;
+ int kvno;
+ int ret;
+
+ /* retrieve current kvno and and keys */
+ ret = slapi_entry_attr_find(target, "krbPrincipalKey", &krbPrincipalKey);
+ if (ret != 0) {
+ return 0;
+ }
+
+ kvno = 0;
+
+ slapi_attr_get_valueset(krbPrincipalKey, &svs);
+ hint = slapi_valueset_first_value(svs, &sv);
+ while (hint != -1) {
+ cbval = slapi_value_get_berval(sv);
+ if (!cbval) {
+ slapi_log_error(SLAPI_LOG_TRACE, "ipa_pwd_extop",
+ "Error retrieving berval from Slapi_Value\n");
+ goto next;
+ }
+ be = ber_init(cbval);
+ if (!be) {
+ slapi_log_error(SLAPI_LOG_TRACE, "ipa_pwd_extop",
+ "ber_init() failed!\n");
+ goto next;
+ }
+
+ tag = ber_scanf(be, "{xxt[i]", &tmp, &tkvno);
+ if (tag == LBER_ERROR) {
+ slapi_log_error(SLAPI_LOG_TRACE, "ipa_pwd_extop",
+ "Bad OLD key encoding ?!\n");
+ ber_free(be, 1);
+ goto next;
+ }
+
+ if (tkvno > kvno) {
+ kvno = tkvno;
+ }
+
+ ber_free(be, 1);
+next:
+ hint = slapi_valueset_next_value(svs, hint, &sv);
+ }
+
+ return kvno;
+}
+
+static inline void encode_int16(unsigned int val, unsigned char *p)
+{
+ p[1] = (val >> 8) & 0xff;
+ p[0] = (val ) & 0xff;
+}
+
+static Slapi_Value **encrypt_encode_key(struct ipapwd_krbcfg *krbcfg,
+ struct ipapwd_data *data)
+{
+ krb5_context krbctx;
+ char *krbPrincipalName = NULL;
+ uint32_t krbMaxTicketLife;
+ int kvno, i;
+ int krbTicketFlags;
+ struct berval *bval = NULL;
+ Slapi_Value **svals = NULL;
+ krb5_principal princ;
+ krb5_error_code krberr;
+ krb5_data pwd;
+ struct ipapwd_keyset *kset = NULL;
+
+ krbctx = krbcfg->krbctx;
+
+ svals = (Slapi_Value **)calloc(2, sizeof(Slapi_Value *));
+ if (!svals) {
+ slapi_log_error(SLAPI_LOG_FATAL, "ipa_pwd_extop", "memory allocation failed\n");
+ return NULL;
+ }
+
+ kvno = ipapwd_get_cur_kvno(data->target);
+
+ krbPrincipalName = slapi_entry_attr_get_charptr(data->target, "krbPrincipalName");
+ if (!krbPrincipalName) {
+ slapi_log_error(SLAPI_LOG_FATAL, "ipa_pwd_extop", "no krbPrincipalName present in this entry\n");
+ return NULL;
+ }
+
+ krberr = krb5_parse_name(krbctx, krbPrincipalName, &princ);
+ if (krberr) {
+ slapi_log_error(SLAPI_LOG_FATAL, "ipa_pwd_extop",
+ "krb5_parse_name failed [%s]\n",
+ krb5_get_error_message(krbctx, krberr));
+ goto enc_error;
+ }
+
+ krbMaxTicketLife = slapi_entry_attr_get_uint(data->target, "krbMaxTicketLife");
+ if (krbMaxTicketLife == 0) {
+ /* FIXME: retrieve the default from config (max_life from kdc.conf) */
+ krbMaxTicketLife = 86400; /* just set the default 24h for now */
+ }
+
+ krbTicketFlags = slapi_entry_attr_get_int(data->target, "krbTicketFlags");
+
+ pwd.data = (char *)data->password;
+ pwd.length = strlen(data->password);
+
+ kset = malloc(sizeof(struct ipapwd_keyset));
+ if (!kset) {
+ slapi_log_error(SLAPI_LOG_FATAL, "ipa_pwd_extop", "malloc failed!\n");
+ goto enc_error;
+ }
+
+ /* this encoding assumes all keys have the same kvno */
+ /* major-vno = 1 and minor-vno = 1 */
+ kset->major_vno = 1;
+ kset->minor_vno = 1;
+ /* increment kvno (will be 1 if this is a new entry) */
+ kset->kvno = kvno + 1;
+ /* we also assum mkvno is 0 */
+ kset->mkvno = 0;
+
+ kset->num_keys = krbcfg->num_pref_encsalts;
+ kset->keys = calloc(kset->num_keys, sizeof(struct ipapwd_krbkey));
+ if (!kset->keys) {
+ slapi_log_error(SLAPI_LOG_FATAL, "ipa_pwd_extop", "malloc failed!\n");
+ goto enc_error;
+ }
+
+ for (i = 0; i < kset->num_keys; i++) {
+ krb5_keyblock key;
+ krb5_data salt;
+ krb5_octet *ptr;
+ krb5_data plain;
+ krb5_enc_data cipher;
+ size_t len;
+ const char *p;
+
+ salt.data = NULL;
+
+ switch (krbcfg->pref_encsalts[i].salt_type) {
+
+ case KRB5_KDB_SALTTYPE_ONLYREALM:
+
+ p = strchr(krbPrincipalName, '@');
+ if (!p) {
+ slapi_log_error(SLAPI_LOG_FATAL, "ipa_pwd_extop",
+ "Invalid principal name, no realm found!\n");
+ goto enc_error;
+ }
+ p++;
+ salt.data = strdup(p);
+ if (!salt.data) {
+ slapi_log_error(SLAPI_LOG_FATAL, "ipa_pwd_extop",
+ "memory allocation failed\n");
+ goto enc_error;
+ }
+ salt.length = strlen(salt.data); /* final \0 omitted on purpose */
+ break;
+
+ case KRB5_KDB_SALTTYPE_NOREALM:
+
+ krberr = krb5_principal2salt_norealm(krbctx, princ, &salt);
+ if (krberr) {
+ slapi_log_error(SLAPI_LOG_FATAL, "ipa_pwd_extop",
+ "krb5_principal2salt failed [%s]\n",
+ krb5_get_error_message(krbctx, krberr));
+ goto enc_error;
+ }
+ break;
+
+ case KRB5_KDB_SALTTYPE_NORMAL:
+
+ /* If pre auth is required we can set a random salt, otherwise
+ * we have to use a more conservative approach and set the salt
+ * to be REALMprincipal (the concatenation of REALM and principal
+ * name without any separator) */
+#if 0
+ if (krbTicketFlags & KTF_REQUIRES_PRE_AUTH) {
+ salt.length = KRB5P_SALT_SIZE;
+ salt.data = malloc(KRB5P_SALT_SIZE);
+ if (!salt.data) {
+ slapi_log_error(SLAPI_LOG_FATAL, "ipa_pwd_extop",
+ "memory allocation failed\n");
+ goto enc_error;
+ }
+ krberr = krb5_c_random_make_octets(krbctx, &salt);
+ if (krberr) {
+ slapi_log_error(SLAPI_LOG_FATAL, "ipa_pwd_extop",
+ "krb5_c_random_make_octets failed [%s]\n",
+ krb5_get_error_message(krbctx, krberr));
+ goto enc_error;
+ }
+ } else {
+#endif
+ krberr = krb5_principal2salt(krbctx, princ, &salt);
+ if (krberr) {
+ slapi_log_error(SLAPI_LOG_FATAL, "ipa_pwd_extop",
+ "krb5_principal2salt failed [%s]\n",
+ krb5_get_error_message(krbctx, krberr));
+ goto enc_error;
+ }
+#if 0
+ }
+#endif
+ break;
+
+ case KRB5_KDB_SALTTYPE_V4:
+ salt.length = 0;
+ break;
+
+ case KRB5_KDB_SALTTYPE_AFS3:
+
+ p = strchr(krbPrincipalName, '@');
+ if (!p) {
+ slapi_log_error(SLAPI_LOG_FATAL, "ipa_pwd_extop",
+ "Invalid principal name, no realm found!\n");
+ goto enc_error;
+ }
+ p++;
+ salt.data = strdup(p);
+ if (!salt.data) {
+ slapi_log_error(SLAPI_LOG_FATAL, "ipa_pwd_extop",
+ "memory allocation failed\n");
+ goto enc_error;
+ }
+ salt.length = SALT_TYPE_AFS_LENGTH; /* special value */
+ break;
+
+ default:
+ slapi_log_error(SLAPI_LOG_FATAL, "ipa_pwd_extop",
+ "Invalid salt type [%d]\n", krbcfg->pref_encsalts[i].salt_type);
+ goto enc_error;
+ }
+
+ /* need to build the key now to manage the AFS salt.length special case */
+ krberr = krb5_c_string_to_key(krbctx, krbcfg->pref_encsalts[i].enc_type, &pwd, &salt, &key);
+ if (krberr) {
+ slapi_log_error(SLAPI_LOG_FATAL, "ipa_pwd_extop",
+ "krb5_c_string_to_key failed [%s]\n",
+ krb5_get_error_message(krbctx, krberr));
+ krb5_free_data_contents(krbctx, &salt);
+ goto enc_error;
+ }
+ if (salt.length == SALT_TYPE_AFS_LENGTH) {
+ salt.length = strlen(salt.data);
+ }
+
+ krberr = krb5_c_encrypt_length(krbctx, krbcfg->kmkey->enctype, key.length, &len);
+ if (krberr) {
+ slapi_log_error(SLAPI_LOG_FATAL, "ipa_pwd_extop",
+ "krb5_c_string_to_key failed [%s]\n",
+ krb5_get_error_message(krbctx, krberr));
+ krb5int_c_free_keyblock_contents(krbctx, &key);
+ krb5_free_data_contents(krbctx, &salt);
+ goto enc_error;
+ }
+
+ if ((ptr = (krb5_octet *) malloc(2 + len)) == NULL) {
+ slapi_log_error(SLAPI_LOG_FATAL, "ipa_pwd_extop",
+ "memory allocation failed\n");
+ krb5int_c_free_keyblock_contents(krbctx, &key);
+ krb5_free_data_contents(krbctx, &salt);
+ goto enc_error;
+ }
+
+ encode_int16(key.length, ptr);
+
+ plain.length = key.length;
+ plain.data = (char *)key.contents;
+
+ cipher.ciphertext.length = len;
+ cipher.ciphertext.data = (char *)ptr+2;
+
+ krberr = krb5_c_encrypt(krbctx, krbcfg->kmkey, 0, 0, &plain, &cipher);
+ if (krberr) {
+ slapi_log_error(SLAPI_LOG_FATAL, "ipa_pwd_extop",
+ "krb5_c_encrypt failed [%s]\n",
+ krb5_get_error_message(krbctx, krberr));
+ krb5int_c_free_keyblock_contents(krbctx, &key);
+ krb5_free_data_contents(krbctx, &salt);
+ free(ptr);
+ goto enc_error;
+ }
+
+ /* KrbSalt */
+ kset->keys[i].salt = malloc(sizeof(struct ipapwd_krbkeydata));
+ if (!kset->keys[i].salt) {
+ slapi_log_error(SLAPI_LOG_FATAL, "ipa_pwd_extop", "malloc failed!\n");
+ krb5int_c_free_keyblock_contents(krbctx, &key);
+ free(ptr);
+ goto enc_error;
+ }
+
+ kset->keys[i].salt->type = krbcfg->pref_encsalts[i].salt_type;
+
+ if (salt.length) {
+ kset->keys[i].salt->value.bv_len = salt.length;
+ kset->keys[i].salt->value.bv_val = salt.data;
+ }
+
+ /* EncryptionKey */
+ kset->keys[i].ekey = malloc(sizeof(struct ipapwd_krbkeydata));
+ if (!kset->keys[i].ekey) {
+ slapi_log_error(SLAPI_LOG_FATAL, "ipa_pwd_extop", "malloc failed!\n");
+ krb5int_c_free_keyblock_contents(krbctx, &key);
+ free(ptr);
+ goto enc_error;
+ }
+ kset->keys[i].ekey->type = key.enctype;
+ kset->keys[i].ekey->value.bv_len = len+2;
+ kset->keys[i].ekey->value.bv_val = malloc(len+2);
+ if (!kset->keys[i].ekey->value.bv_val) {
+ slapi_log_error(SLAPI_LOG_FATAL, "ipa_pwd_extop", "malloc failed!\n");
+ krb5int_c_free_keyblock_contents(krbctx, &key);
+ free(ptr);
+ goto enc_error;
+ }
+ memcpy(kset->keys[i].ekey->value.bv_val, ptr, len+2);
+
+ /* make sure we free the memory used now that we are done with it */
+ krb5int_c_free_keyblock_contents(krbctx, &key);
+ free(ptr);
+ }
+
+ bval = encode_keys(kset);
+ if (!bval) {
+ slapi_log_error(SLAPI_LOG_FATAL, "ipa_pwd_extop",
+ "encoding asn1 KrbSalt failed\n");
+ goto enc_error;
+ }
+
+ svals[0] = slapi_value_new_berval(bval);
+ if (!svals[0]) {
+ slapi_log_error(SLAPI_LOG_FATAL, "ipa_pwd_extop",
+ "Converting berval to Slapi_Value\n");
+ goto enc_error;
+ }
+
+ ipapwd_keyset_free(&kset);
+ krb5_free_principal(krbctx, princ);
+ slapi_ch_free_string(&krbPrincipalName);
+ ber_bvfree(bval);
+ return svals;
+
+enc_error:
+ if (kset) ipapwd_keyset_free(&kset);
+ krb5_free_principal(krbctx, princ);
+ slapi_ch_free_string(&krbPrincipalName);
+ if (bval) ber_bvfree(bval);
+ free(svals);
+ return NULL;
+}
+
+static void ipapwd_free_slapi_value_array(Slapi_Value ***svals)
+{
+ Slapi_Value **sv = *svals;
+ int i;
+
+ if (sv) {
+ for (i = 0; sv[i]; i++) {
+ slapi_value_free(&sv[i]);
+ }
+ }
+
+ slapi_ch_free((void **)sv);
+}
+
+
+struct ntlm_keys {
+ uint8_t lm[16];
+ uint8_t nt[16];
+};
+
+#define KTF_LM_HASH 0x01
+#define KTF_NT_HASH 0x02
+#define KTF_DOS_CHARSET "CP850" /* same default as samba */
+#define KTF_UTF8 "UTF-8"
+#define KTF_UCS2 "UCS-2LE"
+
+static const uint8_t parity_table[128] = {
+ 1, 2, 4, 7, 8, 11, 13, 14, 16, 19, 21, 22, 25, 26, 28, 31,
+ 32, 35, 37, 38, 41, 42, 44, 47, 49, 50, 52, 55, 56, 59, 61, 62,
+ 64, 67, 69, 70, 73, 74, 76, 79, 81, 82, 84, 87, 88, 91, 93, 94,
+ 97, 98,100,103,104,107,109,110,112,115,117,118,121,122,124,127,
+ 128,131,133,134,137,138,140,143,145,146,148,151,152,155,157,158,
+ 161,162,164,167,168,171,173,174,176,179,181,182,185,186,188,191,
+ 193,194,196,199,200,203,205,206,208,211,213,214,217,218,220,223,
+ 224,227,229,230,233,234,236,239,241,242,244,247,248,251,253,254};
+
+static void lm_shuffle(uint8_t *out, uint8_t *in)
+{
+ out[0] = parity_table[in[0]>>1];
+ out[1] = parity_table[((in[0]<<6)|(in[1]>>2)) & 0x7F];
+ out[2] = parity_table[((in[1]<<5)|(in[2]>>3)) & 0x7F];
+ out[3] = parity_table[((in[2]<<4)|(in[3]>>4)) & 0x7F];
+ out[4] = parity_table[((in[3]<<3)|(in[4]>>5)) & 0x7F];
+ out[5] = parity_table[((in[4]<<2)|(in[5]>>6)) & 0x7F];
+ out[6] = parity_table[((in[5]<<1)|(in[6]>>7)) & 0x7F];
+ out[7] = parity_table[in[6] & 0x7F];
+}
+
+/* create the lm and nt hashes
+ newPassword: the clear text utf8 password
+ flags: KTF_LM_HASH | KTF_NT_HASH
+*/
+static int encode_ntlm_keys(char *newPasswd, unsigned int flags, struct ntlm_keys *keys)
+{
+ int ret = 0;
+
+ /* do lanman first */
+ if (flags & KTF_LM_HASH) {
+ iconv_t cd;
+ size_t cs, il, ol;
+ char *inc, *outc;
+ char *upperPasswd;
+ char *asciiPasswd;
+ DES_key_schedule schedule;
+ DES_cblock deskey;
+ DES_cblock magic = "KGS!@#$%";
+
+ /* TODO: must store the dos charset somewhere in the directory */
+ cd = iconv_open(KTF_DOS_CHARSET, KTF_UTF8);
+ if (cd == (iconv_t)(-1)) {
+ ret = -1;
+ goto done;
+ }
+
+ /* the lanman password is upper case */
+ upperPasswd = (char *)slapi_utf8StrToUpper((unsigned char *)newPasswd);
+ if (!upperPasswd) {
+ ret = -1;
+ goto done;
+ }
+ il = strlen(upperPasswd);
+
+ /* an ascii string can only be smaller than or equal to an utf8 one */
+ ol = il;
+ if (ol < 14) ol = 14;
+ asciiPasswd = calloc(ol+1, 1);
+ if (!asciiPasswd) {
+ slapi_ch_free_string(&upperPasswd);
+ ret = -1;
+ goto done;
+ }
+
+ inc = upperPasswd;
+ outc = asciiPasswd;
+ cs = iconv(cd, &inc, &il, &outc, &ol);
+ if (cs == -1) {
+ ret = -1;
+ slapi_ch_free_string(&upperPasswd);
+ free(asciiPasswd);
+ iconv_close(cd);
+ goto done;
+ }
+
+ /* done with these */
+ slapi_ch_free_string(&upperPasswd);
+ iconv_close(cd);
+
+ /* we are interested only in the first 14 ASCII chars for lanman */
+ if (strlen(asciiPasswd) > 14) {
+ asciiPasswd[14] = '\0';
+ }
+
+ /* first half */
+ lm_shuffle(deskey, (uint8_t *)asciiPasswd);
+
+ DES_set_key_unchecked(&deskey, &schedule);
+ DES_ecb_encrypt(&magic, (DES_cblock *)keys->lm, &schedule, DES_ENCRYPT);
+
+ /* second half */
+ lm_shuffle(deskey, (uint8_t *)&asciiPasswd[7]);
+
+ DES_set_key_unchecked(&deskey, &schedule);
+ DES_ecb_encrypt(&magic, (DES_cblock *)&(keys->lm[8]), &schedule, DES_ENCRYPT);
+
+ /* done with it */
+ free(asciiPasswd);
+
+ } else {
+ memset(keys->lm, 0, 16);
+ }
+
+ if (flags & KTF_NT_HASH) {
+ iconv_t cd;
+ size_t cs, il, ol, sl;
+ char *inc, *outc;
+ char *ucs2Passwd;
+ MD4_CTX md4ctx;
+
+ /* TODO: must store the dos charset somewhere in the directory */
+ cd = iconv_open(KTF_UCS2, KTF_UTF8);
+ if (cd == (iconv_t)(-1)) {
+ ret = -1;
+ goto done;
+ }
+
+ il = strlen(newPasswd);
+
+ /* an ucs2 string can be at most double than an utf8 one */
+ sl = ol = (il+1)*2;
+ ucs2Passwd = calloc(ol, 1);
+ if (!ucs2Passwd) {
+ ret = -1;
+ goto done;
+ }
+
+ inc = newPasswd;
+ outc = ucs2Passwd;
+ cs = iconv(cd, &inc, &il, &outc, &ol);
+ if (cs == -1) {
+ ret = -1;
+ free(ucs2Passwd);
+ iconv_close(cd);
+ goto done;
+ }
+
+ /* done with it */
+ iconv_close(cd);
+
+ /* get the final ucs2 string length */
+ sl -= ol;
+ /* we are interested only in the first 14 wchars for the nt password */
+ if (sl > 28) {
+ sl = 28;
+ }
+
+ ret = MD4_Init(&md4ctx);
+ if (ret == 0) {
+ ret = -1;
+ free(ucs2Passwd);
+ goto done;
+ }
+ ret = MD4_Update(&md4ctx, ucs2Passwd, sl);
+ if (ret == 0) {
+ ret = -1;
+ free(ucs2Passwd);
+ goto done;
+ }
+ ret = MD4_Final(keys->nt, &md4ctx);
+ if (ret == 0) {
+ ret = -1;
+ free(ucs2Passwd);
+ goto done;
+ }
+
+ } else {
+ memset(keys->nt, 0, 16);
+ }
+
+ ret = 0;
+
+done:
+ return ret;
+}
+
+/* searches the directory and finds the policy closest to the DN */
+/* return 0 on success, -1 on error or if no policy is found */
+static int ipapwd_getPolicy(const char *dn, Slapi_Entry *target, Slapi_Entry **e)
+{
+ const char *krbPwdPolicyReference;
+ const char *pdn;
+ const Slapi_DN *psdn;
+ Slapi_Backend *be;
+ Slapi_PBlock *pb = NULL;
+ char *attrs[] = { "krbMaxPwdLife", "krbMinPwdLife",
+ "krbPwdMinDiffChars", "krbPwdMinLength",
+ "krbPwdHistoryLength", NULL};
+ Slapi_Entry **es = NULL;
+ Slapi_Entry *pe = NULL;
+ char **edn;
+ int ret, res, dist, rdnc, scope, i;
+ Slapi_DN *sdn = NULL;
+
+ slapi_log_error(SLAPI_LOG_TRACE, "ipa_pwd_extop",
+ "ipapwd_getPolicy: Searching policy for [%s]\n", dn);
+
+ sdn = slapi_sdn_new_dn_byref(dn);
+ if (sdn == NULL) {
+ slapi_log_error(SLAPI_LOG_TRACE, "ipa_pwd_extop",
+ "ipapwd_getPolicy: Out of memory on [%s]\n", dn);
+ ret = -1;
+ goto done;
+ }
+
+ krbPwdPolicyReference = slapi_entry_attr_get_charptr(target, "krbPwdPolicyReference");
+ if (krbPwdPolicyReference) {
+ pdn = krbPwdPolicyReference;
+ scope = LDAP_SCOPE_BASE;
+ } else {
+ /* Find ancestor base DN */
+ be = slapi_be_select(sdn);
+ psdn = slapi_be_getsuffix(be, 0);
+ if (psdn == NULL) {
+ slapi_log_error(SLAPI_LOG_TRACE, "ipa_pwd_extop",
+ "ipapwd_getPolicy: Invalid DN [%s]\n", dn);
+ ret = -1;
+ goto done;
+ }
+ pdn = slapi_sdn_get_dn(psdn);
+ scope = LDAP_SCOPE_SUBTREE;
+ }
+
+ *e = NULL;
+
+ pb = slapi_pblock_new();
+ slapi_search_internal_set_pb (pb,
+ pdn, scope,
+ "(objectClass=krbPwdPolicy)",
+ attrs, 0,
+ NULL, /* Controls */
+ NULL, /* UniqueID */
+ ipapwd_plugin_id,
+ 0); /* Flags */
+
+ /* do search the tree */
+ ret = slapi_search_internal_pb(pb);
+ slapi_pblock_get(pb, SLAPI_PLUGIN_INTOP_RESULT, &res);
+ if (ret == -1 || res != LDAP_SUCCESS) {
+ slapi_log_error(SLAPI_LOG_TRACE, "ipa_pwd_extop",
+ "ipapwd_getPolicy: Couldn't find policy, err (%d)\n",
+ res?res:ret);
+ ret = -1;
+ goto done;
+ }
+
+ /* get entries */
+ slapi_pblock_get(pb, SLAPI_PLUGIN_INTOP_SEARCH_ENTRIES, &es);
+ if (!es) {
+ slapi_log_error(SLAPI_LOG_TRACE, "ipa_pwd_extop",
+ "ipapwd_getPolicy: No entries ?!");
+ ret = -1;
+ goto done;
+ }
+
+ /* count entries */
+ for (i = 0; es[i]; i++) /* count */ ;
+
+ /* if there is only one, return that */
+ if (i == 1) {
+ *e = slapi_entry_dup(es[0]);
+
+ ret = 0;
+ goto done;
+ }
+
+ /* count number of RDNs in DN */
+ edn = ldap_explode_dn(dn, 0);
+ if (!edn) {
+ slapi_log_error(SLAPI_LOG_TRACE, "ipa_pwd_extop",
+ "ipapwd_getPolicy: ldap_explode_dn(dn) failed ?!");
+ ret = -1;
+ goto done;
+ }
+ for (rdnc = 0; edn[rdnc]; rdnc++) /* count */ ;
+ ldap_value_free(edn);
+
+ pe = NULL;
+ dist = -1;
+
+ /* find closest entry */
+ for (i = 0; es[i]; i++) {
+ const Slapi_DN *esdn;
+
+ esdn = slapi_entry_get_sdn_const(es[i]);
+ if (esdn == NULL) continue;
+ if (0 == slapi_sdn_compare(esdn, sdn)) {
+ pe = es[i];
+ dist = 0;
+ break;
+ }
+ if (slapi_sdn_issuffix(sdn, esdn)) {
+ const char *dn1;
+ char **e1;
+ int c1;
+
+ dn1 = slapi_sdn_get_dn(esdn);
+ if (!dn1) continue;
+ e1 = ldap_explode_dn(dn1, 0);
+ if (!e1) continue;
+ for (c1 = 0; e1[c1]; c1++) /* count */ ;
+ ldap_value_free(e1);
+ if ((dist == -1) ||
+ ((rdnc - c1) < dist)) {
+ dist = rdnc - c1;
+ pe = es[i];
+ }
+ }
+ if (dist == 0) break; /* found closest */
+ }
+
+ if (pe == NULL) {
+ ret = -1;
+ goto done;
+ }
+
+ *e = slapi_entry_dup(pe);
+ ret = 0;
+done:
+ if (pb) {
+ slapi_free_search_results_internal(pb);
+ slapi_pblock_destroy(pb);
+ }
+ if (sdn) slapi_sdn_free(&sdn);
+ return ret;
+}
+
+#define GENERALIZED_TIME_LENGTH 15
+
+static int ipapwd_sv_pw_cmp(const void *pv1, const void *pv2)
+{
+ const char *pw1 = slapi_value_get_string(*((Slapi_Value **)pv1));
+ const char *pw2 = slapi_value_get_string(*((Slapi_Value **)pv2));
+
+ return strncmp(pw1, pw2, GENERALIZED_TIME_LENGTH);
+}
+
+static Slapi_Value **ipapwd_setPasswordHistory(Slapi_Mods *smods, struct ipapwd_data *data)
+{
+ Slapi_Value **pH = NULL;
+ Slapi_Attr *passwordHistory = NULL;
+ char timestr[GENERALIZED_TIME_LENGTH+1];
+ char *histr, *old_pw;
+ struct tm utctime;
+ int ret, pc;
+
+ old_pw = slapi_entry_attr_get_charptr(data->target, "userPassword");
+ if (!old_pw) {
+ /* no old password to store, just return */
+ return NULL;
+ }
+
+ if (!gmtime_r(&(data->timeNow), &utctime)) {
+ slapi_log_error(SLAPI_LOG_FATAL, "ipa_pwd_extop", "failed to retrieve current date (buggy gmtime_r ?)\n");
+ return NULL;
+ }
+ strftime(timestr, GENERALIZED_TIME_LENGTH+1, "%Y%m%d%H%M%SZ", &utctime);
+
+ histr = slapi_ch_smprintf("%s%s", timestr, old_pw);
+ if (!histr) {
+ slapi_log_error(SLAPI_LOG_PLUGIN, "ipa_pwd_extop",
+ "ipapwd_checkPassword: Out of Memory\n");
+ return NULL;
+ }
+
+ /* retrieve current history */
+ ret = slapi_entry_attr_find(data->target, "passwordHistory", &passwordHistory);
+ if (ret == 0) {
+ int ret, hint, count, i;
+ const char *pwstr;
+ Slapi_Value *pw;
+
+ hint = 0;
+ count = 0;
+ ret = slapi_attr_get_numvalues(passwordHistory, &count);
+ /* if we have one */
+ if (count > 0 && data->pwHistoryLen > 0) {
+ pH = calloc(count + 2, sizeof(Slapi_Value *));
+ if (!pH) {
+ slapi_log_error(SLAPI_LOG_PLUGIN, "ipa_pwd_extop",
+ "ipapwd_checkPassword: Out of Memory\n");
+ free(histr);
+ return NULL;
+ }
+
+ i = 0;
+ hint = slapi_attr_first_value(passwordHistory, &pw);
+ while (hint != -1) {
+ pwstr = slapi_value_get_string(pw);
+ /* if shorter than GENERALIZED_TIME_LENGTH, it
+ * is garbage, we never set timeless entries */
+ if (pwstr &&
+ (strlen(pwstr) > GENERALIZED_TIME_LENGTH)) {
+ pH[i] = pw;
+ i++;
+ }
+ hint = slapi_attr_next_value(passwordHistory, hint, &pw);
+ }
+
+ qsort(pH, i, sizeof(Slapi_Value *), ipapwd_sv_pw_cmp);
+
+ if (i >= data->pwHistoryLen) {
+ i = data->pwHistoryLen;
+ pH[i] = NULL;
+ i--;
+ }
+
+ pc = i;
+
+ /* copy only interesting entries */
+ for (i = 0; i < pc; i++) {
+ pH[i] = slapi_value_dup(pH[i]);
+ if (pH[i] == NULL) {
+ slapi_log_error(SLAPI_LOG_PLUGIN, "ipa_pwd_extop",
+ "ipapwd_checkPassword: Out of Memory\n");
+ while (i) {
+ i--;
+ slapi_value_free(&pH[i]);
+ }
+ free(pH);
+ free(histr);
+ return NULL;
+ }
+ }
+ }
+ }
+
+ if (pH == NULL) {
+ pH = calloc(2, sizeof(Slapi_Value *));
+ if (!pH) {
+ slapi_log_error(SLAPI_LOG_PLUGIN, "ipa_pwd_extop",
+ "ipapwd_checkPassword: Out of Memory\n");
+ free(histr);
+ return NULL;
+ }
+ pc = 0;
+ }
+
+ /* add new history value */
+ pH[pc] = slapi_value_new_string(histr);
+
+ free(histr);
+
+ return pH;
+}
+
+static Slapi_Value *ipapwd_strip_pw_date(Slapi_Value *pw)
+{
+ const char *pwstr;
+
+ pwstr = slapi_value_get_string(pw);
+ return slapi_value_new_string(&pwstr[GENERALIZED_TIME_LENGTH]);
+}
+
+#define IPAPWD_POLICY_MASK 0x0FF
+#define IPAPWD_POLICY_ERROR 0x100
+#define IPAPWD_POLICY_OK 0
+
+/* 90 days default pwd max lifetime */
+#define IPAPWD_DEFAULT_PWDLIFE (90 * 24 *3600)
+#define IPAPWD_DEFAULT_MINLEN 0
+
+/* check password strenght and history */
+static int ipapwd_CheckPolicy(struct ipapwd_data *data)
+{
+ char *krbPrincipalExpiration = NULL;
+ char *krbLastPwdChange = NULL;
+ char *krbPasswordExpiration = NULL;
+ int krbMaxPwdLife = IPAPWD_DEFAULT_PWDLIFE;
+ int krbPwdMinLength = IPAPWD_DEFAULT_MINLEN;
+ int krbPwdMinDiffChars = 0;
+ int krbMinPwdLife = 0;
+ int pwdCharLen = 0;
+ Slapi_Entry *policy = NULL;
+ Slapi_Attr *passwordHistory = NULL;
+ struct tm tm;
+ int tmp, ret;
+ char *old_pw;
+
+ /* check account is not expired. Ignore unixtime = 0 (Jan 1 1970) */
+ krbPrincipalExpiration = slapi_entry_attr_get_charptr(data->target, "krbPrincipalExpiration");
+ if (krbPrincipalExpiration &&
+ (strcasecmp("19700101000000Z", krbPrincipalExpiration) != 0)) {
+ /* if expiration date is set check it */
+ memset(&tm, 0, sizeof(struct tm));
+ ret = sscanf(krbPrincipalExpiration,
+ "%04u%02u%02u%02u%02u%02u",
+ &tm.tm_year, &tm.tm_mon, &tm.tm_mday,
+ &tm.tm_hour, &tm.tm_min, &tm.tm_sec);
+
+ if (ret == 6) {
+ tm.tm_year -= 1900;
+ tm.tm_mon -= 1;
+
+ if (data->timeNow > timegm(&tm)) {
+ slapi_log_error(SLAPI_LOG_TRACE, "ipa_pwd_extop", "Account Expired");
+ return IPAPWD_POLICY_ERROR | LDAP_PWPOLICY_PWDMODNOTALLOWED;
+ }
+ }
+ /* FIXME: else error out ? */
+ }
+ slapi_ch_free_string(&krbPrincipalExpiration);
+
+ /* find the entry with the password policy */
+ ret = ipapwd_getPolicy(data->dn, data->target, &policy);
+ if (ret) {
+ slapi_log_error(SLAPI_LOG_TRACE, "ipa_pwd_extop", "No password policy");
+ goto no_policy;
+ }
+
+ /* Retrieve Max History Len */
+ data->pwHistoryLen = slapi_entry_attr_get_int(policy, "krbPwdHistoryLength");
+
+ if (data->changetype != IPA_CHANGETYPE_NORMAL) {
+ /* We must skip policy checks (Admin change) but
+ * force a password change on the next login.
+ * But not if Directory Manager */
+ if (data->changetype == IPA_CHANGETYPE_ADMIN) {
+ data->expireTime = data->timeNow;
+ }
+
+ /* skip policy checks */
+ slapi_entry_free(policy);
+ goto no_policy;
+ }
+
+ /* first of all check current password, if any */
+ old_pw = slapi_entry_attr_get_charptr(data->target, "userPassword");
+ if (old_pw) {
+ Slapi_Value *cpw[2] = {NULL, NULL};
+ Slapi_Value *pw;
+
+ cpw[0] = slapi_value_new_string(old_pw);
+ pw = slapi_value_new_string(data->password);
+ if (!pw) {
+ slapi_log_error(SLAPI_LOG_PLUGIN, "ipa_pwd_extop",
+ "ipapwd_checkPassword: Out of Memory\n");
+ slapi_entry_free(policy);
+ slapi_ch_free_string(&old_pw);
+ slapi_value_free(&cpw[0]);
+ slapi_value_free(&pw);
+ return LDAP_OPERATIONS_ERROR;
+ }
+
+ ret = slapi_pw_find_sv(cpw, pw);
+ slapi_ch_free_string(&old_pw);
+ slapi_value_free(&cpw[0]);
+ slapi_value_free(&pw);
+
+ if (ret == 0) {
+ slapi_log_error(SLAPI_LOG_TRACE, "ipa_pwd_extop",
+ "ipapwd_checkPassword: Password in history\n");
+ slapi_entry_free(policy);
+ return IPAPWD_POLICY_ERROR | LDAP_PWPOLICY_PWDINHISTORY;
+ }
+ }
+
+ krbPasswordExpiration = slapi_entry_attr_get_charptr(data->target, "krbPasswordExpiration");
+ krbLastPwdChange = slapi_entry_attr_get_charptr(data->target, "krbLastPwdChange");
+ /* if no previous change, it means this is probably a new account
+ * or imported, log and just ignore */
+ if (krbLastPwdChange) {
+
+ memset(&tm, 0, sizeof(struct tm));
+ ret = sscanf(krbLastPwdChange,
+ "%04u%02u%02u%02u%02u%02u",
+ &tm.tm_year, &tm.tm_mon, &tm.tm_mday,
+ &tm.tm_hour, &tm.tm_min, &tm.tm_sec);
+
+ if (ret == 6) {
+ tm.tm_year -= 1900;
+ tm.tm_mon -= 1;
+ data->lastPwChange = timegm(&tm);
+ }
+ /* FIXME: *else* report an error ? */
+ } else {
+ slapi_log_error(SLAPI_LOG_TRACE, "ipa_pwd_extop",
+ "Warning: Last Password Change Time is not available");
+ }
+
+ /* Check min age */
+ krbMinPwdLife = slapi_entry_attr_get_int(policy, "krbMinPwdLife");
+ /* if no default then treat it as no limit */
+ if (krbMinPwdLife != 0) {
+
+ /* check for reset cases */
+ if (krbLastPwdChange == NULL ||
+ ((krbPasswordExpiration != NULL) &&
+ strcmp(krbPasswordExpiration, krbLastPwdChange) == 0)) {
+ /* Expiration and last change time are the same or
+ * missing this happens only when a password is reset
+ * by an admin or the account is new or no expiration
+ * policy is set, PASS */
+ slapi_log_error(SLAPI_LOG_TRACE, "ipa_pwd_extop",
+ "ipapwd_checkPolicy: Ignore krbMinPwdLife Expiration, not enough info\n");
+
+ } else if (data->timeNow < data->lastPwChange + krbMinPwdLife) {
+ slapi_log_error(SLAPI_LOG_TRACE, "ipa_pwd_extop",
+ "ipapwd_checkPolicy: Too soon to change password\n");
+ slapi_entry_free(policy);
+ slapi_ch_free_string(&krbPasswordExpiration);
+ slapi_ch_free_string(&krbLastPwdChange);
+ return IPAPWD_POLICY_ERROR | LDAP_PWPOLICY_PWDTOOYOUNG;
+ }
+ }
+
+ /* free strings or we leak them */
+ slapi_ch_free_string(&krbPasswordExpiration);
+ slapi_ch_free_string(&krbLastPwdChange);
+
+ /* Retrieve min length */
+ tmp = slapi_entry_attr_get_int(policy, "krbPwdMinLength");
+ if (tmp != 0) {
+ krbPwdMinLength = tmp;
+ }
+
+ /* check complexity */
+ /* FIXME: this code is partially based on Directory Server code,
+ * the plan is to merge this code later making it available
+ * trough a pulic DS API for slapi plugins */
+ krbPwdMinDiffChars = slapi_entry_attr_get_int(policy, "krbPwdMinDiffChars");
+ if (krbPwdMinDiffChars != 0) {
+ int num_digits = 0;
+ int num_alphas = 0;
+ int num_uppers = 0;
+ int num_lowers = 0;
+ int num_specials = 0;
+ int num_8bit = 0;
+ int num_repeated = 0;
+ int max_repeated = 0;
+ int num_categories = 0;
+ char *p, *pwd;
+
+ pwd = strdup(data->password);
+
+ /* check character types */
+ p = pwd;
+ while ( p && *p )
+ {
+ if ( ldap_utf8isdigit( p ) ) {
+ num_digits++;
+ } else if ( ldap_utf8isalpha( p ) ) {
+ num_alphas++;
+ if ( slapi_utf8isLower( (unsigned char *)p ) ) {
+ num_lowers++;
+ } else {
+ num_uppers++;
+ }
+ } else {
+ /* check if this is an 8-bit char */
+ if ( *p & 128 ) {
+ num_8bit++;
+ } else {
+ num_specials++;
+ }
+ }
+
+ /* check for repeating characters. If this is the
+ first char of the password, no need to check */
+ if ( pwd != p ) {
+ int len = ldap_utf8len( p );
+ char *prev_p = ldap_utf8prev( p );
+
+ if ( len == ldap_utf8len( prev_p ) )
+ {
+ if ( memcmp( p, prev_p, len ) == 0 )
+ {
+ num_repeated++;
+ if ( max_repeated < num_repeated ) {
+ max_repeated = num_repeated;
+ }
+ } else {
+ num_repeated = 0;
+ }
+ } else {
+ num_repeated = 0;
+ }
+ }
+
+ p = ldap_utf8next( p );
+ }
+
+ free(pwd);
+ p = pwd = NULL;
+
+ /* tally up the number of character categories */
+ if ( num_digits > 0 )
+ ++num_categories;
+ if ( num_uppers > 0 )
+ ++num_categories;
+ if ( num_lowers > 0 )
+ ++num_categories;
+ if ( num_specials > 0 )
+ ++num_categories;
+ if ( num_8bit > 0 )
+ ++num_categories;
+
+ /* FIXME: the kerberos plicy schema does not define separated threshold values,
+ * so just treat anything as a category, we will fix this when we merge
+ * with DS policies */
+
+ if (max_repeated > 1)
+ --num_categories;
+
+ if (num_categories < krbPwdMinDiffChars) {
+ slapi_log_error(SLAPI_LOG_TRACE, "ipa_pwd_extop",
+ "ipapwd_checkPassword: Password not complex enough\n");
+ slapi_entry_free(policy);
+ return IPAPWD_POLICY_ERROR | LDAP_PWPOLICY_INVALIDPWDSYNTAX;
+ }
+ }
+
+ /* Check password history */
+ ret = slapi_entry_attr_find(data->target, "passwordHistory", &passwordHistory);
+ if (ret == 0) {
+ int ret, hint, count, i, j;
+ const char *pwstr;
+ Slapi_Value **pH;
+ Slapi_Value *pw;
+
+ hint = 0;
+ count = 0;
+ ret = slapi_attr_get_numvalues(passwordHistory, &count);
+ /* check history only if we have one */
+ if (count > 0 && data->pwHistoryLen > 0) {
+ pH = calloc(count + 2, sizeof(Slapi_Value *));
+ if (!pH) {
+ slapi_log_error(SLAPI_LOG_PLUGIN, "ipa_pwd_extop",
+ "ipapwd_checkPassword: Out of Memory\n");
+ slapi_entry_free(policy);
+ return LDAP_OPERATIONS_ERROR;
+ }
+
+ i = 0;
+ hint = slapi_attr_first_value(passwordHistory, &pw);
+ while (hint != -1) {
+ pwstr = slapi_value_get_string(pw);
+ /* if shorter than GENERALIZED_TIME_LENGTH, it
+ * is garbage, we never set timeless entries */
+ if (pwstr &&
+ (strlen(pwstr) > GENERALIZED_TIME_LENGTH)) {
+ pH[i] = pw;
+ i++;
+ }
+ hint = slapi_attr_next_value(passwordHistory, hint, &pw);
+ }
+
+ qsort(pH, i, sizeof(Slapi_Value *), ipapwd_sv_pw_cmp);
+
+ if (i > data->pwHistoryLen) {
+ i = data->pwHistoryLen;
+ pH[i] = NULL;
+ }
+
+ for (j = 0; pH[j]; j++) {
+ pH[j] = ipapwd_strip_pw_date(pH[j]);
+ }
+
+ pw = slapi_value_new_string(data->password);
+ if (!pw) {
+ slapi_log_error(SLAPI_LOG_PLUGIN, "ipa_pwd_extop",
+ "ipapwd_checkPassword: Out of Memory\n");
+ slapi_entry_free(policy);
+ free(pH);
+ return LDAP_OPERATIONS_ERROR;
+ }
+
+ ret = slapi_pw_find_sv(pH, pw);
+
+ for (j = 0; pH[j]; j++) {
+ slapi_value_free(&pH[j]);
+ }
+ slapi_value_free(&pw);
+ free(pH);
+
+ if (ret == 0) {
+ slapi_log_error(SLAPI_LOG_TRACE, "ipa_pwd_extop",
+ "ipapwd_checkPassword: Password in history\n");
+ slapi_entry_free(policy);
+ return IPAPWD_POLICY_ERROR | LDAP_PWPOLICY_PWDINHISTORY;
+ }
+ }
+ }
+
+ /* Calculate max age */
+ tmp = slapi_entry_attr_get_int(policy, "krbMaxPwdLife");
+ if (tmp != 0) {
+ krbMaxPwdLife = tmp;
+ }
+
+ slapi_entry_free(policy);
+
+no_policy:
+
+ /* check min lenght */
+ pwdCharLen = ldap_utf8characters(data->password);
+
+ if (pwdCharLen < krbPwdMinLength) {
+ slapi_log_error(SLAPI_LOG_TRACE, "ipa_pwd_extop",
+ "ipapwd_checkPassword: Password too short\n");
+ return IPAPWD_POLICY_ERROR | LDAP_PWPOLICY_PWDTOOSHORT;
+ }
+
+ if (data->expireTime == 0) {
+ data->expireTime = data->timeNow + krbMaxPwdLife;
+ }
+
+ return IPAPWD_POLICY_OK;
+}
+
+
+/* Searches the dn in directory,
+ * If found : fills in slapi_entry structure and returns 0
+ * If NOT found : returns the search result as LDAP_NO_SUCH_OBJECT
+ */
+static int ipapwd_getEntry(const char *dn, Slapi_Entry **e2, char **attrlist)
+{
+ Slapi_DN *sdn;
+ int search_result = 0;
+
+ slapi_log_error(SLAPI_LOG_TRACE, "ipa_pwd_extop", "=> ipapwd_getEntry\n");
+
+ sdn = slapi_sdn_new_dn_byref(dn);
+ if ((search_result = slapi_search_internal_get_entry( sdn, attrlist, e2,
+ ipapwd_plugin_id)) != LDAP_SUCCESS ){
+ slapi_log_error(SLAPI_LOG_TRACE, "ipa_pwd_extop",
+ "ipapwd_getEntry: No such entry-(%s), err (%d)\n",
+ dn, search_result);
+ }
+
+ slapi_sdn_free( &sdn );
+ slapi_log_error(SLAPI_LOG_TRACE, "ipa_pwd_extop",
+ "<= ipapwd_getEntry: %d\n", search_result);
+ return search_result;
+}
+
+
+/* Construct Mods pblock and perform the modify operation
+ * Sets result of operation in SLAPI_PLUGIN_INTOP_RESULT
+ */
+static int ipapwd_apply_mods(const char *dn, Slapi_Mods *mods)
+{
+ Slapi_PBlock *pb;
+ int ret;
+
+ slapi_log_error(SLAPI_LOG_TRACE, "ipa_pwd_extop", "=> ipapwd_apply_mods\n");
+
+ if (!mods || (slapi_mods_get_num_mods(mods) == 0)) {
+ return -1;
+ }
+
+ pb = slapi_pblock_new();
+ slapi_modify_internal_set_pb (pb, dn,
+ slapi_mods_get_ldapmods_byref(mods),
+ NULL, /* Controls */
+ NULL, /* UniqueID */
+ ipapwd_plugin_id, /* PluginID */
+ 0); /* Flags */
+
+ ret = slapi_modify_internal_pb (pb);
+ if (ret) {
+ slapi_log_error(SLAPI_LOG_TRACE, "ipa_pwd_extop",
+ "WARNING: modify error %d on entry '%s'\n",
+ ret, dn);
+ } else {
+
+ slapi_pblock_get(pb, SLAPI_PLUGIN_INTOP_RESULT, &ret);
+
+ if (ret != LDAP_SUCCESS){
+ slapi_log_error(SLAPI_LOG_TRACE, "ipa_pwd_extop",
+ "WARNING: modify error %d on entry '%s'\n",
+ ret, dn);
+ } else {
+ slapi_log_error(SLAPI_LOG_TRACE, "ipa_pwd_extop",
+ "<= ipapwd_apply_mods: Successful\n");
+ }
+ }
+
+ slapi_pblock_destroy(pb);
+
+ return ret;
+}
+
+/* ascii hex output of bytes in "in"
+ * out len is 32 (preallocated)
+ * in len is 16 */
+static const char hexchars[] = "0123456789ABCDEF";
+static void hexbuf(char *out, const uint8_t *in)
+{
+ int i;
+
+ for (i = 0; i < 16; i++) {
+ out[i*2] = hexchars[in[i] >> 4];
+ out[i*2+1] = hexchars[in[i] & 0x0f];
+ }
+}
+
+/* Modify the Password attributes of the entry */
+static int ipapwd_SetPassword(struct ipapwd_krbcfg *krbcfg,
+ struct ipapwd_data *data)
+{
+ int ret = 0, i = 0;
+ Slapi_Mods *smods;
+ Slapi_Value **svals = NULL;
+ Slapi_Value **pwvals = NULL;
+ struct tm utctime;
+ char timestr[GENERALIZED_TIME_LENGTH+1];
+ krb5_context krbctx;
+ krb5_error_code krberr;
+ char lm[33], nt[33];
+ struct ntlm_keys ntlm;
+ int ntlm_flags = 0;
+ Slapi_Value *sambaSamAccount;
+
+ slapi_log_error(SLAPI_LOG_TRACE, "ipa_pwd_extop", "=> ipapwd_SetPassword\n");
+
+ smods = slapi_mods_new();
+
+ /* generate kerberos keys to be put into krbPrincipalKey */
+ svals = encrypt_encode_key(krbcfg, data);
+ if (!svals) {
+ slapi_log_error(SLAPI_LOG_FATAL, "ipa_pwd_extop", "key encryption/encoding failed\n");
+ ret = LDAP_OPERATIONS_ERROR;
+ goto free_and_return;
+ }
+
+ slapi_mods_add_mod_values(smods, LDAP_MOD_REPLACE, "krbPrincipalKey", svals);
+
+ /* change Last Password Change field with the current date */
+ if (!gmtime_r(&(data->timeNow), &utctime)) {
+ slapi_log_error(SLAPI_LOG_FATAL, "ipa_pwd_extop", "failed to retrieve current date (buggy gmtime_r ?)\n");
+ ret = LDAP_OPERATIONS_ERROR;
+ goto free_and_return;
+ }
+ strftime(timestr, GENERALIZED_TIME_LENGTH+1, "%Y%m%d%H%M%SZ", &utctime);
+ slapi_mods_add_string(smods, LDAP_MOD_REPLACE, "krbLastPwdChange", timestr);
+
+ /* set Password Expiration date */
+ if (!gmtime_r(&(data->expireTime), &utctime)) {
+ slapi_log_error(SLAPI_LOG_FATAL, "ipa_pwd_extop", "failed to convert expiration date\n");
+ ret = LDAP_OPERATIONS_ERROR;
+ goto free_and_return;
+ }
+ strftime(timestr, GENERALIZED_TIME_LENGTH+1, "%Y%m%d%H%M%SZ", &utctime);
+ slapi_mods_add_string(smods, LDAP_MOD_REPLACE, "krbPasswordExpiration", timestr);
+
+ sambaSamAccount = slapi_value_new_string("sambaSamAccount");
+ if (slapi_entry_attr_has_syntax_value(data->target, "objectClass", sambaSamAccount)) {
+ /* TODO: retrieve if we want to store the LM hash or not */
+ ntlm_flags = KTF_LM_HASH | KTF_NT_HASH;
+ }
+ slapi_value_free(&sambaSamAccount);
+
+ if (ntlm_flags) {
+ char *password = strdup(data->password);
+ if (encode_ntlm_keys(password, ntlm_flags, &ntlm) != 0) {
+ free(password);
+ ret = LDAP_OPERATIONS_ERROR;
+ goto free_and_return;
+ }
+ if (ntlm_flags & KTF_LM_HASH) {
+ hexbuf(lm, ntlm.lm);
+ lm[32] = '\0';
+ slapi_mods_add_string(smods, LDAP_MOD_REPLACE, "sambaLMPassword", lm);
+ }
+ if (ntlm_flags & KTF_NT_HASH) {
+ hexbuf(nt, ntlm.nt);
+ nt[32] = '\0';
+ slapi_mods_add_string(smods, LDAP_MOD_REPLACE, "sambaNTPassword", nt);
+ }
+ free(password);
+ }
+
+ /* let DS encode the password itself, this allows also other plugins to
+ * intercept it to perform operations like synchronization with Active
+ * Directory domains through the replication plugin */
+ slapi_mods_add_string(smods, LDAP_MOD_REPLACE, "userPassword", data->password);
+
+ /* set password history */
+ pwvals = ipapwd_setPasswordHistory(smods, data);
+ if (pwvals) {
+ slapi_mods_add_mod_values(smods, LDAP_MOD_REPLACE, "passwordHistory", pwvals);
+ }
+
+ /* FIXME:
+ * instead of replace we should use a delete/add so that we are
+ * completely sure nobody else modified the entry meanwhile and
+ * fail if that's the case */
+
+ /* commit changes */
+ ret = ipapwd_apply_mods(data->dn, smods);
+
+ slapi_log_error(SLAPI_LOG_TRACE, "ipa_pwd_extop", "<= ipapwd_SetPassword: %d\n", ret);
+
+free_and_return:
+ slapi_mods_free(&smods);
+ ipapwd_free_slapi_value_array(&svals);
+ ipapwd_free_slapi_value_array(&pwvals);
+
+ return ret;
+}
+
+static int ipapwd_chpwop(Slapi_PBlock *pb, struct ipapwd_krbcfg *krbcfg)
+{
+ char *bindDN = NULL;
+ char *authmethod = NULL;
+ char *dn = NULL;
+ char *oldPasswd = NULL;
+ char *newPasswd = NULL;
+ char *errMesg = NULL;
+ int ret=0, rc=0, is_root=0;
+ ber_tag_t tag=0;
+ ber_len_t len=-1;
+ struct berval *extop_value = NULL;
+ BerElement *ber = NULL;
+ Slapi_Entry *targetEntry=NULL;
+ char *attrlist[] = {"*", "passwordHistory", NULL };
+ struct ipapwd_data pwdata;
+
+ /* Get the ber value of the extended operation */
+ slapi_pblock_get(pb, SLAPI_EXT_OP_REQ_VALUE, &extop_value);
+
+ if ((ber = ber_init(extop_value)) == NULL)
+ {
+ errMesg = "PasswdModify Request decode failed.\n";
+ rc = LDAP_PROTOCOL_ERROR;
+ goto free_and_return;
+ }
+
+ /* Format of request to parse
+ *
+ * PasswdModifyRequestValue ::= SEQUENCE {
+ * userIdentity [0] OCTET STRING OPTIONAL
+ * oldPasswd [1] OCTET STRING OPTIONAL
+ * newPasswd [2] OCTET STRING OPTIONAL }
+ *
+ * The request value field is optional. If it is
+ * provided, at least one field must be filled in.
+ */
+
+ /* ber parse code */
+ if ( ber_scanf( ber, "{") == LBER_ERROR )
+ {
+ /* The request field wasn't provided. We'll
+ * now try to determine the userid and verify
+ * knowledge of the old password via other
+ * means.
+ */
+ goto parse_req_done;
+ } else {
+ tag = ber_peek_tag( ber, &len);
+ }
+
+ /* identify userID field by tags */
+ if (tag == LDAP_EXTOP_PASSMOD_TAG_USERID )
+ {
+ if (ber_scanf(ber, "a", &dn) == LBER_ERROR) {
+ slapi_ch_free_string(&dn);
+ slapi_log_error(SLAPI_LOG_FATAL, "ipa_pwd_extop",
+ "ber_scanf failed\n");
+ errMesg = "ber_scanf failed at userID parse.\n";
+ rc = LDAP_PROTOCOL_ERROR;
+ goto free_and_return;
+ }
+
+ tag = ber_peek_tag(ber, &len);
+ }
+
+ /* identify oldPasswd field by tags */
+ if (tag == LDAP_EXTOP_PASSMOD_TAG_OLDPWD )
+ {
+ if (ber_scanf(ber, "a", &oldPasswd) == LBER_ERROR) {
+ slapi_log_error(SLAPI_LOG_FATAL, "ipa_pwd_extop",
+ "ber_scanf failed\n");
+ errMesg = "ber_scanf failed at oldPasswd parse.\n";
+ rc = LDAP_PROTOCOL_ERROR;
+ goto free_and_return;
+ }
+ tag = ber_peek_tag(ber, &len);
+ }
+
+ /* identify newPasswd field by tags */
+ if (tag == LDAP_EXTOP_PASSMOD_TAG_NEWPWD )
+ {
+ if (ber_scanf(ber, "a", &newPasswd) == LBER_ERROR) {
+ slapi_log_error(SLAPI_LOG_FATAL, "ipa_pwd_extop",
+ "ber_scanf failed\n");
+ errMesg = "ber_scanf failed at newPasswd parse.\n";
+ rc = LDAP_PROTOCOL_ERROR;
+ goto free_and_return;
+ }
+ }
+
+parse_req_done:
+ /* Uncomment for debugging, otherwise we don't want to leak the
+ * password values into the log... */
+ /* LDAPDebug( LDAP_DEBUG_ARGS, "passwd: dn (%s), oldPasswd (%s),
+ * newPasswd (%s)\n", dn, oldPasswd, newPasswd); */
+
+
+ /* Get Bind DN */
+ slapi_pblock_get(pb, SLAPI_CONN_DN, &bindDN);
+
+ /* If the connection is bound anonymously, we must refuse
+ * to process this operation. */
+ if (bindDN == NULL || *bindDN == '\0') {
+ /* Refuse the operation because they're bound anonymously */
+ errMesg = "Anonymous Binds are not allowed.\n";
+ rc = LDAP_INSUFFICIENT_ACCESS;
+ goto free_and_return;
+ }
+
+ /* A new password was not supplied in the request, and we do not support
+ * password generation yet.
+ */
+ if (newPasswd == NULL || *newPasswd == '\0') {
+ errMesg = "Password generation not implemented.\n";
+ rc = LDAP_UNWILLING_TO_PERFORM;
+ goto free_and_return;
+ }
+
+ if (oldPasswd == NULL || *oldPasswd == '\0') {
+ /* If user is authenticated, they already gave their password during
+ the bind operation (or used sasl or client cert auth or OS creds) */
+ slapi_pblock_get(pb, SLAPI_CONN_AUTHMETHOD, &authmethod);
+ if (!authmethod || !strcmp(authmethod, SLAPD_AUTH_NONE)) {
+ errMesg = "User must be authenticated to the directory server.\n";
+ rc = LDAP_INSUFFICIENT_ACCESS;
+ goto free_and_return;
+ }
+ }
+
+ /* Determine the target DN for this operation */
+ /* Did they give us a DN ? */
+ if (dn == NULL || *dn == '\0') {
+ /* Get the DN from the bind identity on this connection */
+ dn = slapi_ch_strdup(bindDN);
+ slapi_log_error(SLAPI_LOG_TRACE, "ipa_pwd_extop",
+ "Missing userIdentity in request, using the bind DN instead.\n");
+ }
+
+ slapi_pblock_set( pb, SLAPI_ORIGINAL_TARGET, dn );
+
+ /* Now we have the DN, look for the entry */
+ ret = ipapwd_getEntry(dn, &targetEntry, attrlist);
+ /* If we can't find the entry, then that's an error */
+ if (ret) {
+ /* Couldn't find the entry, fail */
+ errMesg = "No such Entry exists.\n" ;
+ rc = LDAP_NO_SUCH_OBJECT;
+ goto free_and_return;
+ }
+
+ /* First thing to do is to ask access control if the bound identity has
+ * rights to modify the userpassword attribute on this entry. If not,
+ * then we fail immediately with insufficient access. This means that
+ * we don't leak any useful information to the client such as current
+ * password wrong, etc.
+ */
+
+ is_root = slapi_dn_isroot(bindDN);
+ slapi_pblock_set(pb, SLAPI_REQUESTOR_ISROOT, &is_root);
+
+ /* In order to perform the access control check, we need to select a
+ * backend (even though we don't actually need it otherwise).
+ */
+ {
+ Slapi_Backend *be = NULL;
+
+ be = slapi_be_select(slapi_entry_get_sdn(targetEntry));
+ if (NULL == be) {
+ errMesg = "Failed to find backend for target entry";
+ rc = LDAP_OPERATIONS_ERROR;
+ goto free_and_return;
+ }
+ slapi_pblock_set(pb, SLAPI_BACKEND, be);
+ }
+
+ ret = slapi_access_allowed( pb, targetEntry, "krbPrincipalKey", NULL, SLAPI_ACL_WRITE );
+ if ( ret != LDAP_SUCCESS ) {
+ errMesg = "Insufficient access rights\n";
+ rc = LDAP_INSUFFICIENT_ACCESS;
+ goto free_and_return;
+ }
+
+ /* Now we have the entry which we want to modify
+ * They gave us a password (old), check it against the target entry
+ * Is the old password valid ?
+ */
+ if (oldPasswd && *oldPasswd) {
+ /* If user is authenticated, they already gave their password
+ * during the bind operation (or used sasl or client cert auth
+ * or OS creds) */
+ slapi_log_error(SLAPI_LOG_TRACE, "ipa_pwd_extop",
+ "oldPasswd provided, but we will ignore it");
+ }
+
+ memset(&pwdata, 0, sizeof(pwdata));
+ pwdata.target = targetEntry;
+ pwdata.dn = dn;
+ pwdata.password = newPasswd;
+ pwdata.timeNow = time(NULL);
+ pwdata.changetype = IPA_CHANGETYPE_NORMAL;
+
+ /*
+ * (technically strcasecmp to compare DNs is not absolutely correct,
+ * but it should work for the cases we care about here)
+ */
+
+ /* determine type of password change */
+ /* special cases */
+ if ((strcasecmp(dn, bindDN) != 0) &&
+ (strcasecmp(ipa_changepw_principal_dn, bindDN) != 0)) {
+ int i;
+
+ pwdata.changetype = IPA_CHANGETYPE_ADMIN;
+
+ for (i = 0; i < krbcfg->num_passsync_mgrs; i++) {
+ if (strcasecmp(krbcfg->passsync_mgrs[i], bindDN) == 0) {
+ pwdata.changetype = IPA_CHANGETYPE_DSMGR;
+ break;
+ }
+ }
+ }
+
+ /* check the policy */
+ ret = ipapwd_CheckPolicy(&pwdata);
+ if (ret) {
+ errMesg = "Password Fails to meet minimum strength criteria";
+ if (ret & IPAPWD_POLICY_ERROR) {
+ slapi_pwpolicy_make_response_control(pb, -1, -1, ret & IPAPWD_POLICY_MASK);
+ rc = LDAP_CONSTRAINT_VIOLATION;
+ } else {
+ errMesg = "Internal error";
+ rc = ret;
+ }
+ goto free_and_return;
+ }
+
+ /* Now we're ready to set the kerberos key material */
+ ret = ipapwd_SetPassword(krbcfg, &pwdata);
+ if (ret != LDAP_SUCCESS) {
+ /* Failed to modify the password,
+ * e.g. because insufficient access allowed */
+ errMesg = "Failed to update password";
+ if (ret > 0) {
+ rc = ret;
+ } else {
+ rc = LDAP_OPERATIONS_ERROR;
+ }
+ goto free_and_return;
+ }
+
+ slapi_log_error(SLAPI_LOG_TRACE, "ipa_pwd_extop", "<= ipapwd_extop: %d\n", rc);
+
+ /* Free anything that we allocated above */
+free_and_return:
+ slapi_ch_free_string(&oldPasswd);
+ slapi_ch_free_string(&newPasswd);
+ /* Either this is the same pointer that we allocated and set above,
+ * or whoever used it should have freed it and allocated a new
+ * value that we need to free here */
+ slapi_pblock_get(pb, SLAPI_ORIGINAL_TARGET, &dn);
+ slapi_ch_free_string(&dn);
+ slapi_pblock_set(pb, SLAPI_ORIGINAL_TARGET, NULL);
+ slapi_ch_free_string(&authmethod);
+
+ if (targetEntry) slapi_entry_free(targetEntry);
+ if (ber) ber_free(ber, 1);
+
+ slapi_log_error(SLAPI_LOG_PLUGIN, "ipa_pwd_extop", errMesg ? errMesg : "success");
+ slapi_send_ldap_result(pb, rc, NULL, errMesg, 0, NULL);
+
+ return SLAPI_PLUGIN_EXTENDED_SENT_RESULT;
+
+}
+
+/* Password Modify Extended operation plugin function */
+static int ipapwd_setkeytab(Slapi_PBlock *pb, struct ipapwd_krbcfg *krbcfg)
+{
+ char *bindDN = NULL;
+ char *serviceName = NULL;
+ char *errMesg = NULL;
+ int ret=0, rc=0, is_root=0;
+ struct berval *extop_value = NULL;
+ BerElement *ber = NULL;
+ Slapi_PBlock *pbte = NULL;
+ Slapi_Entry *targetEntry=NULL;
+ struct berval *bval = NULL;
+ Slapi_Value **svals = NULL;
+ const char *bdn;
+ const Slapi_DN *bsdn;
+ Slapi_DN *sdn;
+ Slapi_Backend *be;
+ Slapi_Entry **es = NULL;
+ int scope, res;
+ char *filter;
+ char *attrlist[] = {"krbPrincipalKey", "krbLastPwdChange", NULL };
+ krb5_context krbctx = NULL;
+ krb5_principal krbname = NULL;
+ krb5_error_code krberr;
+ int i, kvno;
+ Slapi_Mods *smods;
+ ber_tag_t rtag, ttmp;
+ ber_int_t tint;
+ ber_len_t tlen;
+ struct ipapwd_keyset *kset = NULL;
+ struct tm utctime;
+ char timestr[GENERALIZED_TIME_LENGTH+1];
+ time_t time_now = time(NULL);
+
+ svals = (Slapi_Value **)calloc(2, sizeof(Slapi_Value *));
+ if (!svals) {
+ slapi_log_error(SLAPI_LOG_FATAL, "ipa_pwd_extop",
+ "memory allocation failed\n");
+ rc = LDAP_OPERATIONS_ERROR;
+ goto free_and_return;
+ }
+
+ krberr = krb5_init_context(&krbctx);
+ if (krberr) {
+ slapi_log_error(SLAPI_LOG_FATAL, "ipa_pwd_extop",
+ "krb5_init_context failed\n");
+ rc = LDAP_OPERATIONS_ERROR;
+ goto free_and_return;
+ }
+
+ /* Get Bind DN */
+ slapi_pblock_get(pb, SLAPI_CONN_DN, &bindDN);
+
+ /* If the connection is bound anonymously, we must refuse to process
+ * this operation. */
+ if (bindDN == NULL || *bindDN == '\0') {
+ /* Refuse the operation because they're bound anonymously */
+ errMesg = "Anonymous Binds are not allowed.\n";
+ rc = LDAP_INSUFFICIENT_ACCESS;
+ goto free_and_return;
+ }
+
+ /* Get the ber value of the extended operation */
+ slapi_pblock_get(pb, SLAPI_EXT_OP_REQ_VALUE, &extop_value);
+
+ if ((ber = ber_init(extop_value)) == NULL)
+ {
+ errMesg = "KeytabGet Request decode failed.\n";
+ rc = LDAP_PROTOCOL_ERROR;
+ goto free_and_return;
+ }
+
+ /* Format of request to parse
+ *
+ * KeytabGetRequest ::= SEQUENCE {
+ * serviceIdentity OCTET STRING
+ * keys SEQUENCE OF KrbKey,
+ * ...
+ * }
+ *
+ * KrbKey ::= SEQUENCE {
+ * key [0] EncryptionKey,
+ * salt [1] KrbSalt OPTIONAL,
+ * s2kparams [2] OCTET STRING OPTIONAL,
+ * ...
+ * }
+ *
+ * EncryptionKey ::= SEQUENCE {
+ * keytype [0] Int32,
+ * keyvalue [1] OCTET STRING
+ * }
+ *
+ * KrbSalt ::= SEQUENCE {
+ * type [0] Int32,
+ * salt [1] OCTET STRING OPTIONAL
+ * }
+ */
+
+ /* ber parse code */
+ rtag = ber_scanf(ber, "{a{", &serviceName);
+ if (rtag == LBER_ERROR) {
+ slapi_log_error(SLAPI_LOG_FATAL, "ipa_pwd_extop",
+ "ber_scanf failed\n");
+ errMesg = "Invalid payload, failed to decode.\n";
+ rc = LDAP_PROTOCOL_ERROR;
+ goto free_and_return;
+ }
+
+ /* make sure it is a valid name */
+ krberr = krb5_parse_name(krbctx, serviceName, &krbname);
+ if (krberr) {
+ slapi_ch_free_string(&serviceName);
+ slapi_log_error(SLAPI_LOG_FATAL, "ipa_pwd_extop",
+ "krb5_parse_name failed\n");
+ rc = LDAP_OPERATIONS_ERROR;
+ goto free_and_return;
+ } else {
+ /* invert so that we get the canonical form
+ * (add REALM if not present for example) */
+ char *canonname;
+ krberr = krb5_unparse_name(krbctx, krbname, &canonname);
+ if (krberr) {
+ slapi_ch_free_string(&serviceName);
+ slapi_log_error(SLAPI_LOG_FATAL, "ipa_pwd_extop",
+ "krb5_unparse_name failed\n");
+ rc = LDAP_OPERATIONS_ERROR;
+ goto free_and_return;
+ }
+ slapi_ch_free_string(&serviceName);
+ serviceName = canonname;
+ }
+
+ /* check entry before doing any other decoding */
+
+ /* Find ancestor base DN */
+ sdn = slapi_sdn_new_dn_byval(ipa_realm_dn);
+ be = slapi_be_select(sdn);
+ slapi_sdn_free(&sdn);
+ bsdn = slapi_be_getsuffix(be, 0);
+ if (bsdn == NULL) {
+ slapi_log_error(SLAPI_LOG_TRACE, "ipa_pwd_extop",
+ "Search for Base DN failed\n");
+ errMesg = "PrincipalName not found.\n";
+ rc = LDAP_NO_SUCH_OBJECT;
+ goto free_and_return;
+ }
+ bdn = slapi_sdn_get_dn(bsdn);
+ scope = LDAP_SCOPE_SUBTREE;
+
+ /* get Entry by krbPrincipalName */
+ filter = slapi_ch_smprintf("(krbPrincipalName=%s)", serviceName);
+
+ pbte = slapi_pblock_new();
+ slapi_search_internal_set_pb(pbte,
+ bdn, scope, filter, attrlist, 0,
+ NULL, /* Controls */
+ NULL, /* UniqueID */
+ ipapwd_plugin_id,
+ 0); /* Flags */
+
+ /* do search the tree */
+ ret = slapi_search_internal_pb(pbte);
+ slapi_pblock_get(pbte, SLAPI_PLUGIN_INTOP_RESULT, &res);
+ if (ret == -1 || res != LDAP_SUCCESS) {
+ slapi_log_error(SLAPI_LOG_TRACE, "ipa_pwd_extop",
+ "Search for Principal failed, err (%d)\n",
+ res?res:ret);
+ errMesg = "PrincipalName not found.\n";
+ rc = LDAP_NO_SUCH_OBJECT;
+ goto free_and_return;
+ }
+
+ /* get entries */
+ slapi_pblock_get(pbte, SLAPI_PLUGIN_INTOP_SEARCH_ENTRIES, &es);
+ if (!es) {
+ slapi_log_error(SLAPI_LOG_TRACE, "ipa_pwd_extop", "No entries ?!");
+ errMesg = "PrincipalName not found.\n";
+ rc = LDAP_NO_SUCH_OBJECT;
+ goto free_and_return;
+ }
+
+ /* count entries */
+ for (i = 0; es[i]; i++) /* count */ ;
+
+ /* if there is none or more than one, freak out */
+ if (i != 1) {
+ slapi_log_error(SLAPI_LOG_TRACE, "ipa_pwd_extop",
+ "Too many entries, or entry no found (%d)", i);
+ errMesg = "PrincipalName not found.\n";
+ rc = LDAP_NO_SUCH_OBJECT;
+ goto free_and_return;
+ }
+ targetEntry = es[0];
+
+ /* First thing to do is to ask access control if the bound identity has
+ * rights to modify the userpassword attribute on this entry. If not,
+ * then we fail immediately with insufficient access. This means that
+ * we don't leak any useful information to the client such as current
+ * password wrong, etc.
+ */
+
+ is_root = slapi_dn_isroot(bindDN);
+ slapi_pblock_set(pb, SLAPI_REQUESTOR_ISROOT, &is_root);
+
+ /* In order to perform the access control check,
+ * we need to select a backend (even though
+ * we don't actually need it otherwise).
+ */
+ slapi_pblock_set(pb, SLAPI_BACKEND, be);
+
+ /* Access Strategy:
+ * If the user has WRITE-ONLY access, a new keytab is set on the entry.
+ */
+
+ ret = slapi_access_allowed(pb, targetEntry, "krbPrincipalKey", NULL, SLAPI_ACL_WRITE);
+ if (ret != LDAP_SUCCESS) {
+ errMesg = "Insufficient access rights\n";
+ rc = LDAP_INSUFFICIENT_ACCESS;
+ goto free_and_return;
+ }
+
+ /* increment kvno (will be 1 if this is a new entry) */
+ kvno = ipapwd_get_cur_kvno(targetEntry) + 1;
+
+ /* ok access allowed, init kset and continue to parse ber buffer */
+
+ errMesg = "Unable to set key\n";
+ rc = LDAP_OPERATIONS_ERROR;
+
+ kset = malloc(sizeof(struct ipapwd_keyset));
+ if (!kset) {
+ slapi_log_error(SLAPI_LOG_FATAL, "ipa_pwd_extop", "malloc failed!\n");
+ goto free_and_return;
+ }
+
+ /* this encoding assumes all keys have the same kvno */
+ /* major-vno = 1 and minor-vno = 1 */
+ kset->major_vno = 1;
+ kset->minor_vno = 1;
+ kset->kvno = kvno;
+ /* we also assum mkvno is 0 */
+ kset->mkvno = 0;
+
+ kset->keys = NULL;
+ kset->num_keys = 0;
+
+ rtag = ber_peek_tag(ber, &tlen);
+ while (rtag == LBER_SEQUENCE) {
+ krb5_data plain;
+ krb5_enc_data cipher;
+ struct berval tval;
+ krb5_octet *kdata;
+ size_t klen;
+
+ i = kset->num_keys;
+
+ if (kset->keys) {
+ struct ipapwd_krbkey *newset;
+
+ newset = realloc(kset->keys, sizeof(struct ipapwd_krbkey) * (i + 1));
+ if (!newset) {
+ slapi_log_error(SLAPI_LOG_FATAL, "ipa_pwd_extop", "malloc failed!\n");
+ goto free_and_return;
+ }
+ kset->keys = newset;
+ } else {
+ kset->keys = malloc(sizeof(struct ipapwd_krbkey));
+ if (!kset->keys) {
+ slapi_log_error(SLAPI_LOG_FATAL, "ipa_pwd_extop", "malloc failed!\n");
+ goto free_and_return;
+ }
+ }
+ kset->num_keys += 1;
+
+ kset->keys[i].salt = NULL;
+ kset->keys[i].ekey = NULL;
+ kset->keys[i].s2kparams.bv_len = 0;
+ kset->keys[i].s2kparams.bv_val = NULL;
+
+ /* EncryptionKey */
+ rtag = ber_scanf(ber, "{t[{t[i]t[o]}]", &ttmp, &ttmp, &tint, &ttmp, &tval);
+ if (rtag == LBER_ERROR) {
+ slapi_log_error(SLAPI_LOG_FATAL, "ipa_pwd_extop", "ber_scanf failed\n");
+ errMesg = "Invalid payload, failed to decode.\n";
+ rc = LDAP_PROTOCOL_ERROR;
+ goto free_and_return;
+ }
+
+ kset->keys[i].ekey = calloc(1, sizeof(struct ipapwd_krbkeydata));
+ if (!kset->keys[i].ekey) {
+ slapi_log_error(SLAPI_LOG_FATAL, "ipa_pwd_extop", "malloc failed!\n");
+ goto free_and_return;
+ }
+
+ kset->keys[i].ekey->type = tint;
+
+ plain.length = tval.bv_len;
+ plain.data = tval.bv_val;
+
+ krberr = krb5_c_encrypt_length(krbctx, krbcfg->kmkey->enctype, plain.length, &klen);
+ if (krberr) {
+ free(tval.bv_val);
+ slapi_log_error(SLAPI_LOG_FATAL, "ipa_pwd_extop", "krb encryption failed!\n");
+ goto free_and_return;
+ }
+
+ kdata = malloc(2 + klen);
+ if (!kdata) {
+ free(tval.bv_val);
+ slapi_log_error(SLAPI_LOG_FATAL, "ipa_pwd_extop", "malloc failed!\n");
+ goto free_and_return;
+ }
+ encode_int16(plain.length, kdata);
+
+ kset->keys[i].ekey->value.bv_len = 2 + klen;
+ kset->keys[i].ekey->value.bv_val = (char *)kdata;
+
+ cipher.ciphertext.length = klen;
+ cipher.ciphertext.data = (char *)kdata + 2;
+
+ krberr = krb5_c_encrypt(krbctx, krbcfg->kmkey, 0, 0, &plain, &cipher);
+ if (krberr) {
+ free(tval.bv_val);
+ slapi_log_error(SLAPI_LOG_FATAL, "ipa_pwd_extop", "krb encryption failed!\n");
+ goto free_and_return;
+ }
+
+ free(tval.bv_val);
+
+ rtag = ber_peek_tag(ber, &tlen);
+
+ /* KrbSalt */
+ if (rtag == (ber_tag_t)(LBER_CONSTRUCTED | LBER_CLASS_CONTEXT | 1)) {
+
+ rtag = ber_scanf(ber, "t[{t[i]", &ttmp, &ttmp, &tint);
+ if (rtag == LBER_ERROR) {
+ slapi_log_error(SLAPI_LOG_FATAL, "ipa_pwd_extop", "ber_scanf failed\n");
+ errMesg = "Invalid payload, failed to decode.\n";
+ rc = LDAP_PROTOCOL_ERROR;
+ goto free_and_return;
+ }
+
+ kset->keys[i].salt = calloc(1, sizeof(struct ipapwd_krbkeydata));
+ if (!kset->keys[i].salt) {
+ slapi_log_error(SLAPI_LOG_FATAL, "ipa_pwd_extop", "malloc failed!\n");
+ goto free_and_return;
+ }
+
+ kset->keys[i].salt->type = tint;
+
+ rtag = ber_peek_tag(ber, &tlen);
+ if (rtag == (ber_tag_t)(LBER_CONSTRUCTED | LBER_CLASS_CONTEXT | 1)) {
+
+ rtag = ber_scanf(ber, "t[o]}]", &ttmp, &tval);
+ if (rtag == LBER_ERROR) {
+ slapi_log_error(SLAPI_LOG_FATAL, "ipa_pwd_extop", "ber_scanf failed\n");
+ errMesg = "Invalid payload, failed to decode.\n";
+ rc = LDAP_PROTOCOL_ERROR;
+ goto free_and_return;
+ }
+
+ kset->keys[i].salt->value = tval;
+
+ rtag = ber_peek_tag(ber, &tlen);
+ }
+ }
+
+ /* FIXME: s2kparams - NOT implemented yet */
+ if (rtag == (ber_tag_t)(LBER_CONSTRUCTED | LBER_CLASS_CONTEXT | 2)) {
+ rtag = ber_scanf(ber, "t[x]}", &ttmp);
+ } else {
+ rtag = ber_scanf(ber, "}", &ttmp);
+ }
+ if (rtag == LBER_ERROR) {
+ slapi_log_error(SLAPI_LOG_FATAL, "ipa_pwd_extop", "ber_scanf failed\n");
+ errMesg = "Invalid payload, failed to decode.\n";
+ rc = LDAP_PROTOCOL_ERROR;
+ goto free_and_return;
+ }
+
+ rtag = ber_peek_tag(ber, &tlen);
+ }
+
+ ber_free(ber, 1);
+ ber = NULL;
+
+ /* filter un-supported encodings */
+ ret = filter_keys(krbcfg, kset);
+ if (ret) {
+ slapi_log_error(SLAPI_LOG_FATAL, "ipa_pwd_extop",
+ "keyset filtering failed\n");
+ goto free_and_return;
+ }
+
+ /* check if we have any left */
+ if (kset->num_keys == 0) {
+ slapi_log_error(SLAPI_LOG_FATAL, "ipa_pwd_extop",
+ "keyset filtering rejected all proposed keys\n");
+ errMesg = "All enctypes provided are unsupported";
+ rc = LDAP_UNWILLING_TO_PERFORM;
+ goto free_and_return;
+ }
+
+ smods = slapi_mods_new();
+
+ /* change Last Password Change field with the current date */
+ if (!gmtime_r(&(time_now), &utctime)) {
+ slapi_log_error(SLAPI_LOG_FATAL, "ipa_pwd_extop",
+ "failed to retrieve current date (buggy gmtime_r ?)\n");
+ slapi_mods_free(&smods);
+ goto free_and_return;
+ }
+ strftime(timestr, GENERALIZED_TIME_LENGTH+1, "%Y%m%d%H%M%SZ", &utctime);
+ slapi_mods_add_string(smods, LDAP_MOD_REPLACE, "krbLastPwdChange", timestr);
+
+ /* FIXME: set Password Expiration date ? */
+#if 0
+ if (!gmtime_r(&(data->expireTime), &utctime)) {
+ slapi_log_error(SLAPI_LOG_FATAL, "ipa_pwd_extop",
+ "failed to convert expiration date\n");
+ slapi_ch_free_string(&randPasswd);
+ slapi_mods_free(&smods);
+ rc = LDAP_OPERATIONS_ERROR;
+ goto free_and_return;
+ }
+ strftime(timestr, GENERALIZED_TIME_LENGTH+1, "%Y%m%d%H%M%SZ", &utctime);
+ slapi_mods_add_string(smods, LDAP_MOD_REPLACE, "krbPasswordExpiration", timestr);
+#endif
+
+ bval = encode_keys(kset);
+ if (!bval) {
+ slapi_log_error(SLAPI_LOG_FATAL, "ipa_pwd_extop",
+ "encoding asn1 KrbSalt failed\n");
+ slapi_mods_free(&smods);
+ goto free_and_return;
+ }
+
+ svals[0] = slapi_value_new_berval(bval);
+ if (!svals[0]) {
+ slapi_log_error(SLAPI_LOG_FATAL, "ipa_pwd_extop",
+ "Converting berval to Slapi_Value\n");
+ slapi_mods_free(&smods);
+ goto free_and_return;
+ }
+
+ slapi_mods_add_mod_values(smods, LDAP_MOD_REPLACE, "krbPrincipalKey", svals);
+
+ /* commit changes */
+ ret = ipapwd_apply_mods(slapi_entry_get_dn_const(targetEntry), smods);
+
+ if (ret != LDAP_SUCCESS) {
+ slapi_mods_free(&smods);
+ goto free_and_return;
+
+ }
+ slapi_mods_free(&smods);
+
+ /* Format of response
+ *
+ * KeytabGetRequest ::= SEQUENCE {
+ * new_kvno Int32
+ * SEQUENCE OF KeyTypes
+ * }
+ *
+ * * List of accepted enctypes *
+ * KeyTypes ::= SEQUENCE {
+ * enctype Int32
+ * }
+ */
+
+ errMesg = "Internal Error\n";
+ rc = LDAP_OPERATIONS_ERROR;
+
+ ber = ber_alloc();
+ if (!ber) {
+ goto free_and_return;
+ }
+
+ ret = ber_printf(ber, "{i{", (ber_int_t)kvno);
+ if (ret == -1) {
+ goto free_and_return;
+ }
+
+ for (i = 0; i < kset->num_keys; i++) {
+ ret = ber_printf(ber, "{i}", (ber_int_t)kset->keys[i].ekey->type);
+ if (ret == -1) {
+ goto free_and_return;
+ }
+ }
+ ret = ber_printf(ber, "}}");
+ if (ret == -1) {
+ goto free_and_return;
+ }
+
+ if (ret != -1) {
+ struct berval *bvp;
+ LDAPControl new_ctrl = {0};
+
+ ret = ber_flatten(ber, &bvp);
+ if (ret == -1) {
+ goto free_and_return;
+ }
+
+ new_ctrl.ldctl_oid = KEYTAB_RET_OID;
+ new_ctrl.ldctl_value = *bvp;
+ new_ctrl.ldctl_iscritical = 0;
+ rc= slapi_pblock_set(pb, SLAPI_ADD_RESCONTROL, &new_ctrl);
+ ber_bvfree(bvp);
+ }
+
+ /* Free anything that we allocated above */
+free_and_return:
+ free(serviceName);
+ if (kset) ipapwd_keyset_free(&kset);
+
+ if (bval) ber_bvfree(bval);
+ if (ber) ber_free(ber, 1);
+
+ if (pbte) {
+ slapi_free_search_results_internal(pbte);
+ slapi_pblock_destroy(pbte);
+ }
+ if (svals) {
+ for (i = 0; svals[i]; i++) {
+ slapi_value_free(&svals[i]);
+ }
+ free(svals);
+ }
+
+ if (krbname) krb5_free_principal(krbctx, krbname);
+ if (krbctx) krb5_free_context(krbctx);
+
+ slapi_log_error(SLAPI_LOG_PLUGIN, "ipa_pwd_extop", errMesg ? errMesg : "success");
+ slapi_send_ldap_result(pb, rc, NULL, errMesg, 0, NULL);
+
+ return SLAPI_PLUGIN_EXTENDED_SENT_RESULT;
+}
+
+static int new_ipapwd_encsalt(krb5_context krbctx, const char * const *encsalts,
+ struct ipapwd_encsalt **es_types, int *num_es_types)
+{
+ struct ipapwd_encsalt *es;
+ int nes, i;
+
+ for (i = 0; encsalts[i]; i++) /* count */ ;
+ es = calloc(i + 1, sizeof(struct ipapwd_encsalt));
+ if (!es) {
+ slapi_log_error(SLAPI_LOG_FATAL, "ipapwd_start", "Out of memory!\n");
+ return LDAP_OPERATIONS_ERROR;
+ }
+
+ for (i = 0, nes = 0; encsalts[i]; i++) {
+ char *enc, *salt;
+ krb5_int32 tmpsalt;
+ krb5_enctype tmpenc;
+ krb5_boolean similar;
+ krb5_error_code krberr;
+ int j;
+
+ enc = strdup(encsalts[i]);
+ if (!enc) {
+ slapi_log_error(SLAPI_LOG_PLUGIN, "ipapwd_start",
+ "Allocation error\n");
+ return LDAP_OPERATIONS_ERROR;
+ }
+ salt = strchr(enc, ':');
+ if (!salt) {
+ slapi_log_error(SLAPI_LOG_PLUGIN, "ipapwd_start",
+ "Invalid krb5 enc string\n");
+ free(enc);
+ continue;
+ }
+ *salt = '\0'; /* null terminate the enc type */
+ salt++; /* skip : */
+
+ krberr = krb5_string_to_enctype(enc, &tmpenc);
+ if (krberr) {
+ slapi_log_error(SLAPI_LOG_PLUGIN, "ipapwd_start",
+ "Invalid krb5 enctype\n");
+ free(enc);
+ continue;
+ }
+
+ krberr = krb5_string_to_salttype(salt, &tmpsalt);
+ for (j = 0; j < nes; j++) {
+ krb5_c_enctype_compare(krbctx, es[j].enc_type, tmpenc, &similar);
+ if (similar && (es[j].salt_type == tmpsalt)) {
+ break;
+ }
+ }
+
+ if (j == nes) {
+ /* not found */
+ es[j].enc_type = tmpenc;
+ es[j].salt_type = tmpsalt;
+ nes++;
+ }
+
+ free(enc);
+ }
+
+ *es_types = es;
+ *num_es_types = nes;
+
+ return LDAP_SUCCESS;
+}
+
+static struct ipapwd_krbcfg *ipapwd_getConfig(void)
+{
+ krb5_error_code krberr;
+ struct ipapwd_krbcfg *config = NULL;
+ krb5_keyblock *kmkey = NULL;
+ Slapi_Entry *realm_entry = NULL;
+ Slapi_Entry *config_entry = NULL;
+ Slapi_Attr *a;
+ Slapi_Value *v;
+ BerElement *be = NULL;
+ ber_tag_t tag, tmp;
+ ber_int_t ttype;
+ const struct berval *bval;
+ struct berval *mkey = NULL;
+ char **encsalts;
+ char *tmpstr;
+ int i, ret;
+
+ config = calloc(1, sizeof(struct ipapwd_krbcfg));
+ if (!config) {
+ slapi_log_error(SLAPI_LOG_FATAL, "ipapwd_getConfig",
+ "Out of memory!\n");
+ goto free_and_error;
+ }
+ kmkey = calloc(1, sizeof(krb5_keyblock));
+ if (!kmkey) {
+ slapi_log_error(SLAPI_LOG_FATAL, "ipapwd_getConfig",
+ "Out of memory!\n");
+ goto free_and_error;
+ }
+ config->kmkey = kmkey;
+
+ krberr = krb5_init_context(&config->krbctx);
+ if (krberr) {
+ slapi_log_error(SLAPI_LOG_FATAL, "ipapwd_getConfig",
+ "krb5_init_context failed\n");
+ goto free_and_error;
+ }
+
+ ret = krb5_get_default_realm(config->krbctx, &config->realm);
+ if (ret) {
+ slapi_log_error(SLAPI_LOG_FATAL, "ipapwd_getConfig",
+ "Failed to get default realm?!\n");
+ goto free_and_error;
+ }
+
+ /* get the Realm Container entry */
+ ret = ipapwd_getEntry(ipa_realm_dn, &realm_entry, NULL);
+ if (ret != LDAP_SUCCESS) {
+ slapi_log_error(SLAPI_LOG_FATAL, "ipapwd_getConfig",
+ "No realm Entry?\n");
+ goto free_and_error;
+ }
+
+ /*** get the Kerberos Master Key ***/
+
+ ret = slapi_entry_attr_find(realm_entry, "krbMKey", &a);
+ if (ret == -1) {
+ slapi_log_error(SLAPI_LOG_FATAL, "ipapwd_getConfig",
+ "No master key??\n");
+ goto free_and_error;
+ }
+
+ /* there should be only one value here */
+ ret = slapi_attr_first_value(a, &v);
+ if (ret == -1) {
+ slapi_log_error(SLAPI_LOG_FATAL, "ipapwd_getConfig",
+ "No master key??\n");
+ goto free_and_error;
+ }
+
+ bval = slapi_value_get_berval(v);
+ if (!bval) {
+ slapi_log_error(SLAPI_LOG_FATAL, "ipapwd_getConfig",
+ "Error retrieving master key berval\n");
+ goto free_and_error;
+ }
+
+ be = ber_init(bval);
+ if (!bval) {
+ slapi_log_error(SLAPI_LOG_FATAL, "ipapwd_getConfig",
+ "ber_init() failed!\n");
+ goto free_and_error;
+ }
+
+ tag = ber_scanf(be, "{i{iO}}", &tmp, &ttype, &mkey);
+ if (tag == LBER_ERROR) {
+ slapi_log_error(SLAPI_LOG_TRACE, "ipapwd_getConfig",
+ "Bad Master key encoding ?!\n");
+ goto free_and_error;
+ }
+
+ kmkey->magic = KV5M_KEYBLOCK;
+ kmkey->enctype = ttype;
+ kmkey->length = mkey->bv_len;
+ kmkey->contents = malloc(mkey->bv_len);
+ if (!kmkey->contents) {
+ slapi_log_error(SLAPI_LOG_FATAL, "ipapwd_getConfig",
+ "Out of memory!\n");
+ goto free_and_error;
+ }
+ memcpy(kmkey->contents, mkey->bv_val, mkey->bv_len);
+ ber_bvfree(mkey);
+ ber_free(be, 1);
+ mkey = NULL;
+ be = NULL;
+
+ /*** get the Supported Enc/Salt types ***/
+
+ encsalts = slapi_entry_attr_get_charray(realm_entry, "krbSupportedEncSaltTypes");
+ if (encsalts) {
+ ret = new_ipapwd_encsalt(config->krbctx,
+ (const char * const *)encsalts,
+ &config->supp_encsalts,
+ &config->num_supp_encsalts);
+ slapi_ch_array_free(encsalts);
+ } else {
+ slapi_log_error(SLAPI_LOG_TRACE, "ipapwd_getConfig",
+ "No configured salt types use defaults\n");
+ ret = new_ipapwd_encsalt(config->krbctx,
+ ipapwd_def_encsalts,
+ &config->supp_encsalts,
+ &config->num_supp_encsalts);
+ }
+ if (ret) {
+ slapi_log_error(SLAPI_LOG_FATAL, "ipapwd_getConfig",
+ "Can't get Supported EncSalt Types\n");
+ goto free_and_error;
+ }
+
+ /*** get the Preferred Enc/Salt types ***/
+
+ encsalts = slapi_entry_attr_get_charray(realm_entry, "krbDefaultEncSaltTypes");
+ if (encsalts) {
+ ret = new_ipapwd_encsalt(config->krbctx,
+ (const char * const *)encsalts,
+ &config->pref_encsalts,
+ &config->num_pref_encsalts);
+ slapi_ch_array_free(encsalts);
+ } else {
+ slapi_log_error(SLAPI_LOG_TRACE, "ipapwd_getConfig",
+ "No configured salt types use defaults\n");
+ ret = new_ipapwd_encsalt(config->krbctx,
+ ipapwd_def_encsalts,
+ &config->pref_encsalts,
+ &config->num_pref_encsalts);
+ }
+ if (ret) {
+ slapi_log_error(SLAPI_LOG_FATAL, "ipapwd_getConfig",
+ "Can't get Preferred EncSalt Types\n");
+ goto free_and_error;
+ }
+
+ slapi_entry_free(realm_entry);
+
+ /* get the Realm Container entry */
+ ret = ipapwd_getEntry(ipa_pwd_config_dn, &config_entry, NULL);
+ if (ret != LDAP_SUCCESS) {
+ slapi_log_error(SLAPI_LOG_FATAL, "ipapwd_getConfig",
+ "No config Entry? Impossible!\n");
+ goto free_and_error;
+ }
+ config->passsync_mgrs = slapi_entry_attr_get_charray(config_entry, "passSyncManagersDNs");
+ /* now add Directory Manager, it is always added by default */
+ tmpstr = slapi_ch_strdup("cn=Directory Manager");
+ slapi_ch_array_add(&config->passsync_mgrs, tmpstr);
+ if (config->passsync_mgrs == NULL) {
+ slapi_log_error(SLAPI_LOG_FATAL, "ipapwd_getConfig",
+ "Out of memory!\n");
+ goto free_and_error;
+ }
+ for (i = 0; config->passsync_mgrs[i]; i++) /* count */ ;
+ config->num_passsync_mgrs = i;
+
+ return config;
+
+free_and_error:
+ if (mkey) ber_bvfree(mkey);
+ if (be) ber_free(be, 1);
+ if (kmkey) {
+ free(kmkey->contents);
+ free(kmkey);
+ }
+ if (config) {
+ if (config->krbctx) {
+ if (config->realm)
+ krb5_free_default_realm(config->krbctx, config->realm);
+ krb5_free_context(config->krbctx);
+ }
+ free(config->pref_encsalts);
+ free(config->supp_encsalts);
+ slapi_ch_array_free(config->passsync_mgrs);
+ free(config);
+ }
+ slapi_entry_free(config_entry);
+ slapi_entry_free(realm_entry);
+ return NULL;
+}
+
+#define IPAPWD_CHECK_CONN_SECURE 0x00000001
+#define IPAPWD_CHECK_DN 0x00000002
+
+static int ipapwd_gen_checks(Slapi_PBlock *pb, char **errMesg,
+ struct ipapwd_krbcfg **config,
+ int check_flags)
+{
+ int ret, sasl_ssf, is_ssl;
+ int rc = LDAP_SUCCESS;
+ Slapi_Backend *be;
+ const Slapi_DN *psdn;
+ Slapi_DN *sdn;
+ char *dn = NULL;
+
+ slapi_log_error(SLAPI_LOG_TRACE, "ipa_pwd_extop", "=> ipapwd_gen_checks\n");
+
+#ifdef LDAP_EXTOP_PASSMOD_CONN_SECURE
+ if (check_flags & IPAPWD_CHECK_CONN_SECURE) {
+ /* Allow password modify only for SSL/TLS established connections and
+ * connections using SASL privacy layers */
+ if (slapi_pblock_get(pb, SLAPI_CONN_SASL_SSF, &sasl_ssf) != 0) {
+ slapi_log_error(SLAPI_LOG_PLUGIN, "ipa_pwd_extop",
+ "Could not get SASL SSF from connection\n");
+ *errMesg = "Operation requires a secure connection.\n";
+ rc = LDAP_OPERATIONS_ERROR;
+ goto done;
+ }
+
+ if (slapi_pblock_get(pb, SLAPI_CONN_IS_SSL_SESSION, &is_ssl) != 0) {
+ slapi_log_error(SLAPI_LOG_PLUGIN, "ipa_pwd_extop",
+ "Could not get IS SSL from connection\n");
+ *errMesg = "Operation requires a secure connection.\n";
+ rc = LDAP_OPERATIONS_ERROR;
+ goto done;
+ }
+
+ if ((0 == is_ssl) && (sasl_ssf <= 1)) {
+ *errMesg = "Operation requires a secure connection.\n";
+ rc = LDAP_CONFIDENTIALITY_REQUIRED;
+ goto done;
+ }
+ }
+#endif
+
+ if (check_flags & IPAPWD_CHECK_DN) {
+ /* check we have a valid DN in the pblock or just abort */
+ ret = slapi_pblock_get(pb, SLAPI_TARGET_DN, &dn);
+ if (ret) {
+ slapi_log_error(SLAPI_LOG_PLUGIN, "ipa_pwd_extop",
+ "Tried to change password for an invalid DN [%s]\n",
+ dn?dn:"<NULL>");
+ *errMesg = "Invalid DN";
+ rc = LDAP_OPERATIONS_ERROR;
+ goto done;
+ }
+ sdn = slapi_sdn_new_dn_byref(dn);
+ if (!sdn) {
+ *errMesg = "Internal Error";
+ rc = LDAP_OPERATIONS_ERROR;
+ goto done;
+ }
+ be = slapi_be_select(sdn);
+ slapi_sdn_free(&sdn);
+
+ psdn = slapi_be_getsuffix(be, 0);
+ if (!psdn) {
+ *errMesg = "Invalid DN";
+ rc = LDAP_OPERATIONS_ERROR;
+ goto done;
+ }
+ }
+
+ /* get the kerberos context and master key */
+ *config = ipapwd_getConfig();
+ if (NULL == *config) {
+ slapi_log_error(SLAPI_LOG_PLUGIN, "ipa_pwd_extop",
+ "Error Retrieving Master Key");
+ *errMesg = "Fatal Internal Error";
+ rc = LDAP_OPERATIONS_ERROR;
+ }
+
+done:
+ return rc;
+}
+
+static int ipapwd_extop(Slapi_PBlock *pb)
+{
+ struct ipapwd_krbcfg *krbcfg = NULL;
+ char *errMesg = NULL;
+ char *oid = NULL;
+ int rc, ret;
+
+ slapi_log_error(SLAPI_LOG_TRACE, "ipa_pwd_extop", "=> ipapwd_extop\n");
+
+ rc = ipapwd_gen_checks(pb, &errMesg, &krbcfg, IPAPWD_CHECK_CONN_SECURE);
+ if (rc) {
+ goto free_and_return;
+ }
+
+ /* Before going any further, we'll make sure that the right extended
+ * operation plugin has been called: i.e., the OID shipped whithin the
+ * extended operation request must match this very plugin's OIDs:
+ * EXOP_PASSWD_OID or KEYTAB_SET_OID. */
+ if (slapi_pblock_get(pb, SLAPI_EXT_OP_REQ_OID, &oid) != 0) {
+ errMesg = "Could not get OID value from request.\n";
+ rc = LDAP_OPERATIONS_ERROR;
+ slapi_log_error(SLAPI_LOG_PLUGIN, "ipa_pwd_extop", errMesg);
+ goto free_and_return;
+ } else {
+ slapi_log_error(SLAPI_LOG_PLUGIN, "ipa_pwd_extop",
+ "Received extended operation request with OID %s\n", oid);
+ }
+
+ if (strcasecmp(oid, EXOP_PASSWD_OID) == 0) {
+ ret = ipapwd_chpwop(pb, krbcfg);
+ free_ipapwd_krbcfg(&krbcfg);
+ return ret;
+ }
+ if (strcasecmp(oid, KEYTAB_SET_OID) == 0) {
+ ret = ipapwd_setkeytab(pb, krbcfg);
+ free_ipapwd_krbcfg(&krbcfg);
+ return ret;
+ }
+
+ errMesg = "Request OID does not match supported OIDs.\n";
+ rc = LDAP_OPERATIONS_ERROR;
+
+free_and_return:
+ if (krbcfg) free_ipapwd_krbcfg(&krbcfg);
+
+ slapi_log_error(SLAPI_LOG_PLUGIN, "ipa_pwd_extop", errMesg);
+ slapi_send_ldap_result(pb, rc, NULL, errMesg, 0, NULL);
+
+ return SLAPI_PLUGIN_EXTENDED_SENT_RESULT;
+}
+
+/*****************************************************************************
+ * pre/post operations to intercept writes to userPassword
+ ****************************************************************************/
+
+#define IPAPWD_OP_NULL 0
+#define IPAPWD_OP_ADD 1
+#define IPAPWD_OP_MOD 2
+struct ipapwd_operation {
+ struct ipapwd_data pwdata;
+ int pwd_op;
+ int is_krb;
+};
+
+/* structure with information for each extension */
+struct ipapwd_op_ext {
+ char *object_name; /* name of the object extended */
+ int object_type; /* handle to the extended object */
+ int handle; /* extension handle */
+};
+
+static struct ipapwd_op_ext ipapwd_op_ext_list;
+
+static void *ipapwd_op_ext_constructor(void *object, void *parent)
+{
+ struct ipapwd_operation *ext;
+
+ ext = (struct ipapwd_operation *)slapi_ch_calloc(1, sizeof(struct ipapwd_operation));
+ return ext;
+}
+
+static void ipapwd_op_ext_destructor(void *ext, void *object, void *parent)
+{
+ struct ipapwd_operation *pwdop = (struct ipapwd_operation *)ext;
+ if (!pwdop)
+ return;
+ if (pwdop->pwd_op != IPAPWD_OP_NULL) {
+ slapi_ch_free_string(&(pwdop->pwdata.dn));
+ slapi_ch_free_string(&(pwdop->pwdata.password));
+ }
+ slapi_ch_free((void **)&pwdop);
+}
+
+static int ipapwd_entry_checks(Slapi_PBlock *pb, struct slapi_entry *e,
+ int *is_root, int *is_krb, int *is_smb,
+ char *attr, int access)
+{
+ Slapi_Value *sval;
+ int rc;
+
+ /* Check ACIs */
+ slapi_pblock_get(pb, SLAPI_REQUESTOR_ISROOT, is_root);
+
+ if (!*is_root) {
+ /* verify this user is allowed to write a user password */
+ rc = slapi_access_allowed(pb, e, attr, NULL, access);
+ if (rc != LDAP_SUCCESS) {
+ /* we have no business here, the operation will be denied anyway */
+ rc = LDAP_SUCCESS;
+ goto done;
+ }
+ }
+
+ /* Check if this is a krbPrincial and therefore needs us to generate other
+ * hashes */
+ sval = slapi_value_new_string("krbPrincipalAux");
+ if (!sval) {
+ rc = LDAP_OPERATIONS_ERROR;
+ goto done;
+ }
+ *is_krb = slapi_entry_attr_has_syntax_value(e, SLAPI_ATTR_OBJECTCLASS, sval);
+ slapi_value_free(&sval);
+
+ sval = slapi_value_new_string("sambaSamAccount");
+ if (!sval) {
+ rc = LDAP_OPERATIONS_ERROR;
+ goto done;
+ }
+ *is_smb = slapi_entry_attr_has_syntax_value(e, SLAPI_ATTR_OBJECTCLASS, sval);
+ slapi_value_free(&sval);
+
+ rc = LDAP_SUCCESS;
+
+done:
+ return rc;
+}
+
+static int ipapwd_preop_gen_hashes(struct ipapwd_krbcfg *krbcfg,
+ struct ipapwd_operation *pwdop,
+ char *userpw,
+ int is_krb, int is_smb,
+ Slapi_Value ***svals,
+ char **nthash, char **lmhash)
+{
+ int rc;
+
+ if (is_krb) {
+
+ pwdop->is_krb = 1;
+
+ *svals = encrypt_encode_key(krbcfg, &pwdop->pwdata);
+
+ if (!*svals) {
+ slapi_log_error(SLAPI_LOG_FATAL, IPAPWD_PLUGIN_NAME,
+ "key encryption/encoding failed\n");
+ rc = LDAP_OPERATIONS_ERROR;
+ goto done;
+ }
+ }
+
+ if (is_smb) {
+ char lm[33], nt[33];
+ struct ntlm_keys ntlm;
+ int ntlm_flags = 0;
+ int ret;
+
+ /* TODO: retrieve if we want to store the LM hash or not */
+ ntlm_flags = KTF_LM_HASH | KTF_NT_HASH;
+
+ ret = encode_ntlm_keys(userpw, ntlm_flags, &ntlm);
+ if (ret) {
+ slapi_log_error(SLAPI_LOG_FATAL, IPAPWD_PLUGIN_NAME,
+ "Failed to generate NT/LM hashes\n");
+ rc = LDAP_OPERATIONS_ERROR;
+ goto done;
+ }
+ if (ntlm_flags & KTF_LM_HASH) {
+ hexbuf(lm, ntlm.lm);
+ lm[32] = '\0';
+ *lmhash = slapi_ch_strdup(lm);
+ }
+ if (ntlm_flags & KTF_NT_HASH) {
+ hexbuf(nt, ntlm.nt);
+ nt[32] = '\0';
+ *nthash = slapi_ch_strdup(nt);
+ }
+ }
+
+ rc = LDAP_SUCCESS;
+
+done:
+
+ return rc;
+}
+
+/* PRE ADD Operation:
+ * Gets the clean text password (fail the operation if the password came
+ * pre-hashed, unless this is a replicated operation).
+ * Check user is authorized to add it otherwise just returns, operation will
+ * fail later anyway.
+ * Run a password policy check.
+ * Check if krb or smb hashes are required by testing if the krb or smb
+ * objectclasses are present.
+ * store information for the post operation
+ */
+static int ipapwd_pre_add(Slapi_PBlock *pb)
+{
+ struct ipapwd_krbcfg *krbcfg = NULL;
+ char *errMesg = "Internal operations error\n";
+ struct slapi_entry *e = NULL;
+ char *userpw = NULL;
+ char *dn = NULL;
+ struct ipapwd_operation *pwdop = NULL;
+ void *op;
+ int is_repl_op, is_root, is_krb, is_smb;
+ int ret, rc;
+
+ slapi_log_error(SLAPI_LOG_TRACE, IPAPWD_PLUGIN_NAME, "=> ipapwd_pre_add\n");
+
+ ret = slapi_pblock_get(pb, SLAPI_IS_REPLICATED_OPERATION, &is_repl_op);
+ if (ret != 0) {
+ slapi_log_error(SLAPI_LOG_FATAL, IPAPWD_PLUGIN_NAME,
+ "slapi_pblock_get failed!?\n");
+ rc = LDAP_OPERATIONS_ERROR;
+ goto done;
+ }
+
+ /* pass through if this is a replicated operation */
+ if (is_repl_op)
+ return 0;
+
+ /* retrieve the entry */
+ slapi_pblock_get(pb, SLAPI_ADD_ENTRY, &e);
+ if (NULL == e)
+ return 0;
+
+ /* check this is something interesting for us first */
+ userpw = slapi_entry_attr_get_charptr(e, SLAPI_USERPWD_ATTR);
+ if (!userpw) {
+ /* nothing interesting here */
+ return 0;
+ }
+
+ /* Ok this is interesting,
+ * Check this is a clear text password, or refuse operation */
+ if ('{' == userpw[0]) {
+ if (0 == strncasecmp(userpw, "{CLEAR}", strlen("{CLEAR}"))) {
+ char *tmp = slapi_ch_strdup(&userpw[strlen("{CLEAR}")]);
+ if (NULL == tmp) {
+ slapi_log_error(SLAPI_LOG_FATAL, IPAPWD_PLUGIN_NAME,
+ "Strdup failed, Out of memory\n");
+ rc = LDAP_OPERATIONS_ERROR;
+ goto done;
+ }
+ slapi_ch_free_string(&userpw);
+ userpw = tmp;
+ } else if (slapi_is_encoded(userpw)) {
+
+ slapi_ch_free_string(&userpw);
+
+ /* check if we have access to the unhashed user password */
+ userpw = slapi_entry_attr_get_charptr(e, "unhashed#user#password");
+ if (!userpw) {
+ slapi_log_error(SLAPI_LOG_PLUGIN, IPAPWD_PLUGIN_NAME,
+ "Pre-Encoded passwords are not valid\n");
+ errMesg = "Pre-Encoded passwords are not valid\n";
+ rc = LDAP_CONSTRAINT_VIOLATION;
+ goto done;
+ }
+ }
+ }
+
+ rc = ipapwd_entry_checks(pb, e,
+ &is_root, &is_krb, &is_smb,
+ NULL, SLAPI_ACL_ADD);
+ if (rc) {
+ goto done;
+ }
+
+ rc = ipapwd_gen_checks(pb, &errMesg, &krbcfg, IPAPWD_CHECK_DN);
+ if (rc) {
+ goto done;
+ }
+
+ /* Get target DN */
+ ret = slapi_pblock_get(pb, SLAPI_TARGET_DN, &dn);
+ if (ret) {
+ rc = LDAP_OPERATIONS_ERROR;
+ goto done;
+ }
+
+ /* time to get the operation handler */
+ ret = slapi_pblock_get(pb, SLAPI_OPERATION, &op);
+ if (ret != 0) {
+ slapi_log_error(SLAPI_LOG_FATAL, IPAPWD_PLUGIN_NAME,
+ "slapi_pblock_get failed!?\n");
+ rc = LDAP_OPERATIONS_ERROR;
+ goto done;
+ }
+
+ pwdop = slapi_get_object_extension(ipapwd_op_ext_list.object_type,
+ op, ipapwd_op_ext_list.handle);
+ if (NULL == pwdop) {
+ rc = LDAP_OPERATIONS_ERROR;
+ goto done;
+ }
+
+ pwdop->pwd_op = IPAPWD_OP_ADD;
+ pwdop->pwdata.password = slapi_ch_strdup(userpw);
+
+ if (is_root) {
+ pwdop->pwdata.changetype = IPA_CHANGETYPE_DSMGR;
+ } else {
+ char *binddn;
+ int i;
+
+ pwdop->pwdata.changetype = IPA_CHANGETYPE_ADMIN;
+
+ /* Check Bind DN */
+ slapi_pblock_get(pb, SLAPI_CONN_DN, &binddn);
+
+ /* if it is a passsync manager we also need to skip resets */
+ for (i = 0; i < krbcfg->num_passsync_mgrs; i++) {
+ if (strcasecmp(krbcfg->passsync_mgrs[i], binddn) == 0) {
+ pwdop->pwdata.changetype = IPA_CHANGETYPE_DSMGR;
+ break;
+ }
+ }
+ }
+
+ pwdop->pwdata.dn = slapi_ch_strdup(dn);
+ pwdop->pwdata.timeNow = time(NULL);
+ pwdop->pwdata.target = e;
+
+ ret = ipapwd_CheckPolicy(&pwdop->pwdata);
+ if (ret) {
+ errMesg = "Password Fails to meet minimum strength criteria";
+ rc = LDAP_CONSTRAINT_VIOLATION;
+ goto done;
+ }
+
+ if (is_krb || is_smb) {
+
+ Slapi_Value **svals = NULL;
+ char *nt = NULL;
+ char *lm = NULL;
+
+ rc = ipapwd_preop_gen_hashes(krbcfg,
+ pwdop, userpw,
+ is_krb, is_smb,
+ &svals, &nt, &lm);
+ if (rc) {
+ goto done;
+ }
+
+ if (svals) {
+ /* add/replace values in existing entry */
+ ret = slapi_entry_attr_replace_sv(e, "krbPrincipalKey", svals);
+ if (ret) {
+ slapi_log_error(SLAPI_LOG_FATAL, IPAPWD_PLUGIN_NAME,
+ "failed to set encoded values in entry\n");
+ rc = LDAP_OPERATIONS_ERROR;
+ ipapwd_free_slapi_value_array(&svals);
+ goto done;
+ }
+
+ ipapwd_free_slapi_value_array(&svals);
+ }
+
+ if (lm) {
+ /* set value */
+ slapi_entry_attr_set_charptr(e, "sambaLMPassword", lm);
+ slapi_ch_free_string(&lm);
+ }
+ if (nt) {
+ /* set value */
+ slapi_entry_attr_set_charptr(e, "sambaNTPassword", nt);
+ slapi_ch_free_string(&nt);
+ }
+ }
+
+ rc = LDAP_SUCCESS;
+
+done:
+ if (pwdop) pwdop->pwdata.target = NULL;
+ free_ipapwd_krbcfg(&krbcfg);
+ slapi_ch_free_string(&userpw);
+ if (rc != LDAP_SUCCESS) {
+ slapi_send_ldap_result(pb, rc, NULL, errMesg, 0, NULL);
+ return -1;
+ }
+ return 0;
+}
+
+/* PRE MOD Operation:
+ * Gets the clean text password (fail the operation if the password came
+ * pre-hashed, unless this is a replicated operation).
+ * Check user is authorized to add it otherwise just returns, operation will
+ * fail later anyway.
+ * Check if krb or smb hashes are required by testing if the krb or smb
+ * objectclasses are present.
+ * Run a password policy check.
+ * store information for the post operation
+ */
+static int ipapwd_pre_mod(Slapi_PBlock *pb)
+{
+ struct ipapwd_krbcfg *krbcfg = NULL;
+ char *errMesg = NULL;
+ LDAPMod **mods;
+ Slapi_Mod *smod, *tmod;
+ Slapi_Mods *smods = NULL;
+ char *userpw = NULL;
+ char *unhashedpw = NULL;
+ char *dn = NULL;
+ Slapi_DN *tmp_dn;
+ struct slapi_entry *e = NULL;
+ struct ipapwd_operation *pwdop = NULL;
+ void *op;
+ int is_repl_op, is_pwd_op, is_root, is_krb, is_smb;
+ int ret, rc;
+
+ slapi_log_error(SLAPI_LOG_TRACE, IPAPWD_PLUGIN_NAME, "=> ipapwd_pre_mod\n");
+
+ ret = slapi_pblock_get(pb, SLAPI_IS_REPLICATED_OPERATION, &is_repl_op);
+ if (ret != 0) {
+ slapi_log_error(SLAPI_LOG_FATAL, IPAPWD_PLUGIN_NAME,
+ "slapi_pblock_get failed!?\n");
+ rc = LDAP_OPERATIONS_ERROR;
+ goto done;
+ }
+
+ /* pass through if this is a replicated operation */
+ if (is_repl_op) {
+ rc = LDAP_SUCCESS;
+ goto done;
+ }
+
+ /* grab the mods - we'll put them back later with
+ * our modifications appended
+ */
+ slapi_pblock_get(pb, SLAPI_MODIFY_MODS, &mods);
+ smods = slapi_mods_new();
+ slapi_mods_init_passin(smods, mods);
+
+ /* In the first pass,
+ * only check there is anything we are interested in */
+ is_pwd_op = 0;
+ tmod = slapi_mod_new();
+ smod = slapi_mods_get_first_smod(smods, tmod);
+ while (smod) {
+ struct berval *bv;
+ const char *type;
+ int mop;
+
+ type = slapi_mod_get_type(smod);
+ if (slapi_attr_types_equivalent(type, SLAPI_USERPWD_ATTR)) {
+ mop = slapi_mod_get_operation(smod);
+ /* check op filtering out LDAP_MOD_BVALUES */
+ switch (mop & 0x0f) {
+ case LDAP_MOD_ADD:
+ case LDAP_MOD_REPLACE:
+ is_pwd_op = 1;
+ default:
+ break;
+ }
+ }
+
+ /* we check for unahsehd password here so that we are sure to catch them
+ * early, before further checks go on, this helps checking
+ * LDAP_MOD_DELETE operations in some corner cases later */
+ /* we keep only the last one if multiple are provided for any absurd
+ * reason */
+ if (slapi_attr_types_equivalent(type, "unhashed#user#password")) {
+ bv = slapi_mod_get_first_value(smod);
+ if (!bv) {
+ slapi_mod_free(&tmod);
+ rc = LDAP_OPERATIONS_ERROR;
+ goto done;
+ }
+ slapi_ch_free_string(&unhashedpw);
+ unhashedpw = slapi_ch_malloc(bv->bv_len+1);
+ if (!unhashedpw) {
+ slapi_mod_free(&tmod);
+ rc = LDAP_OPERATIONS_ERROR;
+ goto done;
+ }
+ memcpy(unhashedpw, bv->bv_val, bv->bv_len);
+ unhashedpw[bv->bv_len] = '\0';
+ }
+ slapi_mod_done(tmod);
+ smod = slapi_mods_get_next_smod(smods, tmod);
+ }
+ slapi_mod_free(&tmod);
+
+ /* If userPassword is not modified we are done here */
+ if (! is_pwd_op) {
+ rc = LDAP_SUCCESS;
+ goto done;
+ }
+
+ /* OK swe have something interesting here, start checking for
+ * pre-requisites */
+
+ /* Get target DN */
+ ret = slapi_pblock_get(pb, SLAPI_TARGET_DN, &dn);
+ if (ret) {
+ rc = LDAP_OPERATIONS_ERROR;
+ goto done;
+ }
+
+ tmp_dn = slapi_sdn_new_dn_byref(dn);
+ if (tmp_dn) {
+ /* xxxPAR: Ideally SLAPI_MODIFY_EXISTING_ENTRY should be
+ * available but it turns out that is only true if you are
+ * a dbm backend pre-op plugin - lucky dbm backend pre-op
+ * plugins.
+ * I think that is wrong since the entry is useful for filter
+ * tests and schema checks and this plugin shouldn't be limited
+ * to a single backend type, but I don't want that fight right
+ * now so we go get the entry here
+ *
+ slapi_pblock_get( pb, SLAPI_MODIFY_EXISTING_ENTRY, &e);
+ */
+ ret = slapi_search_internal_get_entry(tmp_dn, 0, &e, ipapwd_plugin_id);
+ slapi_sdn_free(&tmp_dn);
+ if (ret != LDAP_SUCCESS) {
+ slapi_log_error(SLAPI_LOG_PLUGIN, IPAPWD_PLUGIN_NAME,
+ "Failed tpo retrieve entry?!?\n");
+ rc = LDAP_NO_SUCH_OBJECT;
+ goto done;
+ }
+ }
+
+ rc = ipapwd_entry_checks(pb, e,
+ &is_root, &is_krb, &is_smb,
+ SLAPI_USERPWD_ATTR, SLAPI_ACL_WRITE);
+ if (rc) {
+ goto done;
+ }
+
+ rc = ipapwd_gen_checks(pb, &errMesg, &krbcfg, IPAPWD_CHECK_DN);
+ if (rc) {
+ goto done;
+ }
+
+ /* run through the mods again and adjust flags if operations affect them */
+ tmod = slapi_mod_new();
+ smod = slapi_mods_get_first_smod(smods, tmod);
+ while (smod) {
+ struct berval *bv;
+ const char *type;
+ int mop;
+
+ type = slapi_mod_get_type(smod);
+ if (slapi_attr_types_equivalent(type, SLAPI_USERPWD_ATTR)) {
+ mop = slapi_mod_get_operation(smod);
+ /* check op filtering out LDAP_MOD_BVALUES */
+ switch (mop & 0x0f) {
+ case LDAP_MOD_ADD:
+ /* FIXME: should we try to track cases where we would end up
+ * with multiple userPassword entries ?? */
+ case LDAP_MOD_REPLACE:
+ is_pwd_op = 1;
+ bv = slapi_mod_get_first_value(smod);
+ if (!bv) {
+ slapi_mod_free(&tmod);
+ rc = LDAP_OPERATIONS_ERROR;
+ goto done;
+ }
+ slapi_ch_free_string(&userpw);
+ userpw = slapi_ch_malloc(bv->bv_len+1);
+ if (!userpw) {
+ slapi_mod_free(&tmod);
+ rc = LDAP_OPERATIONS_ERROR;
+ goto done;
+ }
+ memcpy(userpw, bv->bv_val, bv->bv_len);
+ userpw[bv->bv_len] = '\0';
+ break;
+ case LDAP_MOD_DELETE:
+ /* reset only if we are deleting all values, or the exact
+ * same value previously set, otherwise we are just trying to
+ * add a new value and delete an existing one */
+ bv = slapi_mod_get_first_value(smod);
+ if (!bv) {
+ is_pwd_op = 0;
+ } else {
+ if (0 == strncmp(userpw, bv->bv_val, bv->bv_len) ||
+ 0 == strncmp(unhashedpw, bv->bv_val, bv->bv_len))
+ is_pwd_op = 0;
+ }
+ default:
+ break;
+ }
+ }
+
+ if (slapi_attr_types_equivalent(type, SLAPI_ATTR_OBJECTCLASS)) {
+ mop = slapi_mod_get_operation(smod);
+ /* check op filtering out LDAP_MOD_BVALUES */
+ switch (mop & 0x0f) {
+ case LDAP_MOD_REPLACE:
+ /* if objectclasses are replaced we need to start clean with
+ * flags, so we sero them out and see if they get set again */
+ is_krb = 0;
+ is_smb = 0;
+
+ case LDAP_MOD_ADD:
+ bv = slapi_mod_get_first_value(smod);
+ if (!bv) {
+ slapi_mod_free(&tmod);
+ rc = LDAP_OPERATIONS_ERROR;
+ goto done;
+ }
+ do {
+ if (0 == strncasecmp("krbPrincipalAux", bv->bv_val, bv->bv_len))
+ is_krb = 1;
+ if (0 == strncasecmp("sambaSamAccount", bv->bv_val, bv->bv_len))
+ is_smb = 1;
+ } while ((bv = slapi_mod_get_next_value(smod)) != NULL);
+
+ break;
+
+ case LDAP_MOD_DELETE:
+ /* can this happen for objectclasses ? */
+ is_krb = 0;
+ is_smb = 0;
+
+ default:
+ break;
+ }
+ }
+
+ slapi_mod_done(tmod);
+ smod = slapi_mods_get_next_smod(smods, tmod);
+ }
+ slapi_mod_free(&tmod);
+
+ /* It seem like we have determined that the end result will be deletion of
+ * the userPassword attribute, so we have no more business here */
+ if (! is_pwd_op) {
+ rc = LDAP_SUCCESS;
+ goto done;
+ }
+
+ /* Check this is a clear text password, or refuse operation (only if we need
+ * to comput other hashes */
+ if (! unhashedpw) {
+ if ('{' == userpw[0]) {
+ if (0 == strncasecmp(userpw, "{CLEAR}", strlen("{CLEAR}"))) {
+ unhashedpw = slapi_ch_strdup(&userpw[strlen("{CLEAR}")]);
+ if (NULL == unhashedpw) {
+ slapi_log_error(SLAPI_LOG_FATAL, IPAPWD_PLUGIN_NAME,
+ "Strdup failed, Out of memory\n");
+ rc = LDAP_OPERATIONS_ERROR;
+ goto done;
+ }
+ slapi_ch_free_string(&userpw);
+
+ } else if (slapi_is_encoded(userpw)) {
+
+ slapi_log_error(SLAPI_LOG_PLUGIN, IPAPWD_PLUGIN_NAME,
+ "Pre-Encoded passwords are not valid\n");
+ errMesg = "Pre-Encoded passwords are not valid\n";
+ rc = LDAP_CONSTRAINT_VIOLATION;
+ goto done;
+ }
+ }
+ }
+
+ /* time to get the operation handler */
+ ret = slapi_pblock_get(pb, SLAPI_OPERATION, &op);
+ if (ret != 0) {
+ slapi_log_error(SLAPI_LOG_FATAL, IPAPWD_PLUGIN_NAME,
+ "slapi_pblock_get failed!?\n");
+ rc = LDAP_OPERATIONS_ERROR;
+ goto done;
+ }
+
+ pwdop = slapi_get_object_extension(ipapwd_op_ext_list.object_type,
+ op, ipapwd_op_ext_list.handle);
+ if (NULL == pwdop) {
+ rc = LDAP_OPERATIONS_ERROR;
+ goto done;
+ }
+
+ pwdop->pwd_op = IPAPWD_OP_MOD;
+ pwdop->pwdata.password = slapi_ch_strdup(unhashedpw);
+ pwdop->pwdata.changetype = IPA_CHANGETYPE_NORMAL;
+
+ if (is_root) {
+ pwdop->pwdata.changetype = IPA_CHANGETYPE_DSMGR;
+ } else {
+ char *binddn;
+ Slapi_DN *bdn, *tdn;
+ int i;
+
+ /* Check Bind DN */
+ slapi_pblock_get(pb, SLAPI_CONN_DN, &binddn);
+ bdn = slapi_sdn_new_dn_byref(binddn);
+ tdn = slapi_sdn_new_dn_byref(dn);
+
+ /* if the change is performed by someone else,
+ * it is an admin change that will require a new
+ * password change immediately as per our IPA policy */
+ if (slapi_sdn_compare(bdn, tdn)) {
+ pwdop->pwdata.changetype = IPA_CHANGETYPE_ADMIN;
+
+ /* if it is a passsync manager we also need to skip resets */
+ for (i = 0; i < krbcfg->num_passsync_mgrs; i++) {
+ if (strcasecmp(krbcfg->passsync_mgrs[i], binddn) == 0) {
+ pwdop->pwdata.changetype = IPA_CHANGETYPE_DSMGR;
+ break;
+ }
+ }
+
+ }
+
+ slapi_sdn_free(&bdn);
+ slapi_sdn_free(&tdn);
+
+ }
+
+ pwdop->pwdata.dn = slapi_ch_strdup(dn);
+ pwdop->pwdata.timeNow = time(NULL);
+ pwdop->pwdata.target = e;
+
+ ret = ipapwd_CheckPolicy(&pwdop->pwdata);
+ if (ret) {
+ errMesg = "Password Fails to meet minimum strength criteria";
+ rc = LDAP_CONSTRAINT_VIOLATION;
+ goto done;
+ }
+
+ if (is_krb || is_smb) {
+
+ Slapi_Value **svals = NULL;
+ char *nt = NULL;
+ char *lm = NULL;
+
+ rc = ipapwd_preop_gen_hashes(krbcfg,
+ pwdop, unhashedpw,
+ is_krb, is_smb,
+ &svals, &nt, &lm);
+ if (rc) {
+ goto done;
+ }
+
+ if (svals) {
+ /* replace values */
+ slapi_mods_add_mod_values(smods, LDAP_MOD_REPLACE,
+ "krbPrincipalKey", svals);
+ ipapwd_free_slapi_value_array(&svals);
+ }
+
+ if (lm) {
+ /* replace value */
+ slapi_mods_add_string(smods, LDAP_MOD_REPLACE,
+ "sambaLMPassword", lm);
+ slapi_ch_free_string(&lm);
+ }
+ if (nt) {
+ /* replace value */
+ slapi_mods_add_string(smods, LDAP_MOD_REPLACE,
+ "sambaNTPassword", nt);
+ slapi_ch_free_string(&nt);
+ }
+ }
+
+ rc = LDAP_SUCCESS;
+
+done:
+ free_ipapwd_krbcfg(&krbcfg);
+ slapi_ch_free_string(&userpw); /* just to be sure */
+ slapi_ch_free_string(&unhashedpw); /* we copied it to pwdop */
+ if (e) slapi_entry_free(e); /* this is a copy in this function */
+ if (pwdop) pwdop->pwdata.target = NULL;
+
+ /* put back a, possibly modified, set of mods */
+ if (smods) {
+ mods = slapi_mods_get_ldapmods_passout(smods);
+ slapi_pblock_set(pb, SLAPI_MODIFY_MODS, mods);
+ slapi_mods_free(&smods);
+ }
+
+ if (rc != LDAP_SUCCESS) {
+ slapi_send_ldap_result(pb, rc, NULL, errMesg, 0, NULL);
+ return -1;
+ }
+
+ return 0;
+}
+
+static int ipapwd_post_op(Slapi_PBlock *pb)
+{
+ char *errMesg = "Internal operations error\n";
+ void *op;
+ struct ipapwd_operation *pwdop = NULL;
+ Slapi_Mods *smods;
+ Slapi_Value **pwvals;
+ struct tm utctime;
+ char timestr[GENERALIZED_TIME_LENGTH+1];
+ int ret;
+
+ slapi_log_error(SLAPI_LOG_TRACE, IPAPWD_PLUGIN_NAME,
+ "=> ipapwd_post_add\n");
+
+ /* time to get the operation handler */
+ ret = slapi_pblock_get(pb, SLAPI_OPERATION, &op);
+ if (ret != 0) {
+ slapi_log_error(SLAPI_LOG_FATAL, IPAPWD_PLUGIN_NAME,
+ "slapi_pblock_get failed!?\n");
+ return 0;
+ }
+
+ pwdop = slapi_get_object_extension(ipapwd_op_ext_list.object_type,
+ op, ipapwd_op_ext_list.handle);
+ if (NULL == pwdop) {
+ slapi_log_error(SLAPI_LOG_PLUGIN, IPAPWD_PLUGIN_NAME,
+ "Internal error, couldn't find pluginextension ?!\n");
+ return 0;
+ }
+
+ /* not interesting */
+ if (IPAPWD_OP_NULL == pwdop->pwd_op)
+ return 0;
+
+ if ( ! (pwdop->is_krb)) {
+ slapi_log_error(SLAPI_LOG_PLUGIN, IPAPWD_PLUGIN_NAME,
+ "Not a kerberos user, ignore krb attributes\n");
+ return 0;
+ }
+
+ /* prepare changes that can be made only as root */
+ smods = slapi_mods_new();
+
+ /* change Last Password Change field with the current date */
+ if (!gmtime_r(&(pwdop->pwdata.timeNow), &utctime)) {
+ slapi_log_error(SLAPI_LOG_PLUGIN, IPAPWD_PLUGIN_NAME,
+ "failed to parse current date (buggy gmtime_r ?)\n");
+ goto done;
+ }
+ strftime(timestr, GENERALIZED_TIME_LENGTH+1,
+ "%Y%m%d%H%M%SZ", &utctime);
+ slapi_mods_add_string(smods, LDAP_MOD_REPLACE,
+ "krbLastPwdChange", timestr);
+
+ /* set Password Expiration date */
+ if (!gmtime_r(&(pwdop->pwdata.expireTime), &utctime)) {
+ slapi_log_error(SLAPI_LOG_PLUGIN, IPAPWD_PLUGIN_NAME,
+ "failed to parse expiration date (buggy gmtime_r ?)\n");
+ goto done;
+ }
+ strftime(timestr, GENERALIZED_TIME_LENGTH+1,
+ "%Y%m%d%H%M%SZ", &utctime);
+ slapi_mods_add_string(smods, LDAP_MOD_REPLACE,
+ "krbPasswordExpiration", timestr);
+
+ /* This was a mod operation on an existing entry, make sure we also update
+ * the password history based on the entry we saved from the pre-op */
+ if (IPAPWD_OP_MOD == pwdop->pwd_op) {
+ Slapi_DN *tmp_dn = slapi_sdn_new_dn_byref(pwdop->pwdata.dn);
+ if (tmp_dn) {
+ ret = slapi_search_internal_get_entry(tmp_dn, 0,
+ &pwdop->pwdata.target,
+ ipapwd_plugin_id);
+ slapi_sdn_free(&tmp_dn);
+ if (ret != LDAP_SUCCESS) {
+ slapi_log_error(SLAPI_LOG_PLUGIN, IPAPWD_PLUGIN_NAME,
+ "Failed tpo retrieve entry?!?\n");
+ goto done;
+ }
+ }
+ pwvals = ipapwd_setPasswordHistory(smods, &pwdop->pwdata);
+ if (pwvals) {
+ slapi_mods_add_mod_values(smods, LDAP_MOD_REPLACE,
+ "passwordHistory", pwvals);
+ }
+ }
+
+ ret = ipapwd_apply_mods(pwdop->pwdata.dn, smods);
+ if (ret)
+ slapi_log_error(SLAPI_LOG_PLUGIN, IPAPWD_PLUGIN_NAME,
+ "Failed to set additional password attributes in the post-op!\n");
+
+done:
+ if (pwdop && pwdop->pwdata.target) slapi_entry_free(pwdop->pwdata.target);
+ slapi_mods_free(&smods);
+ return 0;
+}
+
+/* Copied from ipamo_string2filter()
+ *
+ * ipapwd_string2filter()
+ *
+ * For some reason slapi_str2filter writes to its input
+ * which means you cannot pass in a string constant
+ * so this is a fix up function for that
+ */
+Slapi_Filter *ipapwd_string2filter(char *strfilter)
+{
+ Slapi_Filter *ret = NULL;
+ char *idontbelieveit = slapi_ch_strdup(strfilter);
+
+ ret = slapi_str2filter(idontbelieveit);
+
+ slapi_ch_free_string(&idontbelieveit);
+
+ return ret;
+}
+
+/* Init data structs */
+static int ipapwd_start( Slapi_PBlock *pb )
+{
+ krb5_context krbctx;
+ krb5_error_code krberr;
+ char *realm = NULL;
+ char *config_dn;
+ char *partition_dn;
+ Slapi_Entry *config_entry = NULL;
+ int ret;
+
+ krberr = krb5_init_context(&krbctx);
+ if (krberr) {
+ slapi_log_error(SLAPI_LOG_FATAL, "ipapwd_start", "krb5_init_context failed\n");
+ return LDAP_OPERATIONS_ERROR;
+ }
+
+ if (slapi_pblock_get(pb, SLAPI_TARGET_DN, &config_dn) != 0) {
+ slapi_log_error( SLAPI_LOG_FATAL, "ipapwd_start", "No config DN?\n");
+ ret = LDAP_OPERATIONS_ERROR;
+ goto done;
+ }
+
+ if (ipapwd_getEntry(config_dn, &config_entry, NULL) != LDAP_SUCCESS) {
+ slapi_log_error( SLAPI_LOG_FATAL, "ipapwd_start", "No config Entry?\n");
+ ret = LDAP_OPERATIONS_ERROR;
+ goto done;
+ }
+
+ partition_dn = slapi_entry_attr_get_charptr(config_entry, "nsslapd-realmtree");
+ if (!partition_dn) {
+ slapi_log_error( SLAPI_LOG_FATAL, "ipapwd_start", "Missing partition configuration entry (nsslapd-realmTree)!\n");
+ ret = LDAP_OPERATIONS_ERROR;
+ goto done;
+ }
+
+ ret = krb5_get_default_realm(krbctx, &realm);
+ if (ret) {
+ slapi_log_error( SLAPI_LOG_FATAL, "ipapwd_start", "Failed to get default realm?!\n");
+ ret = LDAP_OPERATIONS_ERROR;
+ goto done;
+ }
+ ipa_realm_dn = slapi_ch_smprintf("cn=%s,cn=kerberos,%s", realm, partition_dn);
+ if (!ipa_realm_dn) {
+ slapi_log_error( SLAPI_LOG_FATAL, "ipapwd_start", "Out of memory ?\n");
+ ret = LDAP_OPERATIONS_ERROR;
+ goto done;
+ }
+
+ ipa_pwd_config_dn = slapi_ch_strdup(config_dn);
+ if (!ipa_pwd_config_dn) {
+ slapi_log_error( SLAPI_LOG_FATAL, "ipapwd_start", "Out of memory ?\n");
+ ret = LDAP_OPERATIONS_ERROR;
+ goto done;
+ }
+ ipa_changepw_principal_dn =
+ slapi_ch_smprintf("krbprincipalname=kadmin/changepw@%s,%s",
+ realm, ipa_realm_dn);
+ if (!ipa_changepw_principal_dn) {
+ slapi_log_error( SLAPI_LOG_FATAL, "ipapwd_start", "Out of memory ?\n");
+ ret = LDAP_OPERATIONS_ERROR;
+ goto done;
+ }
+
+ ret = LDAP_SUCCESS;
+
+done:
+ free(realm);
+ krb5_free_context(krbctx);
+ if (config_entry) slapi_entry_free(config_entry);
+ return ret;
+}
+
+
+static int ipapwd_ext_init()
+{
+ int ret;
+
+ ipapwd_op_ext_list.object_name = SLAPI_EXT_OPERATION;
+
+ ret = slapi_register_object_extension(IPAPWD_PLUGIN_NAME,
+ SLAPI_EXT_OPERATION,
+ ipapwd_op_ext_constructor,
+ ipapwd_op_ext_destructor,
+ &ipapwd_op_ext_list.object_type,
+ &ipapwd_op_ext_list.handle);
+
+ return ret;
+}
+
+
+static char *ipapwd_oid_list[] = {
+ EXOP_PASSWD_OID,
+ KEYTAB_SET_OID,
+ NULL
+};
+
+
+static char *ipapwd_name_list[] = {
+ "Password Change Extended Operation",
+ "Keytab Retrieval Extended Operation",
+ NULL
+};
+
+/* Init pre ops */
+static int ipapwd_pre_init(Slapi_PBlock *pb)
+{
+ int ret;
+
+ ret = slapi_pblock_set(pb, SLAPI_PLUGIN_VERSION, SLAPI_PLUGIN_VERSION_01);
+ if (!ret) ret = slapi_pblock_set(pb, SLAPI_PLUGIN_DESCRIPTION, (void *)&pdesc);
+ if (!ret) ret = slapi_pblock_set(pb, SLAPI_PLUGIN_PRE_ADD_FN, (void *)ipapwd_pre_add);
+ if (!ret) ret = slapi_pblock_set(pb, SLAPI_PLUGIN_PRE_MODIFY_FN, (void *)ipapwd_pre_mod);
+
+ return ret;
+}
+
+/* Init post ops */
+static int ipapwd_post_init(Slapi_PBlock *pb)
+{
+ int ret;
+
+ ret = slapi_pblock_set(pb, SLAPI_PLUGIN_VERSION, SLAPI_PLUGIN_VERSION_01);
+ if (!ret) ret = slapi_pblock_set(pb, SLAPI_PLUGIN_DESCRIPTION, (void *)&pdesc);
+ if (!ret) ret = slapi_pblock_set(pb, SLAPI_PLUGIN_POST_ADD_FN, (void *)ipapwd_post_op);
+ if (!ret) ret = slapi_pblock_set(pb, SLAPI_PLUGIN_POST_MODIFY_FN, (void *)ipapwd_post_op);
+
+ return ret;
+}
+
+/* Initialization function */
+int ipapwd_init( Slapi_PBlock *pb )
+{
+ int ret;
+
+ /* Get the arguments appended to the plugin extendedop directive. The first argument
+ * (after the standard arguments for the directive) should contain the OID of the
+ * extended operation. */
+
+ ret = slapi_pblock_get(pb, SLAPI_PLUGIN_IDENTITY, &ipapwd_plugin_id);
+ if ((ret != 0) || (NULL == ipapwd_plugin_id)) {
+ slapi_log_error(SLAPI_LOG_PLUGIN, "ipapwd_init",
+ "Could not get identity or identity was NULL\n");
+ return -1;
+ }
+
+ if (ipapwd_ext_init() != 0) {
+ slapi_log_error(SLAPI_LOG_PLUGIN, IPAPWD_PLUGIN_NAME,
+ "Object Extension Operation failed\n");
+ return -1;
+ }
+
+ /* Register the plug-in function as an extended operation
+ * plug-in function that handles the operation identified by
+ * OID 1.3.6.1.4.1.4203.1.11.1 . Also specify the version of the server
+ * plug-in */
+ ret = slapi_pblock_set(pb, SLAPI_PLUGIN_VERSION, SLAPI_PLUGIN_VERSION_01);
+ if (!ret) ret = slapi_pblock_set(pb, SLAPI_PLUGIN_START_FN, (void *)ipapwd_start);
+ if (!ret) ret = slapi_pblock_set(pb, SLAPI_PLUGIN_DESCRIPTION, (void *)&pdesc);
+ if (!ret) ret = slapi_pblock_set(pb, SLAPI_PLUGIN_EXT_OP_OIDLIST, ipapwd_oid_list);
+ if (!ret) ret = slapi_pblock_set(pb, SLAPI_PLUGIN_EXT_OP_NAMELIST, ipapwd_name_list);
+ if (!ret) slapi_pblock_set(pb, SLAPI_PLUGIN_EXT_OP_FN, (void *)ipapwd_extop);
+ if (ret) {
+ slapi_log_error( SLAPI_LOG_PLUGIN, "ipapwd_init",
+ "Failed to set plug-in version, function, and OID.\n" );
+ return -1;
+ }
+
+ slapi_register_plugin("preoperation", 1,
+ "ipapwd_pre_init", ipapwd_pre_init,
+ "IPA pwd pre ops", NULL,
+ ipapwd_plugin_id);
+
+ slapi_register_plugin("postoperation", 1,
+ "ipapwd_post_init", ipapwd_post_init,
+ "IPA pwd post ops", NULL,
+ ipapwd_plugin_id);
+
+ return 0;
+}
diff --git a/ipa-server/ipa-slapi-plugins/ipa-pwd-extop/pwd-extop-conf.ldif b/ipa-server/ipa-slapi-plugins/ipa-pwd-extop/pwd-extop-conf.ldif
new file mode 100644
index 00000000..e31a8e79
--- /dev/null
+++ b/ipa-server/ipa-slapi-plugins/ipa-pwd-extop/pwd-extop-conf.ldif
@@ -0,0 +1,16 @@
+dn: cn=ipa_pwd_extop,cn=plugins,cn=config
+changetype: add
+objectclass: top
+objectclass: nsSlapdPlugin
+objectclass: extensibleObject
+cn: ipa_pwd_extop
+nsslapd-pluginpath: libipa_pwd_extop
+nsslapd-plugininitfunc: ipapwd_init
+nsslapd-plugintype: extendedop
+nsslapd-pluginenabled: on
+nsslapd-pluginid: ipa_pwd_extop
+nsslapd-pluginversion: 1.0
+nsslapd-pluginvendor: RedHat
+nsslapd-plugindescription: Support saving passwords in multiple formats for different consumers (krb5, samba, freeradius, etc.)
+nsslapd-plugin-depends-on-type: database
+nsslapd-realmTree: $SUFFIX
diff --git a/ipa-server/ipa-slapi-plugins/ipa-winsync/Makefile.am b/ipa-server/ipa-slapi-plugins/ipa-winsync/Makefile.am
new file mode 100644
index 00000000..94bc2dc6
--- /dev/null
+++ b/ipa-server/ipa-slapi-plugins/ipa-winsync/Makefile.am
@@ -0,0 +1,43 @@
+NULL =
+
+INCLUDES = \
+ -I. \
+ -I$(srcdir) \
+ -DPREFIX=\""$(prefix)"\" \
+ -DBINDIR=\""$(bindir)"\" \
+ -DLIBDIR=\""$(libdir)"\" \
+ -DLIBEXECDIR=\""$(libexecdir)"\" \
+ -DDATADIR=\""$(datadir)"\" \
+ $(MOZLDAP_CFLAGS) \
+ $(WARN_CFLAGS) \
+ $(NULL)
+
+plugindir = $(libdir)/dirsrv/plugins
+plugin_LTLIBRARIES = \
+ libipa_winsync.la \
+ $(NULL)
+
+libipa_winsync_la_SOURCES = \
+ ipa-winsync.c \
+ ipa-winsync-config.c \
+ $(NULL)
+
+libipa_winsync_la_LDFLAGS = -avoid-version
+
+#libipa_winsync_la_LIBADD = \
+# $(MOZLDAP_LIBS) \
+# $(NULL)
+
+appdir = $(IPA_DATA_DIR)
+app_DATA = \
+ ipa-winsync-conf.ldif \
+ $(NULL)
+
+EXTRA_DIST = \
+ README \
+ $(app_DATA) \
+ $(NULL)
+
+MAINTAINERCLEANFILES = \
+ *~ \
+ Makefile.in
diff --git a/ipa-server/ipa-slapi-plugins/ipa-winsync/README b/ipa-server/ipa-slapi-plugins/ipa-winsync/README
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/ipa-server/ipa-slapi-plugins/ipa-winsync/README
diff --git a/ipa-server/ipa-slapi-plugins/ipa-winsync/ipa-winsync-conf.ldif b/ipa-server/ipa-slapi-plugins/ipa-winsync/ipa-winsync-conf.ldif
new file mode 100644
index 00000000..5b5c56ac
--- /dev/null
+++ b/ipa-server/ipa-slapi-plugins/ipa-winsync/ipa-winsync-conf.ldif
@@ -0,0 +1,27 @@
+dn: cn=ipa-winsync,cn=plugins,cn=config
+changetype: add
+objectclass: top
+objectclass: nsSlapdPlugin
+objectclass: extensibleObject
+cn: ipa-winsync
+nsslapd-pluginpath: libipa_winsync
+nsslapd-plugininitfunc: ipa_winsync_plugin_init
+nsslapd-pluginDescription: Allows IPA to work with the DS windows sync feature
+nsslapd-pluginid: ipa-winsync
+nsslapd-pluginversion: 1.0
+nsslapd-pluginvendor: Red Hat
+nsslapd-plugintype: preoperation
+nsslapd-pluginenabled: on
+nsslapd-plugin-depends-on-type: database
+ipaWinSyncRealmFilter: (objectclass=krbRealmContainer)
+ipaWinSyncRealmAttr: cn
+ipaWinSyncNewEntryFilter: (cn=ipaConfig)
+ipaWinSyncNewUserOCAttr: ipauserobjectclasses
+ipaWinSyncUserFlatten: true
+ipaWinsyncHomeDirAttr: ipaHomesRootDir
+ipaWinSyncDefaultGroupAttr: ipaDefaultPrimaryGroup
+ipaWinSyncDefaultGroupFilter: (gidNumber=*)(objectclass=posixGroup)(objectclass=groupOfNames)
+ipaWinSyncAcctDisable: both
+ipaWinSyncInactivatedFilter: (&(cn=inactivated)(objectclass=groupOfNames))
+ipaWinSyncActivatedFilter: (&(cn=activated)(objectclass=groupOfNames))
+ipaWinSyncForceSync: true
diff --git a/ipa-server/ipa-slapi-plugins/ipa-winsync/ipa-winsync-config.c b/ipa-server/ipa-slapi-plugins/ipa-winsync/ipa-winsync-config.c
new file mode 100644
index 00000000..45efa6df
--- /dev/null
+++ b/ipa-server/ipa-slapi-plugins/ipa-winsync/ipa-winsync-config.c
@@ -0,0 +1,975 @@
+/** BEGIN COPYRIGHT BLOCK
+ * This Program is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free Software
+ * Foundation; version 2 of the License.
+ *
+ * This Program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * this Program; if not, write to the Free Software Foundation, Inc., 59 Temple
+ * Place, Suite 330, Boston, MA 02111-1307 USA.
+ *
+ * In addition, as a special exception, Red Hat, Inc. gives You the additional
+ * right to link the code of this Program with code not covered under the GNU
+ * General Public License ("Non-GPL Code") and to distribute linked combinations
+ * including the two, subject to the limitations in this paragraph. Non-GPL Code
+ * permitted under this exception must only link to the code of this Program
+ * through those well defined interfaces identified in the file named EXCEPTION
+ * found in the source code files (the "Approved Interfaces"). The files of
+ * Non-GPL Code may instantiate templates or use macros or inline functions from
+ * the Approved Interfaces without causing the resulting work to be covered by
+ * the GNU General Public License. Only Red Hat, Inc. may make changes or
+ * additions to the list of Approved Interfaces. You must obey the GNU General
+ * Public License in all respects for all of the Program code and other code
+ * used in conjunction with the Program except the Non-GPL Code covered by this
+ * exception. If you modify this file, you may extend this exception to your
+ * version of the file, but you are not obligated to do so. If you do not wish
+ * to provide this exception without modification, you must delete this
+ * exception statement from your version and license this file solely under the
+ * GPL without exception.
+ *
+ * Authors:
+ * Rich Megginson <rmeggins@redhat.com>
+ *
+ * Copyright (C) 2008 Red Hat, Inc.
+ * All rights reserved.
+ * END COPYRIGHT BLOCK **/
+
+#ifdef HAVE_CONFIG_H
+# include <config.h>
+#endif
+
+/*
+ * Windows Synchronization Plug-in for IPA
+ * This plugin allows IPA to intercept operations sent from
+ * Windows to the directory server and vice versa. This allows
+ * IPA to intercept new users added to Windows and synced to the
+ * directory server, and allows IPA to modify the entry, adding
+ * objectclasses and attributes, and changing the DN.
+ */
+
+#ifdef WINSYNC_TEST_IPA
+#include <slapi-plugin.h>
+#include "winsync-plugin.h"
+#else
+#include <dirsrv/slapi-plugin.h>
+#include <dirsrv/winsync-plugin.h>
+#endif
+#include "ipa-winsync.h"
+
+#include <string.h>
+
+#define IPA_WINSYNC_CONFIG_FILTER "(objectclass=*)"
+
+/*
+ * function prototypes
+ */
+static int ipa_winsync_validate_config (Slapi_PBlock *pb, Slapi_Entry* entryBefore, Slapi_Entry* e,
+ int *returncode, char *returntext, void *arg);
+static int ipa_winsync_apply_config (Slapi_PBlock *pb, Slapi_Entry* entryBefore, Slapi_Entry* e,
+ int *returncode, char *returntext, void *arg);
+static int ipa_winsync_search (Slapi_PBlock *pb, Slapi_Entry* entryBefore, Slapi_Entry* e,
+ int *returncode, char *returntext, void *arg)
+{
+ return SLAPI_DSE_CALLBACK_OK;
+}
+
+/*
+ * static variables
+ */
+/* for now, there is only one configuration and it is global to the plugin */
+static IPA_WinSync_Config theConfig;
+static int inited = 0;
+
+static int dont_allow_that(Slapi_PBlock *pb, Slapi_Entry* entryBefore, Slapi_Entry* e,
+ int *returncode, char *returntext, void *arg)
+{
+ *returncode = LDAP_UNWILLING_TO_PERFORM;
+ return SLAPI_DSE_CALLBACK_ERROR;
+}
+
+IPA_WinSync_Config *
+ipa_winsync_get_config()
+{
+ return &theConfig;
+}
+
+/*
+ * Read configuration and create a configuration data structure.
+ * This is called after the server has configured itself so we can check
+ * schema and whatnot.
+ * Returns an LDAP error code (LDAP_SUCCESS if all goes well).
+ */
+int
+ipa_winsync_config(Slapi_Entry *config_e)
+{
+ int returncode = LDAP_SUCCESS;
+ char returntext[SLAPI_DSE_RETURNTEXT_SIZE];
+
+ if ( inited ) {
+ slapi_log_error( SLAPI_LOG_FATAL, IPA_WINSYNC_PLUGIN_NAME,
+ "Error: IPA WinSync plug-in already configured. "
+ "Please remove the plugin config entry [%s]\n",
+ slapi_entry_get_dn_const(config_e));
+ return( LDAP_PARAM_ERROR );
+ }
+
+ /* initialize fields */
+ if ((theConfig.lock = slapi_new_mutex()) == NULL) {
+ return( LDAP_LOCAL_ERROR );
+ }
+
+ /* init defaults */
+ theConfig.config_e = slapi_entry_alloc();
+ slapi_entry_init(theConfig.config_e, slapi_ch_strdup(""), NULL);
+ theConfig.flatten = PR_TRUE;
+
+ if (SLAPI_DSE_CALLBACK_OK == ipa_winsync_validate_config(NULL, NULL, config_e,
+ &returncode, returntext, NULL)) {
+ ipa_winsync_apply_config(NULL, NULL, config_e,
+ &returncode, returntext, NULL);
+ }
+
+ /* config DSE must be initialized before we get here */
+ if (returncode == LDAP_SUCCESS) {
+ const char *config_dn = slapi_entry_get_dn_const(config_e);
+ slapi_config_register_callback(SLAPI_OPERATION_MODIFY, DSE_FLAG_PREOP, config_dn, LDAP_SCOPE_BASE,
+ IPA_WINSYNC_CONFIG_FILTER, ipa_winsync_validate_config,NULL);
+ slapi_config_register_callback(SLAPI_OPERATION_MODIFY, DSE_FLAG_POSTOP, config_dn, LDAP_SCOPE_BASE,
+ IPA_WINSYNC_CONFIG_FILTER, ipa_winsync_apply_config,NULL);
+ slapi_config_register_callback(SLAPI_OPERATION_MODRDN, DSE_FLAG_PREOP, config_dn, LDAP_SCOPE_BASE,
+ IPA_WINSYNC_CONFIG_FILTER, dont_allow_that, NULL);
+ slapi_config_register_callback(SLAPI_OPERATION_DELETE, DSE_FLAG_PREOP, config_dn, LDAP_SCOPE_BASE,
+ IPA_WINSYNC_CONFIG_FILTER, dont_allow_that, NULL);
+ slapi_config_register_callback(SLAPI_OPERATION_SEARCH, DSE_FLAG_PREOP, config_dn, LDAP_SCOPE_BASE,
+ IPA_WINSYNC_CONFIG_FILTER, ipa_winsync_search,NULL);
+ }
+
+ inited = 1;
+
+ if (returncode != LDAP_SUCCESS) {
+ slapi_log_error(SLAPI_LOG_FATAL, IPA_WINSYNC_PLUGIN_NAME,
+ "Error %d: %s\n", returncode, returntext);
+ }
+
+ return returncode;
+}
+
+static int
+parse_acct_disable(const char *theval)
+{
+ int retval = ACCT_DISABLE_INVALID;
+ if (!theval || !*theval) {
+ return retval;
+ }
+ if (!PL_strcasecmp(theval, IPA_WINSYNC_ACCT_DISABLE_NONE)) {
+ retval = ACCT_DISABLE_NONE;
+ } else if (!PL_strcasecmp(theval, IPA_WINSYNC_ACCT_DISABLE_TO_AD)) {
+ retval = ACCT_DISABLE_TO_AD;
+ } else if (!PL_strcasecmp(theval, IPA_WINSYNC_ACCT_DISABLE_TO_DS)) {
+ retval = ACCT_DISABLE_TO_DS;
+ } else if (!PL_strcasecmp(theval, IPA_WINSYNC_ACCT_DISABLE_BOTH)) {
+ retval = ACCT_DISABLE_BOTH;
+ }
+
+ return retval;
+}
+
+/*
+ Validate the pending changes in the e entry.
+*/
+static int
+ipa_winsync_validate_config (Slapi_PBlock *pb, Slapi_Entry* entryBefore, Slapi_Entry* e,
+ int *returncode, char *returntext, void *arg)
+{
+ char **attrsvals = NULL;
+ int ii;
+ Slapi_Attr *testattr = NULL;
+ char *strattr = NULL;
+ int acct_disable;
+
+ *returncode = LDAP_UNWILLING_TO_PERFORM; /* be pessimistic */
+
+ /* get realm filter */
+ if (slapi_entry_attr_find(e, IPA_WINSYNC_REALM_FILTER_ATTR, &testattr) ||
+ (NULL == testattr)) {
+ PR_snprintf(returntext, SLAPI_DSE_RETURNTEXT_SIZE,
+ "Error: no value given for %s",
+ IPA_WINSYNC_REALM_FILTER_ATTR);
+ goto done2;
+ }
+
+ /* get realm attr */
+ if (slapi_entry_attr_find(e, IPA_WINSYNC_REALM_ATTR_ATTR, &testattr) ||
+ (NULL == testattr)) {
+ PR_snprintf(returntext, SLAPI_DSE_RETURNTEXT_SIZE,
+ "Error: no value given for %s",
+ IPA_WINSYNC_REALM_ATTR_ATTR);
+ goto done2;
+ }
+
+ /* get new_entry_filter */
+ if (slapi_entry_attr_find(e, IPA_WINSYNC_NEW_ENTRY_FILTER_ATTR,
+ &testattr) ||
+ (NULL == testattr)) {
+ PR_snprintf(returntext, SLAPI_DSE_RETURNTEXT_SIZE,
+ "Error: no value given for %s",
+ IPA_WINSYNC_NEW_ENTRY_FILTER_ATTR);
+ goto done2;
+ }
+
+ /* get new_user_oc_attr */
+ if (slapi_entry_attr_find(e, IPA_WINSYNC_NEW_USER_OC_ATTR,
+ &testattr) ||
+ (NULL == testattr)) {
+ PR_snprintf(returntext, SLAPI_DSE_RETURNTEXT_SIZE,
+ "Error: no value given for %s",
+ IPA_WINSYNC_NEW_USER_OC_ATTR);
+ goto done2;
+ }
+
+ /* get homedir_prefix_attr */
+ if (slapi_entry_attr_find(e, IPA_WINSYNC_HOMEDIR_PREFIX_ATTR,
+ &testattr) ||
+ (NULL == testattr)) {
+ PR_snprintf(returntext, SLAPI_DSE_RETURNTEXT_SIZE,
+ "Error: no value given for %s",
+ IPA_WINSYNC_HOMEDIR_PREFIX_ATTR);
+ goto done2;
+ }
+
+ /* get default_group_attr */
+ if (slapi_entry_attr_find(e, IPA_WINSYNC_DEFAULTGROUP_ATTR,
+ &testattr) ||
+ (NULL == testattr)) {
+ PR_snprintf(returntext, SLAPI_DSE_RETURNTEXT_SIZE,
+ "Error: no value given for %s",
+ IPA_WINSYNC_DEFAULTGROUP_ATTR);
+ goto done2;
+ }
+
+ /* get default_group_filter */
+ if (slapi_entry_attr_find(e, IPA_WINSYNC_DEFAULTGROUP_FILTER_ATTR,
+ &testattr) ||
+ (NULL == testattr)) {
+ PR_snprintf(returntext, SLAPI_DSE_RETURNTEXT_SIZE,
+ "Error: no value given for %s",
+ IPA_WINSYNC_DEFAULTGROUP_FILTER_ATTR);
+ goto done2;
+ }
+
+ /* get the list of attributes & values */
+ /* get new_user_oc_attr */
+ if (!(attrsvals = slapi_entry_attr_get_charray(
+ e, IPA_WINSYNC_NEW_USER_ATTRS_VALS))) {
+ slapi_log_error(SLAPI_LOG_PLUGIN, IPA_WINSYNC_PLUGIN_NAME,
+ "Info: no default attributes and values given in [%s]\n",
+ IPA_WINSYNC_NEW_USER_ATTRS_VALS);
+ }
+
+ /* format of *attrsvals is "attrname value" */
+ /* attrname <space> value */
+ /* value may contain spaces - attrname is everything up to the first
+ space - value is everything after the first space */
+ for (ii = 0; attrsvals && attrsvals[ii]; ++ii) {
+ Slapi_Attr *attr = NULL;
+ char *oidp = NULL;
+ char *val = strchr(attrsvals[ii], ' ');
+ if (!val || !*(val+1)) { /* incorrect format or no value */
+ PR_snprintf(returntext, SLAPI_DSE_RETURNTEXT_SIZE,
+ "Error: no value or incorrect value given for [%s] "
+ "value [%s] index [%d] - correct format is attrname SPACE value",
+ IPA_WINSYNC_NEW_USER_ATTRS_VALS,
+ attrsvals[ii], ii);
+ goto done2;
+ }
+ *val = '\0'; /* separate attr from val */
+ /* check to make sure attribute is in the schema */
+ attr = slapi_attr_new();
+ slapi_attr_set_type(attr, attrsvals[ii]);
+ slapi_attr_get_oid_copy(attr, &oidp);
+ slapi_attr_free(&attr);
+ if (oidp == NULL) { /* no such attribute */
+ PR_snprintf(returntext, SLAPI_DSE_RETURNTEXT_SIZE,
+ "Error: invalid attribute name [%s] given for [%s] "
+ "at index [%d] - attribute is not in server schema",
+ attrsvals[ii], IPA_WINSYNC_NEW_USER_ATTRS_VALS,
+ ii);
+ goto done2;
+ }
+
+ /* attribute is valid - continue */
+ slapi_ch_free_string(&oidp);
+ }
+
+ /* get account disable sync direction */
+ if (!(strattr = slapi_entry_attr_get_charptr(
+ e, IPA_WINSYNC_ACCT_DISABLE))) {
+ PR_snprintf(returntext, SLAPI_DSE_RETURNTEXT_SIZE,
+ "Error: no value given for %s",
+ IPA_WINSYNC_ACCT_DISABLE);
+ goto done2;
+ }
+
+ acct_disable = parse_acct_disable(strattr);
+ if (ACCT_DISABLE_INVALID == acct_disable) {
+ PR_snprintf(returntext, SLAPI_DSE_RETURNTEXT_SIZE,
+ "Error: invalid value [%s] given for [%s] - valid "
+ "values are " IPA_WINSYNC_ACCT_DISABLE_NONE
+ ", " IPA_WINSYNC_ACCT_DISABLE_TO_AD
+ ", " IPA_WINSYNC_ACCT_DISABLE_TO_DS
+ ", or " IPA_WINSYNC_ACCT_DISABLE_BOTH,
+ strattr, IPA_WINSYNC_ACCT_DISABLE);
+ goto done2;
+ }
+
+ /* if using acct disable sync, must have the attributes
+ IPA_WINSYNC_INACTIVATED_FILTER and IPA_WINSYNC_ACTIVATED_FILTER
+ */
+ if (acct_disable != ACCT_DISABLE_NONE) {
+ if (slapi_entry_attr_find(e, IPA_WINSYNC_INACTIVATED_FILTER,
+ &testattr) ||
+ (NULL == testattr)) {
+ PR_snprintf(returntext, SLAPI_DSE_RETURNTEXT_SIZE,
+ "Error: no value given for %s - "
+ "required for account disable sync",
+ IPA_WINSYNC_INACTIVATED_FILTER);
+ goto done2;
+ }
+ if (slapi_entry_attr_find(e, IPA_WINSYNC_ACTIVATED_FILTER,
+ &testattr) ||
+ (NULL == testattr)) {
+ PR_snprintf(returntext, SLAPI_DSE_RETURNTEXT_SIZE,
+ "Error: no value given for %s - "
+ "required for account disable sync",
+ IPA_WINSYNC_ACTIVATED_FILTER);
+ goto done2;
+ }
+ }
+
+ /* success */
+ *returncode = LDAP_SUCCESS;
+
+done2:
+ slapi_ch_free_string(&strattr);
+ slapi_ch_array_free(attrsvals);
+ attrsvals = NULL;
+
+ if (*returncode != LDAP_SUCCESS) {
+ return SLAPI_DSE_CALLBACK_ERROR;
+ } else {
+ return SLAPI_DSE_CALLBACK_OK;
+ }
+}
+
+static int
+ipa_winsync_apply_config (Slapi_PBlock *pb, Slapi_Entry* entryBefore,
+ Slapi_Entry* e, int *returncode, char *returntext,
+ void *arg)
+{
+ PRBool flatten = PR_TRUE;
+ char *realm_filter = NULL;
+ char *realm_attr = NULL;
+ char *new_entry_filter = NULL;
+ char *new_user_oc_attr = NULL; /* don't care about groups for now */
+ char *homedir_prefix_attr = NULL;
+ char *default_group_attr = NULL;
+ char *default_group_filter = NULL;
+ char *acct_disable = NULL;
+ int acct_disable_int;
+ char *inactivated_filter = NULL;
+ char *activated_filter = NULL;
+ char **attrsvals = NULL;
+ int ii;
+ Slapi_Attr *testattr = NULL;
+ PRBool forceSync = PR_FALSE;
+
+ *returncode = LDAP_UNWILLING_TO_PERFORM; /* be pessimistic */
+
+ /* get flatten value */
+ if (!slapi_entry_attr_find(e, IPA_WINSYNC_USER_FLATTEN, &testattr) &&
+ (NULL != testattr)) {
+ flatten = slapi_entry_attr_get_bool(e, IPA_WINSYNC_USER_FLATTEN);
+ }
+
+ /* get realm filter */
+ if (!(realm_filter = slapi_entry_attr_get_charptr(
+ e, IPA_WINSYNC_REALM_FILTER_ATTR))) {
+ PR_snprintf(returntext, SLAPI_DSE_RETURNTEXT_SIZE,
+ "Error: no value given for %s",
+ IPA_WINSYNC_REALM_FILTER_ATTR);
+ goto done3;
+ }
+
+ /* get realm attr */
+ if (!(realm_attr = slapi_entry_attr_get_charptr(
+ e, IPA_WINSYNC_REALM_ATTR_ATTR))) {
+ PR_snprintf(returntext, SLAPI_DSE_RETURNTEXT_SIZE,
+ "Error: no value given for %s",
+ IPA_WINSYNC_REALM_ATTR_ATTR);
+ goto done3;
+ }
+
+ /* get new_entry_filter */
+ if (!(new_entry_filter = slapi_entry_attr_get_charptr(
+ e, IPA_WINSYNC_NEW_ENTRY_FILTER_ATTR))) {
+ PR_snprintf(returntext, SLAPI_DSE_RETURNTEXT_SIZE,
+ "Error: no value given for %s",
+ IPA_WINSYNC_NEW_ENTRY_FILTER_ATTR);
+ goto done3;
+ }
+
+ /* get new_user_oc_attr */
+ if (!(new_user_oc_attr = slapi_entry_attr_get_charptr(
+ e, IPA_WINSYNC_NEW_USER_OC_ATTR))) {
+ PR_snprintf(returntext, SLAPI_DSE_RETURNTEXT_SIZE,
+ "Error: no value given for %s",
+ IPA_WINSYNC_NEW_USER_OC_ATTR);
+ goto done3;
+ }
+
+ /* get homedir_prefix_attr */
+ if (!(homedir_prefix_attr = slapi_entry_attr_get_charptr(
+ e, IPA_WINSYNC_HOMEDIR_PREFIX_ATTR))) {
+ PR_snprintf(returntext, SLAPI_DSE_RETURNTEXT_SIZE,
+ "Error: no value given for %s",
+ IPA_WINSYNC_HOMEDIR_PREFIX_ATTR);
+ goto done3;
+ }
+
+ /* get default_group_attr */
+ if (!(default_group_attr = slapi_entry_attr_get_charptr(
+ e, IPA_WINSYNC_DEFAULTGROUP_ATTR))) {
+ PR_snprintf(returntext, SLAPI_DSE_RETURNTEXT_SIZE,
+ "Error: no value given for %s",
+ IPA_WINSYNC_DEFAULTGROUP_ATTR);
+ goto done3;
+ }
+
+ /* get default_group_filter */
+ if (!(default_group_filter = slapi_entry_attr_get_charptr(
+ e, IPA_WINSYNC_DEFAULTGROUP_FILTER_ATTR))) {
+ PR_snprintf(returntext, SLAPI_DSE_RETURNTEXT_SIZE,
+ "Error: no value given for %s",
+ IPA_WINSYNC_DEFAULTGROUP_FILTER_ATTR);
+ goto done3;
+ }
+
+ /* get the list of attributes & values */
+ /* get new_user_oc_attr */
+ if (!(attrsvals = slapi_entry_attr_get_charray(
+ e, IPA_WINSYNC_NEW_USER_ATTRS_VALS))) {
+ slapi_log_error(SLAPI_LOG_PLUGIN, IPA_WINSYNC_PLUGIN_NAME,
+ "Info: no default attributes and values given in [%s]\n",
+ IPA_WINSYNC_NEW_USER_ATTRS_VALS);
+ }
+
+ /* get acct disable sync value */
+ if (!(acct_disable = slapi_entry_attr_get_charptr(
+ e, IPA_WINSYNC_ACCT_DISABLE))) {
+ PR_snprintf(returntext, SLAPI_DSE_RETURNTEXT_SIZE,
+ "Error: no value given for %s",
+ IPA_WINSYNC_ACCT_DISABLE);
+ goto done3;
+ }
+
+ acct_disable_int = parse_acct_disable(acct_disable);
+ if (ACCT_DISABLE_INVALID == acct_disable_int) {
+ PR_snprintf(returntext, SLAPI_DSE_RETURNTEXT_SIZE,
+ "Error: invalid value [%s] given for [%s] - valid "
+ "values are " IPA_WINSYNC_ACCT_DISABLE_NONE
+ ", " IPA_WINSYNC_ACCT_DISABLE_TO_AD
+ ", " IPA_WINSYNC_ACCT_DISABLE_TO_DS
+ ", or " IPA_WINSYNC_ACCT_DISABLE_BOTH,
+ acct_disable, IPA_WINSYNC_ACCT_DISABLE);
+ goto done3;
+ }
+
+ if (acct_disable_int != ACCT_DISABLE_NONE) {
+ /* get inactivated group filter */
+ if (!(inactivated_filter = slapi_entry_attr_get_charptr(
+ e, IPA_WINSYNC_INACTIVATED_FILTER))) {
+ PR_snprintf(returntext, SLAPI_DSE_RETURNTEXT_SIZE,
+ "Error: no value given for %s - required for account disable sync",
+ IPA_WINSYNC_INACTIVATED_FILTER);
+ goto done3;
+ }
+ /* get activated group filter */
+ if (!(activated_filter = slapi_entry_attr_get_charptr(
+ e, IPA_WINSYNC_ACTIVATED_FILTER))) {
+ PR_snprintf(returntext, SLAPI_DSE_RETURNTEXT_SIZE,
+ "Error: no value given for %s - required for account disable sync",
+ IPA_WINSYNC_ACTIVATED_FILTER);
+ goto done3;
+ }
+ }
+
+ /* get forceSync value */
+ if (!slapi_entry_attr_find(e, IPA_WINSYNC_FORCE_SYNC, &testattr) &&
+ (NULL != testattr)) {
+ forceSync = slapi_entry_attr_get_bool(e, IPA_WINSYNC_FORCE_SYNC);
+ }
+
+ /* if we got here, we have valid values for everything
+ set the config entry */
+ slapi_lock_mutex(theConfig.lock);
+ slapi_entry_free(theConfig.config_e);
+ theConfig.config_e = slapi_entry_alloc();
+ slapi_entry_init(theConfig.config_e, slapi_ch_strdup(""), NULL);
+
+ /* format of *attrsvals is "attrname value" */
+ /* attrname <space> value */
+ /* value may contain spaces - attrname is everything up to the first
+ space - value is everything after the first space */
+ for (ii = 0; attrsvals && attrsvals[ii]; ++ii) {
+ int rc;
+ Slapi_Value *sva[2];
+ Slapi_Value *sv = NULL;
+ char *val = strchr(attrsvals[ii], ' ');
+ if (!val || !*(val+1)) { /* incorrect format or no value */
+ PR_snprintf(returntext, SLAPI_DSE_RETURNTEXT_SIZE,
+ "Error: no value or incorrect value given for [%s] "
+ "value [%s] index [%d] - correct format is attrname SPACE value",
+ IPA_WINSYNC_NEW_USER_ATTRS_VALS,
+ attrsvals[ii], ii);
+ goto done3;
+ }
+ *val++ = '\0'; /* separate attr from val */
+ sv = slapi_value_new_string(val);
+ sva[0] = sv;
+ sva[1] = NULL;
+ if ((rc = slapi_entry_add_values_sv(theConfig.config_e,
+ attrsvals[ii], sva)) &&
+ (rc != LDAP_SUCCESS)) {
+ PR_snprintf(returntext, SLAPI_DSE_RETURNTEXT_SIZE,
+ "Error: could not add value [%s] for attribute name "
+ "[%s] - ldap error [%d: %s]", val, attrsvals[ii],
+ attrsvals[ii], IPA_WINSYNC_NEW_USER_ATTRS_VALS,
+ rc, ldap_err2string(rc));
+ slapi_entry_free(theConfig.config_e);
+ theConfig.config_e = NULL;
+ slapi_value_free(&sv);
+ goto done3;
+ }
+ slapi_value_free(&sv);
+ }
+
+ /* all of the attrs and vals have been set - set the other values */
+ slapi_ch_free_string(&theConfig.realm_filter);
+ theConfig.realm_filter = realm_filter;
+ realm_filter = NULL;
+ slapi_ch_free_string(&theConfig.realm_attr);
+ theConfig.realm_attr = realm_attr;
+ realm_attr = NULL;
+ slapi_ch_free_string(&theConfig.new_entry_filter);
+ theConfig.new_entry_filter = new_entry_filter;
+ new_entry_filter = NULL;
+ slapi_ch_free_string(&theConfig.new_user_oc_attr);
+ theConfig.new_user_oc_attr = new_user_oc_attr;
+ new_user_oc_attr = NULL;
+ slapi_ch_free_string(&theConfig.homedir_prefix_attr);
+ theConfig.homedir_prefix_attr = homedir_prefix_attr;
+ homedir_prefix_attr = NULL;
+ slapi_ch_free_string(&theConfig.default_group_attr);
+ theConfig.default_group_attr = default_group_attr;
+ default_group_attr = NULL;
+ slapi_ch_free_string(&theConfig.default_group_filter);
+ theConfig.default_group_filter = default_group_filter;
+ default_group_filter = NULL;
+ theConfig.flatten = flatten;
+ theConfig.acct_disable = parse_acct_disable(acct_disable);
+ slapi_ch_free_string(&theConfig.inactivated_filter);
+ theConfig.inactivated_filter = inactivated_filter;
+ inactivated_filter = NULL;
+ slapi_ch_free_string(&theConfig.activated_filter);
+ theConfig.activated_filter = activated_filter;
+ activated_filter = NULL;
+ theConfig.forceSync = forceSync;
+
+ /* success */
+ *returncode = LDAP_SUCCESS;
+
+done3:
+ slapi_unlock_mutex(theConfig.lock);
+
+ slapi_ch_free_string(&realm_filter);
+ slapi_ch_free_string(&realm_attr);
+ slapi_ch_free_string(&new_entry_filter);
+ slapi_ch_free_string(&new_user_oc_attr);
+ slapi_ch_free_string(&homedir_prefix_attr);
+ slapi_ch_free_string(&default_group_attr);
+ slapi_ch_free_string(&default_group_filter);
+ slapi_ch_array_free(attrsvals);
+ attrsvals = NULL;
+ slapi_ch_free_string(&acct_disable);
+ slapi_ch_free_string(&inactivated_filter);
+ slapi_ch_free_string(&activated_filter);
+
+ if (*returncode != LDAP_SUCCESS) {
+ return SLAPI_DSE_CALLBACK_ERROR;
+ } else {
+ return SLAPI_DSE_CALLBACK_OK;
+ }
+}
+
+/* create per-domain config object */
+void *
+ipa_winsync_config_new_domain(
+ const Slapi_DN *ds_subtree,
+ const Slapi_DN *ad_subtree
+)
+{
+ IPA_WinSync_Domain_Config *iwdc =
+ (IPA_WinSync_Domain_Config *)
+ slapi_ch_calloc(1, sizeof(IPA_WinSync_Domain_Config));
+
+ return (void *)iwdc;
+}
+
+/* destroy per-domain config object */
+void
+ipa_winsync_config_destroy_domain(
+ void *cbdata, const Slapi_DN *ds_subtree,
+ const Slapi_DN *ad_subtree
+)
+{
+ IPA_WinSync_Domain_Config *iwdc =
+ (IPA_WinSync_Domain_Config *)cbdata;
+ slapi_entry_free(iwdc->domain_e);
+ iwdc->domain_e = NULL;
+ slapi_ch_free_string(&iwdc->realm_name);
+ slapi_ch_free_string(&iwdc->homedir_prefix);
+ slapi_ch_free_string(&iwdc->inactivated_group_dn);
+ slapi_ch_free_string(&iwdc->activated_group_dn);
+ slapi_ch_free((void **)&iwdc);
+
+ return;
+}
+
+/*
+ return the value(s) of the given attribute in the entry that
+ matches the given criteria. The criteria must match one
+ and only one entry.
+ Returns:
+ -1 - problem doing internal search
+ LDAP_UNWILLING_TO_PERFORM - more than one matching entry
+ LDAP_NO_SUCH_OBJECT - no entry found that matched
+ 0 and attrval == NULL - entry found but no attribute
+ other ldap error - error doing search for given basedn
+*/
+static int
+internal_find_entry_get_attr_val(const Slapi_DN *basedn, int scope,
+ const char *filter, const char *attrname,
+ Slapi_ValueSet **svs, char **attrval)
+{
+ Slapi_Entry **entries = NULL;
+ Slapi_PBlock *pb = NULL;
+ const char *search_basedn = slapi_sdn_get_dn(basedn);
+ int search_scope = scope;
+ int ret = LDAP_SUCCESS;
+ const char *attrs[2] = {attrname, NULL};
+
+ if (svs) {
+ *svs = NULL;
+ }
+ if (attrval) {
+ *attrval = NULL;
+ }
+ pb = slapi_pblock_new();
+ slapi_search_internal_set_pb(pb, search_basedn, search_scope, filter,
+ (char **)attrs, 0, NULL, NULL,
+ ipa_winsync_get_plugin_identity(), 0);
+ slapi_search_internal_pb(pb);
+
+ /* This search may return no entries, but should never
+ return an error
+ */
+ slapi_pblock_get(pb, SLAPI_PLUGIN_INTOP_RESULT, &ret);
+ if (ret != LDAP_SUCCESS) {
+ slapi_log_error(SLAPI_LOG_FATAL, IPA_WINSYNC_PLUGIN_NAME,
+ "Error [%d:%s] searching for base [%s] filter [%s]"
+ " attr [%s]\n", ret, ldap_err2string(ret),
+ search_basedn, filter, attrs[0]);
+ goto out1;
+ }
+
+ slapi_pblock_get(pb, SLAPI_PLUGIN_INTOP_SEARCH_ENTRIES, &entries);
+ if (entries && entries[0] && entries[1]) {
+ /* error - should never be more than one matching entry */
+ slapi_log_error(SLAPI_LOG_FATAL, IPA_WINSYNC_PLUGIN_NAME,
+ "Error: more than one entry matches search for "
+ "base [%s] filter [%s] attr [%s]\n",
+ search_basedn, filter, attrs[0]);
+ ret = LDAP_UNWILLING_TO_PERFORM;
+ goto out1;
+ }
+
+ if (entries && entries[0]) { /* found one */
+ if (svs) {
+ Slapi_Attr *attr = NULL;
+ slapi_entry_attr_find(entries[0], attrname, &attr);
+ if (attr) {
+ /* slapi_attr_get_valueset allocates svs - must be freed later */
+ slapi_attr_get_valueset(attr, svs);
+ }
+ }
+ if (attrval) {
+ if (!strcmp(attrname, "dn")) { /* special - to just get the DN */
+ *attrval = slapi_ch_strdup(slapi_entry_get_dn_const(entries[0]));
+ } else {
+ *attrval = slapi_entry_attr_get_charptr(entries[0], attrname);
+ }
+ }
+ } else {
+ ret = LDAP_NO_SUCH_OBJECT;
+ slapi_log_error(SLAPI_LOG_PLUGIN, IPA_WINSYNC_PLUGIN_NAME,
+ "Did not find an entry for search "
+ "base [%s] filter [%s] attr [%s]\n",
+ search_basedn, filter, attrs[0]);
+ }
+
+out1:
+ if (pb) {
+ slapi_free_search_results_internal(pb);
+ slapi_pblock_destroy(pb);
+ pb = NULL;
+ }
+
+ return ret;
+}
+
+/*
+ * Perform the agreement/domain specific configuration.
+ * IPA stores its configuration in the tree. We use the
+ * ds_subtree to search for the domain/realm specific
+ * configuration entries.
+ */
+void
+ipa_winsync_config_refresh_domain(
+ void *cbdata, const Slapi_DN *ds_subtree,
+ const Slapi_DN *ad_subtree
+)
+{
+ IPA_WinSync_Domain_Config *iwdc =
+ (IPA_WinSync_Domain_Config *)cbdata;
+ Slapi_DN *config_dn = slapi_sdn_dup(ds_subtree);
+ char *realm_filter = NULL;
+ char *realm_attr = NULL;
+ char *new_entry_filter = NULL;
+ char *new_user_oc_attr = NULL; /* don't care about groups for now */
+ char *homedir_prefix_attr = NULL;
+ char *default_group_attr = NULL;
+ char *default_group_filter = NULL;
+ char *default_group_name = NULL;
+ char *real_group_filter = NULL;
+ char *default_gid = NULL;
+ Slapi_ValueSet *new_user_objclasses = NULL; /* don't care about groups for now */
+ int loopdone = 0;
+ int search_scope = LDAP_SCOPE_SUBTREE;
+ int ret = LDAP_SUCCESS;
+ Slapi_Value *sv = NULL;
+ int acct_disable;
+ char *inactivated_filter = NULL;
+ char *activated_filter = NULL;
+ char *inactivated_group_dn = NULL;
+ char *activated_group_dn = NULL;
+
+ slapi_lock_mutex(theConfig.lock);
+ realm_filter = slapi_ch_strdup(theConfig.realm_filter);
+ realm_attr = slapi_ch_strdup(theConfig.realm_attr);
+ new_entry_filter = slapi_ch_strdup(theConfig.new_entry_filter);
+ new_user_oc_attr = slapi_ch_strdup(theConfig.new_user_oc_attr);
+ homedir_prefix_attr = slapi_ch_strdup(theConfig.homedir_prefix_attr);
+ default_group_attr = slapi_ch_strdup(theConfig.default_group_attr);
+ default_group_filter = slapi_ch_strdup(theConfig.default_group_filter);
+ acct_disable = theConfig.acct_disable;
+ if (acct_disable != ACCT_DISABLE_NONE) {
+ inactivated_filter = slapi_ch_strdup(theConfig.inactivated_filter);
+ activated_filter = slapi_ch_strdup(theConfig.activated_filter);
+ }
+ slapi_unlock_mutex(theConfig.lock);
+
+ /* starting at ds_subtree, search for the entry
+ containing the Kerberos realm to use */
+ slapi_ch_free_string(&iwdc->realm_name);
+ while(!loopdone && !slapi_sdn_isempty(config_dn)) {
+ ret = internal_find_entry_get_attr_val(config_dn, search_scope,
+ realm_filter, realm_attr,
+ NULL, &iwdc->realm_name);
+
+ if ((0 == ret) && iwdc->realm_name) {
+ loopdone = 1;
+ } else if ((LDAP_NO_SUCH_OBJECT == ret) && !iwdc->realm_name) {
+ /* try again */
+ Slapi_DN *parent_dn = slapi_sdn_new();
+ slapi_sdn_get_parent(config_dn, parent_dn);
+ slapi_sdn_free(&config_dn);
+ config_dn = parent_dn;
+ } else { /* error */
+ goto out;
+ }
+ }
+
+ if (!iwdc->realm_name) {
+ /* error - could not find the IPA config entry with the realm name */
+ slapi_log_error(SLAPI_LOG_FATAL, IPA_WINSYNC_PLUGIN_NAME,
+ "Error: could not find the entry containing the realm name for "
+ "ds subtree [%s] filter [%s] attr [%s]\n",
+ slapi_sdn_get_dn(ds_subtree), realm_filter, realm_attr);
+ goto out;
+ }
+
+ /* look for the entry containing the default objectclasses
+ to add to new entries */
+ ret = internal_find_entry_get_attr_val(config_dn, search_scope,
+ new_entry_filter, new_user_oc_attr,
+ &new_user_objclasses, NULL);
+ if (!new_user_objclasses) {
+ /* error - could not find the entry containing list of objectclasses */
+ slapi_log_error(SLAPI_LOG_FATAL, IPA_WINSYNC_PLUGIN_NAME,
+ "Error: could not find the entry containing the new user objectclass list for "
+ "ds subtree [%s] filter [%s] attr [%s]\n",
+ slapi_sdn_get_dn(ds_subtree), new_entry_filter, new_user_oc_attr);
+ goto out;
+ }
+
+ /* get the home directory prefix value */
+ /* note - this is in the same entry as the new entry template, so
+ use the same filter */
+ slapi_ch_free_string(&iwdc->homedir_prefix);
+ ret = internal_find_entry_get_attr_val(config_dn, search_scope,
+ new_entry_filter, homedir_prefix_attr,
+ NULL, &iwdc->homedir_prefix);
+ if (!iwdc->homedir_prefix) {
+ /* error - could not find the home dir prefix */
+ slapi_log_error(SLAPI_LOG_FATAL, IPA_WINSYNC_PLUGIN_NAME,
+ "Error: could not find the entry containing the home directory prefix for "
+ "ds subtree [%s] filter [%s] attr [%s]\n",
+ slapi_sdn_get_dn(ds_subtree), new_entry_filter, homedir_prefix_attr);
+ goto out;
+ }
+
+ /* find the default group - the entry above contains the group name, but
+ we need the gidNumber for posixAccount - so first find the entry
+ and attr value which has the group name, then lookup the group
+ number from the group name */
+ ret = internal_find_entry_get_attr_val(config_dn, search_scope,
+ new_entry_filter, default_group_attr,
+ NULL, &default_group_name);
+ if (!default_group_name) {
+ /* error - could not find the default group name */
+ slapi_log_error(SLAPI_LOG_FATAL, IPA_WINSYNC_PLUGIN_NAME,
+ "Error: could not find the entry containing the default group name for "
+ "ds subtree [%s] filter [%s] attr [%s]\n",
+ slapi_sdn_get_dn(ds_subtree), new_entry_filter, default_group_attr);
+ goto out;
+ }
+
+ /* next, find the group whose name is default_group_name - construct the filter
+ based on the filter attribute value - assumes the group name is stored
+ in the cn attribute value, and the gidNumber in the gidNumber attribute value */
+ real_group_filter = slapi_ch_smprintf("(&(cn=%s)%s)", default_group_name,
+ default_group_filter);
+ ret = internal_find_entry_get_attr_val(config_dn, search_scope,
+ real_group_filter, "gidNumber",
+ NULL, &default_gid);
+ if (!default_gid) {
+ /* error - could not find the default gidNumber */
+ slapi_log_error(SLAPI_LOG_FATAL, IPA_WINSYNC_PLUGIN_NAME,
+ "Error: could not find the entry containing the default gidNumber "
+ "ds subtree [%s] filter [%s] attr [%s]\n",
+ slapi_sdn_get_dn(ds_subtree), new_entry_filter, "gidNumber");
+ goto out;
+ }
+
+ /* If we are syncing account disable, we need to find the groups used
+ to denote active and inactive users e.g.
+ dn: cn=inactivated,cn=account inactivation,cn=accounts,$SUFFIX
+
+ dn: cn=Activated,cn=Account Inactivation,cn=accounts,$SUFFIX
+
+ */
+ if (acct_disable != ACCT_DISABLE_NONE) {
+ ret = internal_find_entry_get_attr_val(config_dn, search_scope,
+ inactivated_filter, "dn",
+ NULL, &inactivated_group_dn);
+ if (!inactivated_group_dn) {
+ /* error - could not find the inactivated group dn */
+ slapi_log_error(SLAPI_LOG_FATAL, IPA_WINSYNC_PLUGIN_NAME,
+ "Error: could not find the DN of the inactivated users group "
+ "ds subtree [%s] filter [%s]\n",
+ slapi_sdn_get_dn(ds_subtree), inactivated_filter);
+ goto out;
+ }
+ ret = internal_find_entry_get_attr_val(config_dn, search_scope,
+ activated_filter, "dn",
+ NULL, &activated_group_dn);
+ if (!activated_group_dn) {
+ /* error - could not find the activated group dn */
+ slapi_log_error(SLAPI_LOG_FATAL, IPA_WINSYNC_PLUGIN_NAME,
+ "Error: could not find the DN of the activated users group "
+ "ds subtree [%s] filter [%s]\n",
+ slapi_sdn_get_dn(ds_subtree), activated_filter);
+ goto out;
+ }
+ }
+
+ /* ok, we have our values */
+ /* first, clear out the old domain config */
+ slapi_entry_free(iwdc->domain_e);
+ iwdc->domain_e = NULL;
+
+ /* next, copy the global attr config */
+ slapi_lock_mutex(theConfig.lock);
+ iwdc->domain_e = slapi_entry_dup(theConfig.config_e);
+ slapi_unlock_mutex(theConfig.lock);
+
+ /* set the objectclasses in the domain_e */
+ slapi_entry_attr_delete(iwdc->domain_e, "objectclass");
+ /* this copies new_user_objclasses */
+ slapi_entry_add_valueset(iwdc->domain_e, "objectclass", new_user_objclasses);
+
+ /* set the default gid number */
+ sv = slapi_value_new_string_passin(default_gid);
+ default_gid = NULL; /* passin owns the memory */
+ if (!slapi_entry_attr_has_syntax_value(iwdc->domain_e, "gidNumber", sv)) {
+ slapi_entry_add_value(iwdc->domain_e, "gidNumber", sv);
+ }
+ slapi_value_free(&sv);
+
+ slapi_ch_free_string(&iwdc->inactivated_group_dn);
+ iwdc->inactivated_group_dn = inactivated_group_dn;
+ inactivated_group_dn = NULL;
+ slapi_ch_free_string(&iwdc->activated_group_dn);
+ iwdc->activated_group_dn = activated_group_dn;
+ activated_group_dn = NULL;
+
+out:
+ slapi_valueset_free(new_user_objclasses);
+ slapi_sdn_free(&config_dn);
+ slapi_ch_free_string(&realm_filter);
+ slapi_ch_free_string(&realm_attr);
+ slapi_ch_free_string(&new_entry_filter);
+ slapi_ch_free_string(&new_user_oc_attr);
+ slapi_ch_free_string(&homedir_prefix_attr);
+ slapi_ch_free_string(&default_group_attr);
+ slapi_ch_free_string(&default_group_filter);
+ slapi_ch_free_string(&default_group_name);
+ slapi_ch_free_string(&real_group_filter);
+ slapi_ch_free_string(&default_gid);
+ slapi_ch_free_string(&inactivated_filter);
+ slapi_ch_free_string(&inactivated_group_dn);
+ slapi_ch_free_string(&activated_filter);
+ slapi_ch_free_string(&activated_group_dn);
+
+ if (LDAP_SUCCESS != ret) {
+ slapi_ch_free_string(&iwdc->realm_name);
+ slapi_ch_free_string(&iwdc->homedir_prefix);
+ slapi_entry_free(iwdc->domain_e);
+ iwdc->domain_e = NULL;
+ }
+
+ return;
+}
diff --git a/ipa-server/ipa-slapi-plugins/ipa-winsync/ipa-winsync.c b/ipa-server/ipa-slapi-plugins/ipa-winsync/ipa-winsync.c
new file mode 100644
index 00000000..9ee8805b
--- /dev/null
+++ b/ipa-server/ipa-slapi-plugins/ipa-winsync/ipa-winsync.c
@@ -0,0 +1,1177 @@
+/** BEGIN COPYRIGHT BLOCK
+ * This Program is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free Software
+ * Foundation; version 2 of the License.
+ *
+ * This Program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * this Program; if not, write to the Free Software Foundation, Inc., 59 Temple
+ * Place, Suite 330, Boston, MA 02111-1307 USA.
+ *
+ * In addition, as a special exception, Red Hat, Inc. gives You the additional
+ * right to link the code of this Program with code not covered under the GNU
+ * General Public License ("Non-GPL Code") and to distribute linked combinations
+ * including the two, subject to the limitations in this paragraph. Non-GPL Code
+ * permitted under this exception must only link to the code of this Program
+ * through those well defined interfaces identified in the file named EXCEPTION
+ * found in the source code files (the "Approved Interfaces"). The files of
+ * Non-GPL Code may instantiate templates or use macros or inline functions from
+ * the Approved Interfaces without causing the resulting work to be covered by
+ * the GNU General Public License. Only Red Hat, Inc. may make changes or
+ * additions to the list of Approved Interfaces. You must obey the GNU General
+ * Public License in all respects for all of the Program code and other code
+ * used in conjunction with the Program except the Non-GPL Code covered by this
+ * exception. If you modify this file, you may extend this exception to your
+ * version of the file, but you are not obligated to do so. If you do not wish
+ * to provide this exception without modification, you must delete this
+ * exception statement from your version and license this file solely under the
+ * GPL without exception.
+ *
+ * Authors:
+ * Rich Megginson <rmeggins@redhat.com>
+ *
+ * Copyright (C) 2008 Red Hat, Inc.
+ * All rights reserved.
+ * END COPYRIGHT BLOCK **/
+
+#ifdef HAVE_CONFIG_H
+# include <config.h>
+#endif
+
+/*
+ * Windows Synchronization Plug-in for IPA
+ * This plugin allows IPA to intercept operations sent from
+ * Windows to the directory server and vice versa. This allows
+ * IPA to intercept new users added to Windows and synced to the
+ * directory server, and allows IPA to modify the entry, adding
+ * objectclasses and attributes, and changing the DN.
+ */
+
+#ifdef WINSYNC_TEST_IPA
+#include <slapi-plugin.h>
+#include "winsync-plugin.h"
+#else
+#include <dirsrv/slapi-plugin.h>
+#include <dirsrv/winsync-plugin.h>
+#endif
+#include "ipa-winsync.h"
+
+static char *ipa_winsync_plugin_name = IPA_WINSYNC_PLUGIN_NAME;
+
+static void
+sync_acct_disable(
+ void *cbdata, /* the usual domain config data */
+ const Slapi_Entry *ad_entry, /* the AD entry */
+ Slapi_Entry *ds_entry, /* the DS entry */
+ int direction, /* the direction - TO_AD or TO_DS */
+ Slapi_Entry *update_entry, /* the entry to update for ADDs */
+ Slapi_Mods *smods, /* the mod list for MODIFYs */
+ int *do_modify /* set to true if mods were applied */
+);
+
+static void
+do_force_sync(
+ const Slapi_Entry *ad_entry, /* the AD entry */
+ Slapi_Entry *ds_entry, /* the DS entry */
+ Slapi_Mods *smods, /* the mod list */
+ int *do_modify /* set to true if mods were applied */
+);
+
+/* This is called when a new agreement is created or loaded
+ at startup.
+*/
+static void *
+ipa_winsync_agmt_init(const Slapi_DN *ds_subtree, const Slapi_DN *ad_subtree)
+{
+ void *cbdata = NULL;
+ slapi_log_error(SLAPI_LOG_PLUGIN, ipa_winsync_plugin_name,
+ "--> ipa_winsync_agmt_init [%s] [%s] -- begin\n",
+ slapi_sdn_get_dn(ds_subtree),
+ slapi_sdn_get_dn(ad_subtree));
+
+ /* do the domain specific configuration based on the ds subtree */
+ cbdata = ipa_winsync_config_new_domain(ds_subtree, ad_subtree);
+
+ slapi_log_error(SLAPI_LOG_PLUGIN, ipa_winsync_plugin_name,
+ "<-- ipa_winsync_agmt_init -- end\n");
+
+ return cbdata;
+}
+
+static void
+ipa_winsync_dirsync_search_params_cb(void *cbdata, const char *agmt_dn,
+ char **base, int *scope, char **filter,
+ char ***attrs, LDAPControl ***serverctrls)
+{
+ slapi_log_error(SLAPI_LOG_PLUGIN, ipa_winsync_plugin_name,
+ "--> ipa_winsync_dirsync_search_params_cb -- begin\n");
+
+ slapi_log_error(SLAPI_LOG_PLUGIN, ipa_winsync_plugin_name,
+ "<-- ipa_winsync_dirsync_search_params_cb -- end\n");
+
+ return;
+}
+
+/* called before searching for a single entry from AD - agmt_dn will be NULL */
+static void
+ipa_winsync_pre_ad_search_cb(void *cbdata, const char *agmt_dn,
+ char **base, int *scope, char **filter,
+ char ***attrs, LDAPControl ***serverctrls)
+{
+ slapi_log_error(SLAPI_LOG_PLUGIN, ipa_winsync_plugin_name,
+ "--> ipa_winsync_pre_ad_search_cb -- begin\n");
+
+ slapi_log_error(SLAPI_LOG_PLUGIN, ipa_winsync_plugin_name,
+ "<-- ipa_winsync_pre_ad_search_cb -- end\n");
+
+ return;
+}
+
+/* called before an internal search to get a single DS entry - agmt_dn will be NULL */
+static void
+ipa_winsync_pre_ds_search_entry_cb(void *cbdata, const char *agmt_dn,
+ char **base, int *scope, char **filter,
+ char ***attrs, LDAPControl ***serverctrls)
+{
+ slapi_log_error(SLAPI_LOG_PLUGIN, ipa_winsync_plugin_name,
+ "--> ipa_winsync_pre_ds_search_cb -- begin\n");
+
+ slapi_log_error(SLAPI_LOG_PLUGIN, ipa_winsync_plugin_name,
+ "-- ipa_winsync_pre_ds_search_cb - base [%s] "
+ "scope [%d] filter [%s]\n",
+ *base, *scope, *filter);
+
+ slapi_log_error(SLAPI_LOG_PLUGIN, ipa_winsync_plugin_name,
+ "<-- ipa_winsync_pre_ds_search_cb -- end\n");
+
+ return;
+}
+
+/* called before the total update to get all entries from the DS to sync to AD */
+static void
+ipa_winsync_pre_ds_search_all_cb(void *cbdata, const char *agmt_dn,
+ char **base, int *scope, char **filter,
+ char ***attrs, LDAPControl ***serverctrls)
+{
+ slapi_log_error(SLAPI_LOG_PLUGIN, ipa_winsync_plugin_name,
+ "--> ipa_winsync_pre_ds_search_all_cb -- orig filter [%s] -- begin\n",
+ ((filter && *filter) ? *filter : "NULL"));
+
+ /* We only want to grab users from the ds side - no groups */
+ slapi_ch_free_string(filter);
+ /* maybe use ntUniqueId=* - only get users that have already been
+ synced with AD - ntUniqueId and ntUserDomainId are
+ indexed for equality only - need to add presence? */
+ *filter = slapi_ch_strdup("(&(objectclass=ntuser)(ntUserDomainId=*))");
+
+ slapi_log_error(SLAPI_LOG_PLUGIN, ipa_winsync_plugin_name,
+ "<-- ipa_winsync_pre_ds_search_all_cb -- end\n");
+
+ return;
+}
+
+static void
+ipa_winsync_pre_ad_mod_user_cb(void *cbdata, const Slapi_Entry *rawentry,
+ Slapi_Entry *ad_entry, Slapi_Entry *ds_entry,
+ Slapi_Mods *smods, int *do_modify)
+{
+ slapi_log_error(SLAPI_LOG_PLUGIN, ipa_winsync_plugin_name,
+ "--> ipa_winsync_pre_ad_mod_user_cb -- begin\n");
+
+ sync_acct_disable(cbdata, rawentry, ds_entry, ACCT_DISABLE_TO_AD,
+ NULL, smods, do_modify);
+
+ slapi_log_error(SLAPI_LOG_PLUGIN, ipa_winsync_plugin_name,
+ "<-- ipa_winsync_pre_ad_mod_user_cb -- end\n");
+
+ return;
+}
+
+static void
+ipa_winsync_pre_ad_mod_group_cb(void *cbdata, const Slapi_Entry *rawentry,
+ Slapi_Entry *ad_entry, Slapi_Entry *ds_entry,
+ Slapi_Mods *smods, int *do_modify)
+{
+ slapi_log_error(SLAPI_LOG_PLUGIN, ipa_winsync_plugin_name,
+ "--> ipa_winsync_pre_ad_mod_group_cb -- begin\n");
+
+ slapi_log_error(SLAPI_LOG_PLUGIN, ipa_winsync_plugin_name,
+ "<-- ipa_winsync_pre_ad_mod_group_cb -- end\n");
+
+ return;
+}
+
+static void
+ipa_winsync_pre_ds_mod_user_cb(void *cbdata, const Slapi_Entry *rawentry,
+ Slapi_Entry *ad_entry, Slapi_Entry *ds_entry,
+ Slapi_Mods *smods, int *do_modify)
+{
+ slapi_log_error(SLAPI_LOG_PLUGIN, ipa_winsync_plugin_name,
+ "--> ipa_winsync_pre_ds_mod_user_cb -- begin\n");
+
+ sync_acct_disable(cbdata, rawentry, ds_entry, ACCT_DISABLE_TO_DS,
+ NULL, smods, do_modify);
+
+ do_force_sync(rawentry, ds_entry, smods, do_modify);
+
+ slapi_log_error(SLAPI_LOG_PLUGIN, ipa_winsync_plugin_name,
+ "<-- ipa_winsync_pre_ds_mod_user_cb -- end\n");
+
+ return;
+}
+
+static void
+ipa_winsync_pre_ds_mod_group_cb(void *cbdata, const Slapi_Entry *rawentry,
+ Slapi_Entry *ad_entry, Slapi_Entry *ds_entry,
+ Slapi_Mods *smods, int *do_modify)
+{
+ slapi_log_error(SLAPI_LOG_PLUGIN, ipa_winsync_plugin_name,
+ "--> ipa_winsync_pre_ds_mod_group_cb -- begin\n");
+
+ slapi_log_error(SLAPI_LOG_PLUGIN, ipa_winsync_plugin_name,
+ "<-- ipa_winsync_pre_ds_mod_group_cb -- end\n");
+
+ return;
+}
+
+static void
+ipa_winsync_pre_ds_add_user_cb(void *cbdata, const Slapi_Entry *rawentry,
+ Slapi_Entry *ad_entry, Slapi_Entry *ds_entry)
+{
+ IPA_WinSync_Domain_Config *ipaconfig = (IPA_WinSync_Domain_Config *)cbdata;
+ Slapi_Attr *attr = NULL;
+ Slapi_Attr *e_attr = NULL;
+ char *type = NULL;
+ IPA_WinSync_Config *global_ipaconfig = ipa_winsync_get_config();
+
+ slapi_log_error(SLAPI_LOG_PLUGIN, ipa_winsync_plugin_name,
+ "--> ipa_winsync_pre_ds_add_user_cb -- begin\n");
+
+ if (!ipaconfig || !ipaconfig->domain_e || !ipaconfig->realm_name ||
+ !ipaconfig->homedir_prefix) {
+ slapi_log_error(SLAPI_LOG_FATAL, ipa_winsync_plugin_name,
+ "Error: configuration failure: cannot map Windows "
+ "entry dn [%s], DS entry dn [%s]\n",
+ slapi_entry_get_dn_const(ad_entry),
+ slapi_entry_get_dn_const(ds_entry));
+ return;
+ }
+
+ /* add the objectclasses and attributes to the entry */
+ for (slapi_entry_first_attr(ipaconfig->domain_e, &attr); attr;
+ slapi_entry_next_attr(ipaconfig->domain_e, attr, &attr))
+ {
+ slapi_attr_get_type(attr, &type);
+ if (!type) {
+ continue; /* should never happen */
+ }
+
+ if (!slapi_entry_attr_find(ds_entry, type, &e_attr) && e_attr) {
+ /* already has attribute - add missing values */
+ Slapi_Value *sv = NULL;
+ int ii = 0;
+ for (ii = slapi_attr_first_value(attr, &sv); ii != -1;
+ ii = slapi_attr_next_value(attr, ii, &sv))
+ {
+ if (!slapi_entry_attr_has_syntax_value(ds_entry, type, sv)) {
+ /* attr-value sv not found in ds_entry; add it */
+ slapi_log_error(SLAPI_LOG_PLUGIN, ipa_winsync_plugin_name,
+ "--> ipa_winsync_pre_ds_add_user_cb -- "
+ "adding val for [%s] to new entry [%s]\n",
+ type, slapi_entry_get_dn_const(ds_entry));
+
+ slapi_entry_add_value(ds_entry, type, sv);
+ }
+ }
+ } else { /* attr not found */
+ Slapi_ValueSet *svs = NULL;
+ slapi_attr_get_valueset(attr, &svs); /* makes a copy */
+ slapi_entry_add_valueset(ds_entry, type, svs);
+ slapi_valueset_free(svs); /* free the copy */
+ }
+ }
+
+ /* add other attributes */
+ type = "krbPrincipalName";
+ if (slapi_entry_attr_find(ds_entry, type, &e_attr) || !e_attr) {
+ char *upn = NULL;
+ char *uid = NULL;
+ char *samAccountName = NULL;
+ /* if the ds_entry already has a uid, use that */
+ if ((uid = slapi_entry_attr_get_charptr(ds_entry, "uid"))) {
+ upn = slapi_ch_smprintf("%s@%s", uid, ipaconfig->realm_name);
+ slapi_ch_free_string(&uid);
+ /* otherwise, use the samAccountName from the ad_entry */
+ } else if ((samAccountName =
+ slapi_entry_attr_get_charptr(ad_entry, "samAccountName"))) {
+ upn = slapi_ch_smprintf("%s@%s", samAccountName, ipaconfig->realm_name);
+ slapi_ch_free_string(&samAccountName);
+ } else { /* fatal error - nothing to use for krbPrincipalName */
+ slapi_log_error(SLAPI_LOG_FATAL, ipa_winsync_plugin_name,
+ "Error creating %s for realm [%s] for Windows "
+ "entry dn [%s], DS entry dn [%s] - Windows entry "
+ "has no samAccountName, and DS entry has no uid.\n",
+ type, ipaconfig->realm_name,
+ slapi_entry_get_dn_const(ad_entry),
+ slapi_entry_get_dn_const(ds_entry));
+ }
+
+ if (upn) {
+ slapi_entry_attr_set_charptr(ds_entry, type, upn);
+ slapi_ch_free_string(&upn);
+ }
+ }
+
+ type = "homeDirectory";
+ if (slapi_entry_attr_find(ds_entry, type, &e_attr) || !e_attr) {
+ char *homeDir = NULL;
+ char *uid = NULL;
+ char *samAccountName = NULL;
+ /* if the ds_entry already has a uid, use that */
+ if ((uid = slapi_entry_attr_get_charptr(ds_entry, "uid"))) {
+ homeDir = slapi_ch_smprintf("%s/%s", ipaconfig->homedir_prefix, uid);
+ slapi_ch_free_string(&uid);
+ /* otherwise, use the samAccountName from the ad_entry */
+ } else if ((samAccountName =
+ slapi_entry_attr_get_charptr(ad_entry, "samAccountName"))) {
+ homeDir = slapi_ch_smprintf("%s/%s", ipaconfig->homedir_prefix,
+ samAccountName);
+ slapi_ch_free_string(&samAccountName);
+ } else { /* fatal error - nothing to use for homeDirectory */
+ slapi_log_error(SLAPI_LOG_FATAL, ipa_winsync_plugin_name,
+ "Error creating %s for realm [%s] for Windows "
+ "entry dn [%s], DS entry dn [%s] - Windows entry "
+ "has no samAccountName, and DS entry has no uid.\n",
+ type, ipaconfig->realm_name,
+ slapi_entry_get_dn_const(ad_entry),
+ slapi_entry_get_dn_const(ds_entry));
+ }
+
+ if (homeDir) {
+ slapi_entry_attr_set_charptr(ds_entry, type, homeDir);
+ slapi_ch_free_string(&homeDir);
+ }
+ }
+
+ /* gecos is not required, but nice to have */
+ type = "gecos";
+ if (slapi_entry_attr_find(ds_entry, type, &e_attr) || !e_attr) {
+ char *cn = NULL;
+ char *displayName = NULL;
+ /* if the ds_entry already has a cn, use that */
+ if ((cn = slapi_entry_attr_get_charptr(ds_entry, "cn"))) {
+ slapi_entry_attr_set_charptr(ds_entry, type, cn);
+ slapi_ch_free_string(&cn);
+ /* otherwise, use the displayName from the ad_entry */
+ } else if ((displayName =
+ slapi_entry_attr_get_charptr(ad_entry, "displayName"))) {
+ slapi_entry_attr_set_charptr(ds_entry, type, displayName);
+ slapi_ch_free_string(&displayName);
+ }
+ }
+
+ sync_acct_disable(cbdata, rawentry, ds_entry, ACCT_DISABLE_TO_DS,
+ ds_entry, NULL, NULL);
+ slapi_log_error(SLAPI_LOG_PLUGIN, ipa_winsync_plugin_name,
+ "<-- ipa_winsync_pre_ds_add_user_cb -- end\n");
+
+ return;
+}
+
+static void
+ipa_winsync_pre_ds_add_group_cb(void *cbdata, const Slapi_Entry *rawentry,
+ Slapi_Entry *ad_entry, Slapi_Entry *ds_entry)
+{
+ slapi_log_error(SLAPI_LOG_PLUGIN, ipa_winsync_plugin_name,
+ "--> ipa_winsync_pre_ds_add_group_cb -- begin\n");
+
+ slapi_log_error(SLAPI_LOG_PLUGIN, ipa_winsync_plugin_name,
+ "<-- ipa_winsync_pre_ds_add_group_cb -- end\n");
+
+ return;
+}
+
+static void
+ipa_winsync_get_new_ds_user_dn_cb(void *cbdata, const Slapi_Entry *rawentry,
+ Slapi_Entry *ad_entry, char **new_dn_string,
+ const Slapi_DN *ds_suffix, const Slapi_DN *ad_suffix)
+{
+ char **rdns = NULL;
+ PRBool flatten = PR_TRUE;
+ IPA_WinSync_Config *ipaconfig = ipa_winsync_get_config();
+
+ slapi_log_error(SLAPI_LOG_PLUGIN, ipa_winsync_plugin_name,
+ "--> ipa_winsync_get_new_ds_user_dn_cb -- old dn [%s] -- begin\n",
+ *new_dn_string);
+
+ slapi_lock_mutex(ipaconfig->lock);
+ flatten = ipaconfig->flatten;
+ slapi_unlock_mutex(ipaconfig->lock);
+
+ if (!flatten) {
+ return;
+ }
+
+ rdns = ldap_explode_dn(*new_dn_string, 0);
+ if (!rdns || !rdns[0]) {
+ ldap_value_free(rdns);
+ return;
+ }
+
+ slapi_ch_free_string(new_dn_string);
+ *new_dn_string = slapi_ch_smprintf("%s,%s", rdns[0], slapi_sdn_get_dn(ds_suffix));
+ ldap_value_free(rdns);
+
+ slapi_log_error(SLAPI_LOG_PLUGIN, ipa_winsync_plugin_name,
+ "<-- ipa_winsync_get_new_ds_user_dn_cb -- new dn [%s] -- end\n",
+ *new_dn_string);
+
+ return;
+}
+
+static void
+ipa_winsync_get_new_ds_group_dn_cb(void *cbdata, const Slapi_Entry *rawentry,
+ Slapi_Entry *ad_entry, char **new_dn_string,
+ const Slapi_DN *ds_suffix, const Slapi_DN *ad_suffix)
+{
+ slapi_log_error(SLAPI_LOG_PLUGIN, ipa_winsync_plugin_name,
+ "--> ipa_winsync_get_new_ds_group_dn_cb -- begin\n");
+
+ slapi_log_error(SLAPI_LOG_PLUGIN, ipa_winsync_plugin_name,
+ "<-- ipa_winsync_get_new_ds_group_dn_cb -- end\n");
+
+ return;
+}
+
+static void
+ipa_winsync_pre_ad_mod_user_mods_cb(void *cbdata, const Slapi_Entry *rawentry,
+ const Slapi_DN *local_dn,
+ const Slapi_Entry *ds_entry,
+ LDAPMod * const *origmods,
+ Slapi_DN *remote_dn, LDAPMod ***modstosend)
+{
+ Slapi_Mods *smods;
+
+ slapi_log_error(SLAPI_LOG_PLUGIN, ipa_winsync_plugin_name,
+ "--> ipa_winsync_pre_ad_mod_user_mods_cb -- begin\n");
+
+ /* wrap the modstosend in a Slapi_Mods for convenience */
+ smods = slapi_mods_new();
+ slapi_mods_init_byref(smods, *modstosend);
+ sync_acct_disable(cbdata, rawentry, (Slapi_Entry *)ds_entry,
+ ACCT_DISABLE_TO_AD, NULL, smods, NULL);
+
+ /* convert back to LDAPMod ** and clean up */
+ *modstosend = slapi_mods_get_ldapmods_passout(smods);
+ slapi_mods_free(&smods);
+ slapi_log_error(SLAPI_LOG_PLUGIN, ipa_winsync_plugin_name,
+ "<-- ipa_winsync_pre_ad_mod_user_mods_cb -- end\n");
+
+ return;
+}
+
+static void
+ipa_winsync_pre_ad_mod_group_mods_cb(void *cbdata, const Slapi_Entry *rawentry,
+ const Slapi_DN *local_dn,
+ const Slapi_Entry *ds_entry,
+ LDAPMod * const *origmods,
+ Slapi_DN *remote_dn, LDAPMod ***modstosend)
+{
+ slapi_log_error(SLAPI_LOG_PLUGIN, ipa_winsync_plugin_name,
+ "--> ipa_winsync_pre_ad_mod_group_mods_cb -- begin\n");
+
+ slapi_log_error(SLAPI_LOG_PLUGIN, ipa_winsync_plugin_name,
+ "<-- ipa_winsync_pre_ad_mod_group_mods_cb -- end\n");
+
+ return;
+}
+
+static int
+ipa_winsync_can_add_entry_to_ad_cb(void *cbdata, const Slapi_Entry *local_entry,
+ const Slapi_DN *remote_dn)
+{
+ slapi_log_error(SLAPI_LOG_PLUGIN, ipa_winsync_plugin_name,
+ "--> ipa_winsync_can_add_entry_to_ad_cb -- begin\n");
+
+ slapi_log_error(SLAPI_LOG_PLUGIN, ipa_winsync_plugin_name,
+ "<-- ipa_winsync_can_add_entry_to_ad_cb -- end\n");
+
+ return 0; /* false - do not allow entries to be added to ad */
+}
+
+static void
+ipa_winsync_begin_update_cb(void *cbdata, const Slapi_DN *ds_subtree,
+ const Slapi_DN *ad_subtree, int is_total)
+{
+ slapi_log_error(SLAPI_LOG_PLUGIN, ipa_winsync_plugin_name,
+ "--> ipa_winsync_begin_update_cb -- begin\n");
+
+ ipa_winsync_config_refresh_domain(cbdata, ds_subtree, ad_subtree);
+
+ slapi_log_error(SLAPI_LOG_PLUGIN, ipa_winsync_plugin_name,
+ "<-- ipa_winsync_begin_update_cb -- end\n");
+
+ return;
+}
+
+static void
+ipa_winsync_end_update_cb(void *cbdata, const Slapi_DN *ds_subtree,
+ const Slapi_DN *ad_subtree, int is_total)
+{
+ slapi_log_error(SLAPI_LOG_PLUGIN, ipa_winsync_plugin_name,
+ "--> ipa_winsync_end_update_cb -- begin\n");
+
+ slapi_log_error(SLAPI_LOG_PLUGIN, ipa_winsync_plugin_name,
+ "<-- ipa_winsync_end_update_cb -- end\n");
+
+ return;
+}
+
+static void
+ipa_winsync_destroy_agmt_cb(void *cbdata, const Slapi_DN *ds_subtree,
+ const Slapi_DN *ad_subtree)
+{
+ slapi_log_error(SLAPI_LOG_PLUGIN, ipa_winsync_plugin_name,
+ "--> ipa_winsync_destroy_agmt_cb -- begin\n");
+
+ ipa_winsync_config_destroy_domain(cbdata, ds_subtree, ad_subtree);
+
+ slapi_log_error(SLAPI_LOG_PLUGIN, ipa_winsync_plugin_name,
+ "<-- ipa_winsync_destroy_agmt_cb -- end\n");
+
+ return;
+}
+
+static void *ipa_winsync_api[] = {
+ NULL, /* reserved for api broker use, must be zero */
+ ipa_winsync_agmt_init,
+ ipa_winsync_dirsync_search_params_cb,
+ ipa_winsync_pre_ad_search_cb,
+ ipa_winsync_pre_ds_search_entry_cb,
+ ipa_winsync_pre_ds_search_all_cb,
+ ipa_winsync_pre_ad_mod_user_cb,
+ ipa_winsync_pre_ad_mod_group_cb,
+ ipa_winsync_pre_ds_mod_user_cb,
+ ipa_winsync_pre_ds_mod_group_cb,
+ ipa_winsync_pre_ds_add_user_cb,
+ ipa_winsync_pre_ds_add_group_cb,
+ ipa_winsync_get_new_ds_user_dn_cb,
+ ipa_winsync_get_new_ds_group_dn_cb,
+ ipa_winsync_pre_ad_mod_user_mods_cb,
+ ipa_winsync_pre_ad_mod_group_mods_cb,
+ ipa_winsync_can_add_entry_to_ad_cb,
+ ipa_winsync_begin_update_cb,
+ ipa_winsync_end_update_cb,
+ ipa_winsync_destroy_agmt_cb
+};
+
+/**
+ * Plugin identifiers
+ */
+static Slapi_PluginDesc ipa_winsync_pdesc = {
+ "ipa-winsync-plugin",
+ "FreeIPA project",
+ "FreeIPA/1.0",
+ "ipa winsync plugin"
+};
+
+static Slapi_ComponentId *ipa_winsync_plugin_id = NULL;
+
+/*
+** Plugin identity mgmt
+*/
+
+void ipa_winsync_set_plugin_identity(void * identity)
+{
+ ipa_winsync_plugin_id=identity;
+}
+
+void * ipa_winsync_get_plugin_identity()
+{
+ return ipa_winsync_plugin_id;
+}
+
+static int
+ipa_winsync_plugin_start(Slapi_PBlock *pb)
+{
+ int rc;
+ Slapi_Entry *config_e = NULL; /* entry containing plugin config */
+
+ slapi_log_error(SLAPI_LOG_PLUGIN, ipa_winsync_plugin_name,
+ "--> ipa_winsync_plugin_start -- begin\n");
+
+ if( slapi_apib_register(WINSYNC_v1_0_GUID, ipa_winsync_api) ) {
+ slapi_log_error( SLAPI_LOG_FATAL, ipa_winsync_plugin_name,
+ "<-- ipa_winsync_plugin_start -- failed to register winsync api -- end\n");
+ return -1;
+ }
+
+ if ( slapi_pblock_get( pb, SLAPI_ADD_ENTRY, &config_e ) != 0 ) {
+ slapi_log_error( SLAPI_LOG_FATAL, ipa_winsync_plugin_name,
+ "missing config entry\n" );
+ return( -1 );
+ }
+
+ if (( rc = ipa_winsync_config( config_e )) != LDAP_SUCCESS ) {
+ slapi_log_error( SLAPI_LOG_FATAL, ipa_winsync_plugin_name,
+ "configuration failed (%s)\n", ldap_err2string( rc ));
+ return( -1 );
+ }
+
+ slapi_log_error(SLAPI_LOG_PLUGIN, ipa_winsync_plugin_name,
+ "<-- ipa_winsync_plugin_start -- end\n");
+ return 0;
+}
+
+static int
+ipa_winsync_plugin_close(Slapi_PBlock *pb)
+{
+ slapi_log_error(SLAPI_LOG_PLUGIN, ipa_winsync_plugin_name,
+ "--> ipa_winsync_plugin_close -- begin\n");
+
+ slapi_apib_unregister(WINSYNC_v1_0_GUID);
+
+ slapi_log_error(SLAPI_LOG_PLUGIN, ipa_winsync_plugin_name,
+ "<-- ipa_winsync_plugin_close -- end\n");
+ return 0;
+}
+
+/* this is the slapi plugin init function,
+ not the one used by the winsync api
+*/
+int ipa_winsync_plugin_init(Slapi_PBlock *pb)
+{
+ void *plugin_id = NULL;
+
+ slapi_log_error(SLAPI_LOG_PLUGIN, ipa_winsync_plugin_name,
+ "--> ipa_winsync_plugin_init -- begin\n");
+
+ if ( slapi_pblock_set( pb, SLAPI_PLUGIN_VERSION,
+ SLAPI_PLUGIN_VERSION_01 ) != 0 ||
+ slapi_pblock_set(pb, SLAPI_PLUGIN_START_FN,
+ (void *) ipa_winsync_plugin_start ) != 0 ||
+ slapi_pblock_set(pb, SLAPI_PLUGIN_CLOSE_FN,
+ (void *) ipa_winsync_plugin_close ) != 0 ||
+ slapi_pblock_set( pb, SLAPI_PLUGIN_DESCRIPTION,
+ (void *)&ipa_winsync_pdesc ) != 0 )
+ {
+ slapi_log_error( SLAPI_LOG_FATAL, ipa_winsync_plugin_name,
+ "<-- ipa_winsync_plugin_init -- failed to register plugin -- end\n");
+ return -1;
+ }
+
+ /* Retrieve and save the plugin identity to later pass to
+ internal operations */
+ if (slapi_pblock_get(pb, SLAPI_PLUGIN_IDENTITY, &plugin_id) != 0) {
+ slapi_log_error(SLAPI_LOG_FATAL, ipa_winsync_plugin_name,
+ "<-- ipa_winsync_plugin_init -- failed to retrieve plugin identity -- end\n");
+ return -1;
+ }
+
+ ipa_winsync_set_plugin_identity(plugin_id);
+
+ slapi_log_error( SLAPI_LOG_PLUGIN, ipa_winsync_plugin_name,
+ "<-- ipa_winsync_plugin_init -- end\n");
+ return 0;
+}
+
+/*
+ * Check if the given entry has account lock on (i.e. entry is disabled)
+ * Mostly copied from check_account_lock in the server code.
+ * Returns: 0 - account is disabled (lock == "true")
+ * 1 - account is enabled (lock == "false" or empty)
+ * -1 - some sort of error
+ */
+static int
+ipa_check_account_lock(Slapi_Entry *ds_entry, int *isvirt)
+{
+ int rc = 1;
+ Slapi_ValueSet *values = NULL;
+ int type_name_disposition = 0;
+ char *actual_type_name = NULL;
+ int attr_free_flags = 0;
+ char *strval;
+
+ /* first, see if the attribute is a "real" attribute */
+ strval = slapi_entry_attr_get_charptr(ds_entry, "nsAccountLock");
+ if (strval) { /* value is real */
+ *isvirt = 0; /* value is real */
+ rc = 1; /* default to enabled */
+ if (PL_strncasecmp(strval, "true", 4) == 0) {
+ rc = 0; /* account is disabled */
+ }
+ slapi_ch_free_string(&strval);
+ slapi_log_error(SLAPI_LOG_PLUGIN, ipa_winsync_plugin_name,
+ "<-- ipa_check_account_lock - entry [%s] has real "
+ "attribute nsAccountLock and entry %s locked\n",
+ slapi_entry_get_dn_const(ds_entry),
+ rc ? "is not" : "is");
+ return rc;
+ }
+
+ rc = slapi_vattr_values_get(ds_entry, "nsAccountLock",
+ &values,
+ &type_name_disposition, &actual_type_name,
+ SLAPI_VIRTUALATTRS_REQUEST_POINTERS,
+ &attr_free_flags);
+ if (rc == 0) {
+ Slapi_Value *v = NULL;
+ const struct berval *bvp = NULL;
+
+ rc = 1; /* default is enabled */
+ *isvirt = 1; /* value is virtual */
+ if ((slapi_valueset_first_value(values, &v) != -1) &&
+ (bvp = slapi_value_get_berval(v)) != NULL) {
+ if ( (bvp != NULL) && (PL_strncasecmp(bvp->bv_val, "true", 4) == 0) ) {
+ slapi_vattr_values_free(&values, &actual_type_name, attr_free_flags);
+ rc = 0; /* account is disabled */
+ }
+ }
+
+ if (values != NULL) {
+ slapi_vattr_values_free(&values, &actual_type_name, attr_free_flags);
+ }
+ slapi_log_error(SLAPI_LOG_PLUGIN, ipa_winsync_plugin_name,
+ "<-- ipa_check_account_lock - entry [%s] has virtual "
+ "attribute nsAccountLock and entry %s locked\n",
+ slapi_entry_get_dn_const(ds_entry),
+ rc ? "is not" : "is");
+ } else {
+ rc = 1; /* no attr == entry is enabled */
+ slapi_log_error(SLAPI_LOG_PLUGIN, ipa_winsync_plugin_name,
+ "<-- ipa_check_account_lock - entry [%s] does not "
+ "have attribute nsAccountLock - entry %s locked\n",
+ slapi_entry_get_dn_const(ds_entry),
+ rc ? "is not" : "is");
+ }
+
+ return rc;
+}
+
+static int
+do_group_modify(const char *dn, const char *modtype, int modop, const char *modval)
+{
+ int rc = 0;
+ LDAPMod mod;
+ LDAPMod *mods[2];
+ const char *val[2];
+ Slapi_PBlock *mod_pb = NULL;
+
+ mod_pb = slapi_pblock_new();
+
+ mods[0] = &mod;
+ mods[1] = NULL;
+
+ val[0] = modval;
+ val[1] = NULL;
+
+ mod.mod_op = modop;
+ mod.mod_type = (char *)modtype;
+ mod.mod_values = (char **)val;
+
+ slapi_modify_internal_set_pb(
+ mod_pb, dn, mods, 0, 0,
+ ipa_winsync_get_plugin_identity(), 0);
+
+ slapi_modify_internal_pb(mod_pb);
+
+ slapi_pblock_get(mod_pb,
+ SLAPI_PLUGIN_INTOP_RESULT,
+ &rc);
+
+ slapi_pblock_destroy(mod_pb);
+
+ slapi_log_error(SLAPI_LOG_PLUGIN, ipa_winsync_plugin_name,
+ "<-- do_group_modify - %s value [%s] in attribute [%s] "
+ "in entry [%s] - result (%d: %s)\n",
+ (modop & LDAP_MOD_ADD) ? "added" : "deleted",
+ modval, modtype, dn,
+ rc, ldap_err2string(rc));
+
+ return rc;
+}
+
+/*
+ * This can be used either in the to ad direction or the to ds direction, since in both
+ * cases we have to read both entries and compare the values.
+ * ad_entry - entry from AD
+ * ds_entry - entry from DS
+ * direction - either ACCT_DISABLE_TO_AD or ACCT_DISABLE_TO_DS
+ *
+ * If smods is given, this is the list of mods to send in the given direction. The
+ * appropriate modify operation will be added to this list or changed to the correct
+ * value if it already exists.
+ * Otherwise, if a destination entry is given, the value will be written into
+ * that entry.
+ */
+static void
+sync_acct_disable(
+ void *cbdata, /* the usual domain config data */
+ const Slapi_Entry *ad_entry, /* the AD entry */
+ Slapi_Entry *ds_entry, /* the DS entry */
+ int direction, /* the direction - TO_AD or TO_DS */
+ Slapi_Entry *update_entry, /* the entry to update for ADDs */
+ Slapi_Mods *smods, /* the mod list for MODIFYs */
+ int *do_modify /* if not NULL, set this to true if mods were added */
+)
+{
+ IPA_WinSync_Domain_Config *ipaconfig = (IPA_WinSync_Domain_Config *)cbdata;
+ IPA_WinSync_Config *global_ipaconfig = ipa_winsync_get_config();
+ int acct_disable;
+ int ds_is_enabled = 1; /* default to true */
+ int ad_is_enabled = 1; /* default to true */
+ unsigned long adval = 0; /* raw account val from ad entry */
+ int isvirt = 1; /* default to virt */
+
+ slapi_lock_mutex(global_ipaconfig->lock);
+ acct_disable = global_ipaconfig->acct_disable;
+ slapi_unlock_mutex(global_ipaconfig->lock);
+
+ if (acct_disable == ACCT_DISABLE_NONE) {
+ return; /* not supported */
+ }
+
+ /* get the account lock state of the ds entry */
+ if (0 == ipa_check_account_lock(ds_entry, &isvirt)) {
+ ds_is_enabled = 0;
+ }
+
+ /* get the account lock state of the ad entry */
+ adval = slapi_entry_attr_get_ulong(ad_entry, "UserAccountControl");
+ if (adval & 0x2) {
+ /* account is disabled */
+ ad_is_enabled = 0;
+ }
+
+ if (ad_is_enabled == ds_is_enabled) { /* both have same value - nothing to do */
+ return;
+ }
+
+ /* have to enable or disable */
+ if (direction == ACCT_DISABLE_TO_AD) {
+ unsigned long mask;
+ /* set the mod or entry */
+ if (update_entry) {
+ if (ds_is_enabled) {
+ mask = ~0x2;
+ adval &= mask; /* unset the 0x2 disable bit */
+ } else {
+ mask = 0x2;
+ adval |= mask; /* set the 0x2 disable bit */
+ }
+ slapi_entry_attr_set_ulong(update_entry, "userAccountControl", adval);
+ slapi_log_error(SLAPI_LOG_PLUGIN, ipa_winsync_plugin_name,
+ "<-- sync_acct_disable - %s AD account [%s] - "
+ "new value is [%ld]\n",
+ (ds_is_enabled) ? "enabled" : "disabled",
+ slapi_entry_get_dn_const(update_entry),
+ adval);
+ } else {
+ /* iterate through the mods - if there is already a mod
+ for userAccountControl, change it - otherwise, add it */
+ char acctvalstr[32];
+ LDAPMod *mod = NULL;
+ struct berval *mod_bval = NULL;
+ for (mod = slapi_mods_get_first_mod(smods); mod;
+ mod = slapi_mods_get_next_mod(smods)) {
+ if (!PL_strcasecmp(mod->mod_type, "userAccountControl") &&
+ mod->mod_bvalues && mod->mod_bvalues[0]) {
+ mod_bval = mod->mod_bvalues[0];
+ /* mod_bval points directly to value inside mod list */
+ break;
+ }
+ }
+ if (!mod_bval) { /* not found - add it */
+ struct berval tmpbval = {0, NULL};
+ Slapi_Mod *smod = slapi_mod_new();
+ slapi_mod_init(smod, 1); /* one element */
+ slapi_mod_set_type(smod, "userAccountControl");
+ slapi_mod_set_operation(smod, LDAP_MOD_REPLACE|LDAP_MOD_BVALUES);
+ slapi_mod_add_value(smod, &tmpbval);
+ /* add_value makes a copy of the bval - so let's get a pointer
+ to that new value - we will change the bval in place */
+ mod_bval = slapi_mod_get_first_value(smod);
+ /* mod_bval points directly to value inside mod list */
+ /* now add the new mod to smods */
+ slapi_mods_add_ldapmod(smods,
+ slapi_mod_get_ldapmod_passout(smod));
+ /* smods now owns the ldapmod */
+ slapi_mod_free(&smod);
+ if (do_modify) {
+ *do_modify = 1; /* added mods */
+ }
+ }
+ if (mod_bval) {
+ /* this is where we set or update the actual value
+ mod_bval points directly into the mod list we are
+ sending */
+ if (mod_bval->bv_val && (mod_bval->bv_len > 0)) {
+ /* get the old val */
+ adval = strtol(mod_bval->bv_val, NULL, 10);
+ }
+ if (ds_is_enabled) {
+ mask = ~0x2;
+ adval &= mask; /* unset the 0x2 disable bit */
+ } else {
+ mask = 0x2;
+ adval |= mask; /* set the 0x2 disable bit */
+ }
+ PR_snprintf(acctvalstr, sizeof(acctvalstr), "%lu", adval);
+ slapi_ch_free_string(&mod_bval->bv_val);
+ mod_bval->bv_val = slapi_ch_strdup(acctvalstr);
+ mod_bval->bv_len = strlen(acctvalstr);
+ }
+ slapi_log_error(SLAPI_LOG_PLUGIN, ipa_winsync_plugin_name,
+ "<-- sync_acct_disable - %s AD account [%s] - "
+ "new value is [%ld]\n",
+ (ds_is_enabled) ? "enabled" : "disabled",
+ slapi_entry_get_dn_const(ad_entry),
+ adval);
+ }
+ }
+
+ if (direction == ACCT_DISABLE_TO_DS) {
+ if (!isvirt) {
+ char *attrtype = NULL;
+ char *attrval = NULL;
+ attrtype = "nsAccountLock";
+ if (ad_is_enabled) {
+ attrval = NULL; /* will delete the value */
+ } else {
+ attrval = "true";
+ }
+
+ if (update_entry) {
+ slapi_entry_attr_set_charptr(update_entry, attrtype, attrval);
+ slapi_log_error(SLAPI_LOG_PLUGIN, ipa_winsync_plugin_name,
+ "<-- sync_acct_disable - %s DS account [%s]\n",
+ (ad_is_enabled) ? "enabled" : "disabled",
+ slapi_entry_get_dn_const(ds_entry));
+ } else { /* do mod */
+ struct berval tmpbval = {0, NULL};
+ Slapi_Mod *smod = slapi_mod_new();
+ slapi_mod_init(smod, 1); /* one element */
+ slapi_mod_set_type(smod, attrtype);
+ if (attrval == NULL) {
+ slapi_mod_set_operation(smod, LDAP_MOD_DELETE|LDAP_MOD_BVALUES);
+ } else {
+ slapi_mod_set_operation(smod, LDAP_MOD_REPLACE|LDAP_MOD_BVALUES);
+ }
+ slapi_mod_add_value(smod, &tmpbval);
+ slapi_mods_add_ldapmod(smods,
+ slapi_mod_get_ldapmod_passout(smod));
+ slapi_mod_free(&smod);
+ slapi_log_error(SLAPI_LOG_PLUGIN, ipa_winsync_plugin_name,
+ "<-- sync_acct_disable - %s DS account [%s]\n",
+ (ad_is_enabled) ? "enabled" : "disabled",
+ slapi_entry_get_dn_const(ds_entry));
+ if (do_modify) {
+ *do_modify = 1; /* added mods */
+ }
+ }
+ } else { /* use the virtual attr scheme */
+ char *adddn, *deldn;
+ const char *dsdn;
+ int rc;
+ /* in the case of disabling a user, need to remove that user from
+ the activated group, if in there, and add to the inactivated group
+ however, in the case of enabling a user, we just have to remove
+ the user from the inactivated group, if in there - if the user
+ is not in any group, the user is activated by default
+ */
+ if (ad_is_enabled) {
+ /* add user to activated group, delete from inactivated group */
+ adddn = NULL; /* no group means active by default */
+ deldn = ipaconfig->inactivated_group_dn;
+ } else {
+ /* add user to inactivated group, delete from activated group */
+ adddn = ipaconfig->inactivated_group_dn;
+ deldn = ipaconfig->activated_group_dn;
+ }
+
+ dsdn = slapi_entry_get_dn_const(ds_entry);
+ slapi_log_error(SLAPI_LOG_PLUGIN, ipa_winsync_plugin_name,
+ "<-- sync_acct_disable - %s DS account [%s] - "
+ "deldn [%s] adddn [%s]\n",
+ (ad_is_enabled) ? "enabling" : "disabling",
+ slapi_entry_get_dn_const(ds_entry),
+ deldn, adddn);
+ /* first, delete the user from the deldn group - ignore (but log)
+ value not found errors - means the user wasn't there yet */
+ rc = do_group_modify(deldn, "member", LDAP_MOD_DELETE, dsdn);
+ if (rc == LDAP_NO_SUCH_ATTRIBUTE) {
+ /* either the value of the attribute doesn't exist */
+ slapi_log_error(SLAPI_LOG_PLUGIN, ipa_winsync_plugin_name,
+ "Could not delete user [%s] from the [%s] group: "
+ "either the user was not in the group already, "
+ "or the group had no members\n",
+ dsdn, deldn);
+ } else if (rc != LDAP_SUCCESS) {
+ slapi_log_error(SLAPI_LOG_FATAL, ipa_winsync_plugin_name,
+ "Error deleting user [%s] from the [%s] group: "
+ "(%d - %s)\n", dsdn, deldn, rc,
+ ldap_err2string(rc));
+ }
+ /* next, add the user to the adddn group - ignore (but log)
+ if the user is already in that group */
+ if (adddn) {
+ rc = do_group_modify(adddn, "member", LDAP_MOD_ADD, dsdn);
+ } else {
+ rc = LDAP_SUCCESS;
+ }
+ if (rc == LDAP_TYPE_OR_VALUE_EXISTS) {
+ /* user already in that group */
+ slapi_log_error(SLAPI_LOG_PLUGIN, ipa_winsync_plugin_name,
+ "Could not add user [%s] to the [%s] group: "
+ "user is already in that group\n",
+ dsdn, adddn);
+ } else if (rc != LDAP_SUCCESS) {
+ slapi_log_error(SLAPI_LOG_FATAL, ipa_winsync_plugin_name,
+ "Error adding user [%s] to the [%s] group: "
+ "(%d - %s)\n", dsdn, adddn, rc,
+ ldap_err2string(rc));
+ }
+#ifndef MEMBEROF_WORKS_FOR_INTERNAL_OPS
+ /* memberOf doesn't currently listen for internal operations
+ that change group membership - so we manually set the
+ memberOf attribute in the ds entry - this should not
+ conflict with memberOf */
+ {
+ Slapi_Value *sv = slapi_value_new();
+ slapi_value_init_string(sv, deldn);
+ if (slapi_entry_attr_has_syntax_value(ds_entry,
+ "memberOf", sv)) {
+ if (smods) {
+ slapi_mods_add_string(smods, LDAP_MOD_DELETE,
+ "memberOf", deldn);
+ if (do_modify) {
+ *do_modify = 1; /* added mods */
+ }
+ } else if (update_entry) {
+ slapi_entry_delete_string(update_entry,
+ "memberOf", deldn);
+ }
+ }
+ if (adddn) {
+ slapi_value_set_string(sv, adddn);
+ if (!slapi_entry_attr_has_syntax_value(ds_entry,
+ "memberOf", sv)) {
+ if (smods) {
+ slapi_mods_add_string(smods, LDAP_MOD_ADD,
+ "memberOf", adddn);
+ if (do_modify) {
+ *do_modify = 1; /* added mods */
+ }
+ } else if (update_entry) {
+ slapi_entry_add_string(update_entry,
+ "memberOf", adddn);
+ }
+ }
+ }
+ slapi_value_free(&sv);
+ }
+#endif /* MEMBEROF_WORKS_FOR_INTERNAL_OPS */
+ slapi_log_error(SLAPI_LOG_PLUGIN, ipa_winsync_plugin_name,
+ "<-- sync_acct_disable - %s DS account [%s]\n",
+ (ad_is_enabled) ? "enabled" : "disabled",
+ slapi_entry_get_dn_const(ds_entry));
+ }
+ }
+
+ return;
+}
+
+/* if entry does not have attribute type and val, and neither
+ does the smods, add them to the smods */
+static void
+find_and_add_mod(Slapi_Entry *ent, Slapi_Mods *smods, const char *type,
+ const char *val, size_t vallen, int *do_modify)
+{
+ int found = 1;
+ Slapi_Value *sv = slapi_value_new();
+ LDAPMod *mod = NULL;
+
+ slapi_value_init_string(sv, val);
+ if (!slapi_entry_attr_has_syntax_value(ent, type, sv)) {
+ /* entry doesn't have type val - see if there is already
+ a mod in the mods list that adds it replaces it */
+ found = 0; /* not found in entry - see if in mod list */
+ for (mod = slapi_mods_get_first_mod(smods);
+ !found && mod;
+ mod = slapi_mods_get_next_mod(smods)) {
+ int ii;
+ if (PL_strcasecmp(mod->mod_type, type)) {
+ continue; /* skip - not a mod of this type */
+ }
+ if (!(mod->mod_op & (LDAP_MOD_ADD|LDAP_MOD_REPLACE))) {
+ continue; /* skip - not an add or replace op */
+ }
+ /* now see if val is in the list of vals for this mod op */
+ for (ii = 0;
+ !found && mod->mod_bvalues && mod->mod_bvalues[ii];
+ ++ii) {
+ if (mod->mod_bvalues[ii]->bv_val) {
+ found = !PL_strncasecmp(mod->mod_bvalues[ii]->bv_val,
+ val, vallen);
+ }
+ }
+ }
+ }
+ if (!found) {
+ slapi_mods_add_string(smods, LDAP_MOD_ADD, type, val);
+ if (do_modify) {
+ *do_modify = 1; /* added a mod */
+ }
+ slapi_log_error(SLAPI_LOG_PLUGIN, ipa_winsync_plugin_name,
+ "<-- find_and_add_mod - added value [%s] "
+ "to attribute [%s] in entry [%s]\n",
+ val, type, slapi_entry_get_dn_const(ent));
+ }
+ slapi_value_free(&sv);
+
+ return;
+}
+
+/*
+ * If force sync is true, any time an entry is being added or modified
+ * in DS, we must ensure the entry has the ntUser objectclass, and that
+ * it has the ntUserDomainID attribute, and the value of that attribute
+ * corresponds to the samAccountName in the AD entry.
+ * ad_entry - entry from AD
+ * ds_entry - entry from DS
+ *
+ * The appropriate modify operation will be added to the given smods
+ * if it doesn't already exist.
+ */
+static void
+do_force_sync(
+ const Slapi_Entry *ad_entry, /* the AD entry */
+ Slapi_Entry *ds_entry, /* the DS entry */
+ Slapi_Mods *smods, /* the mod list for MODIFYs */
+ int *do_modify /* if not NULL, set to true if mods were added */
+)
+{
+ IPA_WinSync_Config *global_ipaconfig = ipa_winsync_get_config();
+ PRBool forceSync;
+
+ slapi_lock_mutex(global_ipaconfig->lock);
+ forceSync = global_ipaconfig->forceSync;
+ slapi_unlock_mutex(global_ipaconfig->lock);
+
+ if (forceSync == PR_FALSE) {
+ return; /* not supported */
+ }
+
+ slapi_log_error(SLAPI_LOG_PLUGIN, ipa_winsync_plugin_name,
+ "do_force_sync - forcing sync of AD entry [%s] "
+ "with DS entry [%s]\n",
+ slapi_entry_get_dn_const(ad_entry),
+ slapi_entry_get_dn_const(ds_entry));
+
+ find_and_add_mod(ds_entry, smods, "objectClass", "ntUser", (size_t)6, do_modify);
+
+ return;
+}
diff --git a/ipa-server/ipa-slapi-plugins/ipa-winsync/ipa-winsync.h b/ipa-server/ipa-slapi-plugins/ipa-winsync/ipa-winsync.h
new file mode 100644
index 00000000..58a9a6c4
--- /dev/null
+++ b/ipa-server/ipa-slapi-plugins/ipa-winsync/ipa-winsync.h
@@ -0,0 +1,160 @@
+/** BEGIN COPYRIGHT BLOCK
+ * This Program is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free Software
+ * Foundation; version 2 of the License.
+ *
+ * This Program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * this Program; if not, write to the Free Software Foundation, Inc., 59 Temple
+ * Place, Suite 330, Boston, MA 02111-1307 USA.
+ *
+ * In addition, as a special exception, Red Hat, Inc. gives You the additional
+ * right to link the code of this Program with code not covered under the GNU
+ * General Public License ("Non-GPL Code") and to distribute linked combinations
+ * including the two, subject to the limitations in this paragraph. Non-GPL Code
+ * permitted under this exception must only link to the code of this Program
+ * through those well defined interfaces identified in the file named EXCEPTION
+ * found in the source code files (the "Approved Interfaces"). The files of
+ * Non-GPL Code may instantiate templates or use macros or inline functions from
+ * the Approved Interfaces without causing the resulting work to be covered by
+ * the GNU General Public License. Only Red Hat, Inc. may make changes or
+ * additions to the list of Approved Interfaces. You must obey the GNU General
+ * Public License in all respects for all of the Program code and other code
+ * used in conjunction with the Program except the Non-GPL Code covered by this
+ * exception. If you modify this file, you may extend this exception to your
+ * version of the file, but you are not obligated to do so. If you do not wish
+ * to provide this exception without modification, you must delete this
+ * exception statement from your version and license this file solely under the
+ * GPL without exception.
+ *
+ * Authors:
+ * Rich Megginson <rmeggins@redhat.com>
+ *
+ * Copyright (C) 2008 Red Hat, Inc.
+ * All rights reserved.
+ * END COPYRIGHT BLOCK **/
+
+#ifndef IPA_WINSYNC_H
+#define IPA_WINSYNC_H
+
+#ifdef HAVE_CONFIG_H
+# include <config.h>
+#endif
+
+#ifdef WINSYNC_TEST_IPA
+#include <slapi-plugin.h>
+#include "winsync-plugin.h"
+#else /* the default */
+#include <dirsrv/slapi-plugin.h>
+#include <dirsrv/winsync-plugin.h>
+#endif /* WINSYNC_TEST_IPA */
+
+#define IPA_WINSYNC_PLUGIN_NAME "ipa-winsync"
+
+typedef struct ipa_winsync_config_struct {
+ Slapi_Mutex *lock; /* for config access */
+ Slapi_Entry *config_e; /* configuration entry */
+ PRBool flatten; /* flatten AD DNs */
+ char *realm_filter;
+ char *realm_attr;
+ char *new_entry_filter;
+ char *new_user_oc_attr; /* don't care about groups for now */
+ char *homedir_prefix_attr;
+ char *default_group_attr;
+ char *default_group_filter;
+ int acct_disable; /* see below for possible values */
+ char *inactivated_filter;
+ char *activated_filter;
+ PRBool forceSync;
+} IPA_WinSync_Config;
+
+/*
+ This is the structure that holds our domain
+ specific configuration
+*/
+typedef struct ipa_winsync_domain_config {
+ Slapi_Entry *domain_e; /* info is stored in this entry */
+ char *realm_name; /* realm name */
+ char *homedir_prefix;
+ char *inactivated_group_dn; /* DN of inactivated group */
+ char *activated_group_dn; /* DN of activated group */
+} IPA_WinSync_Domain_Config;
+
+void ipa_winsync_set_plugin_identity(void * identity);
+void * ipa_winsync_get_plugin_identity();
+
+int ipa_winsync_config( Slapi_Entry *config_e );
+IPA_WinSync_Config *ipa_winsync_get_config( void );
+
+/*
+ * Agreement/domain specific configuration
+ */
+/* return a new domain specific configuration object */
+void *ipa_winsync_config_new_domain(const Slapi_DN *ds_subtree, const Slapi_DN *ad_subtree);
+/* refresh the domain specific configuration object */
+void ipa_winsync_config_refresh_domain(void *cbdata, const Slapi_DN *ds_subtree, const Slapi_DN *ad_subtree);
+/* destroy the domain specific configuration object */
+void ipa_winsync_config_destroy_domain(void *cbdata, const Slapi_DN *ds_subtree, const Slapi_DN *ad_subtree);
+
+/* name of attribute holding the filter to use to
+ find the ipa realm value
+*/
+#define IPA_WINSYNC_REALM_FILTER_ATTR "ipaWinSyncRealmFilter"
+/* name of attribute holding the name of the attribute
+ which contains the ipa realm value
+*/
+#define IPA_WINSYNC_REALM_ATTR_ATTR "ipaWinSyncRealmAttr"
+/* name of attribute holding the filter to use to
+ find the new user template entry
+*/
+#define IPA_WINSYNC_NEW_ENTRY_FILTER_ATTR "ipaWinSyncNewEntryFilter"
+/* name of attribute holding the name of the attribute
+ in the new user template entry which has the list of objectclasses
+*/
+#define IPA_WINSYNC_NEW_USER_OC_ATTR "ipaWinSyncNewUserOCAttr"
+/* name of attribute holding the new user attributes and values */
+#define IPA_WINSYNC_NEW_USER_ATTRS_VALS "ipaWinSyncUserAttr"
+/* name of attribute holding the name of the attribute which
+ has the homeDirectory prefix - suffix is the uid */
+#define IPA_WINSYNC_HOMEDIR_PREFIX_ATTR "ipaWinsyncHomeDirAttr"
+/* name of attribute holding the name of the attribute which is
+ used to get the default posix gidNumber */
+#define IPA_WINSYNC_DEFAULTGROUP_ATTR "ipaWinSyncDefaultGroupAttr"
+/* filter used to find the group with the gid number whose group name
+ is in the IPA_WINSYNC_DEFAULTGROUP_ATTR - the filter will have
+ cn=valueofIPA_WINSYNC_DEFAULTGROUP_ATTR appended to it */
+#define IPA_WINSYNC_DEFAULTGROUP_FILTER_ATTR "ipaWinSyncDefaultGroupFilter"
+/* name of attribute holding boolean value to flatten user dns or not */
+#define IPA_WINSYNC_USER_FLATTEN "ipaWinSyncUserFlatten"
+/* name of attribute holding account disable sync value */
+#define IPA_WINSYNC_ACCT_DISABLE "ipaWinSyncAcctDisable"
+/* possible values of IPA_WINSYNC_ACCT_DISABLE */
+#define IPA_WINSYNC_ACCT_DISABLE_NONE "none"
+#define IPA_WINSYNC_ACCT_DISABLE_TO_AD "to_ad"
+#define IPA_WINSYNC_ACCT_DISABLE_TO_DS "to_ds"
+#define IPA_WINSYNC_ACCT_DISABLE_BOTH "both"
+/* enum representing the values above */
+enum {
+ ACCT_DISABLE_INVALID, /* the invalid value */
+ ACCT_DISABLE_NONE, /* do not sync acct disable status */
+ ACCT_DISABLE_TO_AD, /* sync only from ds to ad */
+ ACCT_DISABLE_TO_DS, /* sync only from ad to ds */
+ ACCT_DISABLE_BOTH /* bi-directional sync */
+};
+/* name of attributes holding the search filters to use to find
+ the DN of the groups that represent inactivated and activated users */
+#define IPA_WINSYNC_INACTIVATED_FILTER "ipaWinSyncInactivatedFilter"
+#define IPA_WINSYNC_ACTIVATED_FILTER "ipaWinSyncActivatedFilter"
+/* name of attribute holding the value of the forceSync parameter -
+ this is a boolean attribute - if true, all users in AD that have
+ a corresponding entry in the DS will be synced - there will be no
+ way to "turn off sync" on individual entries - if this value is
+ false, only users which have the ntUser objectclass and an
+ ntDomainUserID attribute which corresponds to an AD account
+ with the same value for samAccountName will be synced
+*/
+#define IPA_WINSYNC_FORCE_SYNC "ipaWinSyncForceSync"
+#endif /* IPA_WINSYNC_H */
diff --git a/ipa-server/ipa-upgradeconfig b/ipa-server/ipa-upgradeconfig
new file mode 100644
index 00000000..48c4117d
--- /dev/null
+++ b/ipa-server/ipa-upgradeconfig
@@ -0,0 +1,130 @@
+#!/usr/bin/python
+#
+# Upgrade configuration files to a newer template.
+
+import sys
+try:
+ from ipa import ipautil
+ import krbV
+ import re
+ import os
+ import shutil
+ import fileinput
+except ImportError:
+ print >> sys.stderr, """\
+There was a problem importing one of the required Python modules. The
+error was:
+
+ %s
+""" % sys.exc_value
+ sys.exit(1)
+
+def backup_file(filename, ext):
+ """Make a backup of filename using ext as the extension. Do not overwrite
+ previous backups."""
+ if not os.path.isabs(filename):
+ raise ValueError("Absolute path required")
+
+ backupfile = filename + ".bak"
+ (reldir, file) = os.path.split(filename)
+
+ while os.path.exists(backupfile):
+ backupfile = backupfile + "." + str(ext)
+
+ shutil.copy2(filename, backupfile)
+
+def update_conf(sub_dict, filename, template_filename):
+ template = ipautil.template_file(template_filename, sub_dict)
+ fd = open(filename, "w")
+ fd.write(template)
+ fd.close()
+
+def find_hostname():
+ """Find the hostname currently configured in ipa-rewrite.conf"""
+ filename="/etc/httpd/conf.d/ipa-rewrite.conf"
+ pattern = "^[\s#]*.*https:\/\/([A-Za-z0-9\.\-]*)\/.*"
+ p = re.compile(pattern)
+ for line in fileinput.input(filename):
+ if p.search(line):
+ fileinput.close()
+ return p.search(line).group(1)
+ fileinput.close()
+
+ return None
+
+def find_version(filename):
+ """Find the version of a configuration file"""
+ if os.path.exists(filename):
+ pattern = "^[\s#]*VERSION\s+([0-9]+)\s+.*"
+ p = re.compile(pattern)
+ for line in fileinput.input(filename):
+ if p.search(line):
+ fileinput.close()
+ return p.search(line).group(1)
+ fileinput.close()
+
+ # no VERSION found
+ return 0
+ else:
+ return -1
+
+def upgrade(sub_dict, filename, template):
+ old = int(find_version(filename))
+ new = int(find_version(template))
+
+ if old < 0:
+ print "%s not found." % filename
+ sys.exit(1)
+
+ if new < 0:
+ print "%s not found." % template
+
+ if old < new:
+ backup_file(filename, new)
+ update_conf(sub_dict, filename, template)
+ print "Upgraded %s to version %d" % (filename, new)
+
+def check_certs(realm_name):
+ """Check ca.crt is in the right place, and try to fix if not"""
+ if not os.path.exists("/usr/share/ipa/html/ca.crt"):
+ ca_file = "/etc/dirsrv/slapd-" + ("-".join(realm_name.split("."))) + "/cacert.asc"
+ if os.path.exists(ca_file):
+ shutil.copyfile(ca_file, "/usr/share/ipa/html/ca.crt")
+ else:
+ print "Missing Certification Authority file."
+ print "You should place a copy of the CA certificate in /usr/share/ipa/html/ca.crt"
+
+def main():
+ try:
+ krbctx = krbV.default_context()
+ except krbV.Krb5Error, e:
+ print "Unable to get default kerberos realm: %s" % e[1]
+ sys.exit(1)
+
+ try:
+ check_certs(krbctx.default_realm)
+ except Error, e:
+ print "Failed to check CA certificate: %s" % e
+
+ try:
+ fqdn = find_hostname()
+ except IOError:
+ # ipa-rewrite.conf doesn't exist, nothing to do
+ sys.exit(0)
+
+ if fqdn is None:
+ print "Unable to determine hostname from ipa-rewrite.conf"
+ sys.exit(1)
+
+ sub_dict = { "REALM" : krbctx.default_realm, "FQDN": fqdn }
+
+ upgrade(sub_dict, "/etc/httpd/conf.d/ipa.conf", ipautil.SHARE_DIR + "ipa.conf")
+ upgrade(sub_dict, "/etc/httpd/conf.d/ipa-rewrite.conf", ipautil.SHARE_DIR + "ipa-rewrite.conf")
+
+try:
+ if __name__ == "__main__":
+ sys.exit(main())
+except SystemExit, e:
+ sys.exit(e)
+except KeyboardInterrupt, e:
+ sys.exit(1)
diff --git a/ipa-server/ipaserver/Makefile.am b/ipa-server/ipaserver/Makefile.am
new file mode 100644
index 00000000..999dcf24
--- /dev/null
+++ b/ipa-server/ipaserver/Makefile.am
@@ -0,0 +1,24 @@
+NULL =
+
+appdir = $(pythondir)/ipaserver
+app_PYTHON = \
+ __init__.py \
+ bindinstance.py \
+ dsinstance.py \
+ ipaldap.py \
+ krbinstance.py \
+ httpinstance.py \
+ ntpinstance.py \
+ service.py \
+ installutils.py \
+ replication.py \
+ certs.py \
+ ldapupdate.py \
+ $(NULL)
+
+EXTRA_DIST = \
+ $(NULL)
+
+MAINTAINERCLEANFILES = \
+ *~ \
+ Makefile.in
diff --git a/ipa-server/ipaserver/__init__.py b/ipa-server/ipaserver/__init__.py
new file mode 100644
index 00000000..ef86f9ec
--- /dev/null
+++ b/ipa-server/ipaserver/__init__.py
@@ -0,0 +1,21 @@
+# Authors: Karl MacMillan <kmacmillan@mentalrootkit.com>
+# see inline
+#
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 or later
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+__all__ = ["dsinstance", "krbinstance"]
diff --git a/ipa-server/ipaserver/bindinstance.py b/ipa-server/ipaserver/bindinstance.py
new file mode 100644
index 00000000..5badf860
--- /dev/null
+++ b/ipa-server/ipaserver/bindinstance.py
@@ -0,0 +1,156 @@
+# Authors: Simo Sorce <ssorce@redhat.com>
+#
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+import string
+import tempfile
+import shutil
+import os
+import socket
+import logging
+
+import service
+from ipa import sysrestore
+from ipa import ipautil
+
+def check_inst():
+ # So far this file is always present in both RHEL5 and Fedora if all the necessary
+ # bind packages are installed (RHEL5 requires also the pkg: caching-nameserver)
+ if not os.path.exists('/etc/named.rfc1912.zones'):
+ return False
+
+ return True
+
+class BindInstance(service.Service):
+ def __init__(self, fstore=None):
+ service.Service.__init__(self, "named")
+ self.fqdn = None
+ self.domain = None
+ self.host = None
+ self.ip_address = None
+ self.realm = None
+ self.sub_dict = None
+
+ if fstore:
+ self.fstore = fstore
+ else:
+ self.fstore = sysrestore.FileStore('/var/lib/ipa/sysrestore')
+
+ def setup(self, fqdn, ip_address, realm_name, domain_name):
+ self.fqdn = fqdn
+ self.ip_address = ip_address
+ self.realm = realm_name
+ self.domain = domain_name
+ self.host = fqdn.split(".")[0]
+
+ self.__setup_sub_dict()
+
+ def create_sample_bind_zone(self):
+ bind_txt = ipautil.template_file(ipautil.SHARE_DIR + "bind.zone.db.template", self.sub_dict)
+ [bind_fd, bind_name] = tempfile.mkstemp(".db","sample.zone.")
+ os.write(bind_fd, bind_txt)
+ os.close(bind_fd)
+ print "Sample zone file for bind has been created in "+bind_name
+
+ def create_instance(self):
+
+ try:
+ self.stop()
+ except:
+ pass
+
+ self.step("Setting up our zone", self.__setup_zone)
+ self.step("Setting up named.conf", self.__setup_named_conf)
+
+ self.step("restarting named", self.__start)
+ self.step("configuring named to start on boot", self.__enable)
+
+ self.step("Changing resolv.conf to point to ourselves", self.__setup_resolv_conf)
+ self.start_creation("Configuring bind:")
+
+ def __start(self):
+ try:
+ self.backup_state("running", self.is_running())
+ self.restart()
+ except:
+ print "named service failed to start"
+
+ def __enable(self):
+ self.backup_state("enabled", self.is_running())
+ self.chkconfig_on()
+
+ def __setup_sub_dict(self):
+ self.sub_dict = dict(FQDN=self.fqdn,
+ IP=self.ip_address,
+ DOMAIN=self.domain,
+ HOST=self.host,
+ REALM=self.realm)
+
+ def __setup_zone(self):
+ self.backup_state("domain", self.domain)
+ zone_txt = ipautil.template_file(ipautil.SHARE_DIR + "bind.zone.db.template", self.sub_dict)
+ self.fstore.backup_file('/var/named/'+self.domain+'.zone.db')
+ zone_fd = open('/var/named/'+self.domain+'.zone.db', 'w')
+ zone_fd.write(zone_txt)
+ zone_fd.close()
+
+ def __setup_named_conf(self):
+ self.fstore.backup_file('/etc/named.conf')
+ named_txt = ipautil.template_file(ipautil.SHARE_DIR + "bind.named.conf.template", self.sub_dict)
+ named_fd = open('/etc/named.conf', 'w')
+ named_fd.seek(0)
+ named_fd.truncate(0)
+ named_fd.write(named_txt)
+ named_fd.close()
+
+ def __setup_resolv_conf(self):
+ self.fstore.backup_file('/etc/resolv.conf')
+ resolv_txt = "search "+self.domain+"\nnameserver "+self.ip_address+"\n"
+ resolv_fd = open('/etc/resolv.conf', 'w')
+ resolv_fd.seek(0)
+ resolv_fd.truncate(0)
+ resolv_fd.write(resolv_txt)
+ resolv_fd.close()
+
+ def uninstall(self):
+ running = self.restore_state("running")
+ enabled = self.restore_state("enabled")
+ domain = self.restore_state("domain")
+
+ if not running is None:
+ self.stop()
+
+ if not domain is None:
+ try:
+ self.fstore.restore_file(os.path.join ("/var/named/", domain + ".zone.db"))
+ except ValueError, error:
+ logging.debug(error)
+ pass
+
+ for f in ["/etc/named.conf", "/etc/resolv.conf"]:
+ try:
+ self.fstore.restore_file(f)
+ except ValueError, error:
+ logging.debug(error)
+ pass
+
+ if not enabled is None and not enabled:
+ self.chkconfig_off()
+
+ if not running is None and running:
+ self.start()
diff --git a/ipa-server/ipaserver/certs.py b/ipa-server/ipaserver/certs.py
new file mode 100644
index 00000000..8cb1d088
--- /dev/null
+++ b/ipa-server/ipaserver/certs.py
@@ -0,0 +1,424 @@
+# Authors: Karl MacMillan <kmacmillan@mentalrootkit.com>
+#
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+import os, stat, subprocess, re
+import sha
+import errno
+import tempfile
+import shutil
+
+from ipa import sysrestore
+from ipa import ipautil
+
+CA_SERIALNO="/var/lib/ipa/ca_serialno"
+
+class CertDB(object):
+ def __init__(self, dir, fstore=None):
+ self.secdir = dir
+
+ self.noise_fname = self.secdir + "/noise.txt"
+ self.passwd_fname = self.secdir + "/pwdfile.txt"
+ self.certdb_fname = self.secdir + "/cert8.db"
+ self.keydb_fname = self.secdir + "/key3.db"
+ self.secmod_fname = self.secdir + "/secmod.db"
+ self.cacert_fname = self.secdir + "/cacert.asc"
+ self.pk12_fname = self.secdir + "/cacert.p12"
+ self.pin_fname = self.secdir + "/pin.txt"
+ self.reqdir = tempfile.mkdtemp('', 'ipa-', '/var/lib/ipa')
+ self.certreq_fname = self.reqdir + "/tmpcertreq"
+ self.certder_fname = self.reqdir + "/tmpcert.der"
+
+ # Making this a starting value that will generate
+ # unique values for the current DB is the
+ # responsibility of the caller for now. In the
+ # future we might automatically determine this
+ # for a given db.
+ self.cur_serial = -1
+
+ self.cacert_name = "CA certificate"
+ self.valid_months = "120"
+ self.keysize = "1024"
+
+ # We are going to set the owner of all of the cert
+ # files to the owner of the containing directory
+ # instead of that of the process. This works when
+ # this is called by root for a daemon that runs as
+ # a normal user
+ mode = os.stat(self.secdir)
+ self.uid = mode[stat.ST_UID]
+ self.gid = mode[stat.ST_GID]
+
+ if fstore:
+ self.fstore = fstore
+ else:
+ self.fstore = sysrestore.FileStore('/var/lib/ipa/sysrestore')
+
+ def __del__(self):
+ shutil.rmtree(self.reqdir, ignore_errors=True)
+
+ def set_serial_from_pkcs12(self):
+ """A CA cert was loaded from a PKCS#12 file. Set up our serial file"""
+
+ self.cur_serial = self.find_cacert_serial()
+ try:
+ f=open(CA_SERIALNO,"w")
+ f.write(str(self.cur_serial))
+ f.close()
+ except IOError, e:
+ raise RuntimeError("Unable to increment serial number: %s" % str(e))
+
+ def next_serial(self):
+ try:
+ f=open(CA_SERIALNO,"r")
+ r = f.readline()
+ try:
+ self.cur_serial = int(r) + 1
+ except ValueError:
+ raise RuntimeError("The value in %s is not an integer" % CA_SERIALNO)
+ f.close()
+ except IOError, e:
+ if e.errno == errno.ENOENT:
+ self.cur_serial = 1000
+ f=open(CA_SERIALNO,"w")
+ f.write(str(self.cur_serial))
+ f.close()
+ else:
+ raise RuntimeError("Unable to determine serial number: %s" % str(e))
+
+ try:
+ f=open(CA_SERIALNO,"w")
+ f.write(str(self.cur_serial))
+ f.close()
+ except IOError, e:
+ raise RuntimeError("Unable to increment serial number: %s" % str(e))
+
+ return str(self.cur_serial)
+
+ def set_perms(self, fname, write=False):
+ os.chown(fname, self.uid, self.gid)
+ perms = stat.S_IRUSR
+ if write:
+ perms |= stat.S_IWUSR
+ os.chmod(fname, perms)
+
+ def gen_password(self):
+ return sha.sha(ipautil.ipa_generate_password()).hexdigest()
+
+ def run_certutil(self, args, stdin=None):
+ new_args = ["/usr/bin/certutil", "-d", self.secdir]
+ new_args = new_args + args
+ return ipautil.run(new_args, stdin)
+
+ def run_signtool(self, args, stdin=None):
+ new_args = ["/usr/bin/signtool", "-d", self.secdir]
+ new_args = new_args + args
+ ipautil.run(new_args, stdin)
+
+ def create_noise_file(self):
+ ipautil.backup_file(self.noise_fname)
+ f = open(self.noise_fname, "w")
+ f.write(self.gen_password())
+ self.set_perms(self.noise_fname)
+
+ def create_passwd_file(self, passwd=None):
+ ipautil.backup_file(self.passwd_fname)
+ f = open(self.passwd_fname, "w")
+ if passwd is not None:
+ f.write("%s\n" % passwd)
+ else:
+ f.write(self.gen_password())
+ f.close()
+ self.set_perms(self.passwd_fname)
+
+ def create_certdbs(self):
+ ipautil.backup_file(self.certdb_fname)
+ ipautil.backup_file(self.keydb_fname)
+ ipautil.backup_file(self.secmod_fname)
+ self.run_certutil(["-N",
+ "-f", self.passwd_fname])
+ self.set_perms(self.passwd_fname, write=True)
+
+ def create_ca_cert(self):
+ # Generate the encryption key
+ self.run_certutil(["-G", "-z", self.noise_fname, "-f", self.passwd_fname])
+ # Generate the self-signed cert
+ self.run_certutil(["-S", "-n", self.cacert_name,
+ "-s", "cn=IPA Test Certificate Authority",
+ "-x",
+ "-t", "CT,,C",
+ "-m", self.next_serial(),
+ "-v", self.valid_months,
+ "-z", self.noise_fname,
+ "-f", self.passwd_fname])
+
+ def export_ca_cert(self, nickname, create_pkcs12=False):
+ """create_pkcs12 tells us whether we should create a PKCS#12 file
+ of the CA or not. If we are running on a replica then we won't
+ have the private key to make a PKCS#12 file so we don't need to
+ do that step."""
+ # export the CA cert for use with other apps
+ ipautil.backup_file(self.cacert_fname)
+ self.run_certutil(["-L", "-n", nickname,
+ "-a",
+ "-o", self.cacert_fname])
+ self.set_perms(self.cacert_fname)
+ if create_pkcs12:
+ ipautil.backup_file(self.pk12_fname)
+ ipautil.run(["/usr/bin/pk12util", "-d", self.secdir,
+ "-o", self.pk12_fname,
+ "-n", self.cacert_name,
+ "-w", self.passwd_fname,
+ "-k", self.passwd_fname])
+ self.set_perms(self.pk12_fname)
+
+ def load_cacert(self, cacert_fname):
+ self.run_certutil(["-A", "-n", self.cacert_name,
+ "-t", "CT,,C",
+ "-a",
+ "-i", cacert_fname])
+
+ def find_cacert_serial(self):
+ (out,err) = self.run_certutil(["-L", "-n", self.cacert_name])
+ data = out.split('\n')
+ for line in data:
+ x = re.match(r'\s+Serial Number: (\d+) .*', line)
+ if x is not None:
+ return x.group(1)
+
+ raise RuntimeError("Unable to find serial number")
+
+ def create_server_cert(self, nickname, name, other_certdb=None):
+ cdb = other_certdb
+ if not cdb:
+ cdb = self
+ self.request_cert(name)
+ cdb.issue_server_cert(self.certreq_fname, self.certder_fname)
+ self.add_cert(self.certder_fname, nickname)
+ os.unlink(self.certreq_fname)
+ os.unlink(self.certder_fname)
+
+ def create_signing_cert(self, nickname, name, other_certdb=None):
+ cdb = other_certdb
+ if not cdb:
+ cdb = self
+ self.request_cert(name)
+ cdb.issue_signing_cert(self.certreq_fname, self.certder_fname)
+ self.add_cert(self.certder_fname, nickname)
+ os.unlink(self.certreq_fname)
+ os.unlink(self.certder_fname)
+
+ def request_cert(self, name):
+ self.run_certutil(["-R", "-s", name,
+ "-o", self.certreq_fname,
+ "-g", self.keysize,
+ "-z", self.noise_fname,
+ "-f", self.passwd_fname])
+
+ def issue_server_cert(self, certreq_fname, cert_fname):
+ p = subprocess.Popen(["/usr/bin/certutil",
+ "-d", self.secdir,
+ "-C", "-c", self.cacert_name,
+ "-i", certreq_fname,
+ "-o", cert_fname,
+ "-m", self.next_serial(),
+ "-v", self.valid_months,
+ "-f", self.passwd_fname,
+ "-1", "-5"],
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE)
+
+ # Bah - this sucks, but I guess it isn't possible to fully
+ # control this with command line arguments.
+ #
+ # What this is requesting is:
+ # -1 (Create key usage extension)
+ # 2 - Key encipherment
+ # 9 - done
+ # n - not critical
+ #
+ # -5 (Create netscape cert type extension)
+ # 1 - SSL Server
+ # 9 - done
+ # n - not critical
+ p.stdin.write("2\n9\nn\n1\n9\nn\n")
+ p.wait()
+
+ def issue_signing_cert(self, certreq_fname, cert_fname):
+ p = subprocess.Popen(["/usr/bin/certutil",
+ "-d", self.secdir,
+ "-C", "-c", self.cacert_name,
+ "-i", certreq_fname,
+ "-o", cert_fname,
+ "-m", self.next_serial(),
+ "-v", self.valid_months,
+ "-f", self.passwd_fname,
+ "-1", "-5"],
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE)
+
+ # Bah - this sucks, but I guess it isn't possible to fully
+ # control this with command line arguments.
+ #
+ # What this is requesting is:
+ # -1 (Create key usage extension)
+ # 0 - Digital Signature
+ # 5 - Cert signing key
+ # 9 - done
+ # n - not critical
+ #
+ # -5 (Create netscape cert type extension)
+ # 3 - Object Signing
+ # 9 - done
+ # n - not critical
+ p.stdin.write("0\n5\n9\nn\n3\n9\nn\n")
+ p.wait()
+
+ def add_cert(self, cert_fname, nickname):
+ self.run_certutil(["-A", "-n", nickname,
+ "-t", "u,u,u",
+ "-i", cert_fname,
+ "-f", cert_fname])
+
+ def create_pin_file(self):
+ ipautil.backup_file(self.pin_fname)
+ f = open(self.pin_fname, "w")
+ f.write("Internal (Software) Token:")
+ pwd = open(self.passwd_fname)
+ f.write(pwd.read())
+ f.close()
+ self.set_perms(self.pin_fname)
+
+ def find_root_cert(self, nickname):
+ p = subprocess.Popen(["/usr/bin/certutil", "-d", self.secdir,
+ "-O", "-n", nickname], stdout=subprocess.PIPE)
+
+ chain = p.stdout.read()
+ chain = chain.split("\n")
+
+ root_nickname = re.match('\ *"(.*)".*', chain[0]).groups()[0]
+
+ return root_nickname
+
+ def trust_root_cert(self, nickname):
+ root_nickname = self.find_root_cert(nickname)
+
+ self.run_certutil(["-M", "-n", root_nickname,
+ "-t", "CT,CT,"])
+
+ def find_server_certs(self):
+ p = subprocess.Popen(["/usr/bin/certutil", "-d", self.secdir,
+ "-L"], stdout=subprocess.PIPE)
+
+ certs = p.stdout.read()
+
+ certs = certs.split("\n")
+
+ server_certs = []
+
+ for cert in certs:
+ fields = cert.split()
+ if not len(fields):
+ continue
+ flags = fields[-1]
+ if 'u' in flags:
+ name = " ".join(fields[0:-1])
+ # NSS 3.12 added a header to the certutil output
+ if name == "Certificate Nickname Trust":
+ continue
+ server_certs.append((name, flags))
+
+ return server_certs
+
+ def import_pkcs12(self, pkcs12_fname, passwd_fname=None):
+ args = ["/usr/bin/pk12util", "-d", self.secdir,
+ "-i", pkcs12_fname,
+ "-k", self.passwd_fname]
+ if passwd_fname:
+ args = args + ["-w", passwd_fname]
+ try:
+ ipautil.run(args)
+ except ipautil.CalledProcessError, e:
+ if e.returncode == 17:
+ raise RuntimeError("incorrect password")
+ else:
+ raise RuntimeError("unknown error import pkcs#12 file")
+
+ def export_pkcs12(self, pkcs12_fname, pkcs12_pwd_fname, nickname="CA certificate"):
+ ipautil.run(["/usr/bin/pk12util", "-d", self.secdir,
+ "-o", pkcs12_fname,
+ "-n", nickname,
+ "-k", self.passwd_fname,
+ "-w", pkcs12_pwd_fname])
+
+ def create_self_signed(self, passwd=None):
+ self.create_noise_file()
+ self.create_passwd_file(passwd)
+ self.create_certdbs()
+ self.create_ca_cert()
+ self.export_ca_cert(self.cacert_name, True)
+ self.create_pin_file()
+
+ def create_from_cacert(self, cacert_fname, passwd=""):
+ self.create_noise_file()
+ self.create_passwd_file(passwd)
+ self.create_certdbs()
+ self.load_cacert(cacert_fname)
+
+ def create_from_pkcs12(self, pkcs12_fname, pkcs12_pwd_fname, passwd=None):
+ """Create a new NSS database using the certificates in a PKCS#12 file.
+
+ pkcs12_fname: the filename of the PKCS#12 file
+ pkcs12_pwd_fname: the file containing the pin for the PKCS#12 file
+ nickname: the nickname/friendly-name of the cert we are loading
+ passwd: The password to use for the new NSS database we are creating
+ """
+ self.create_noise_file()
+ self.create_passwd_file(passwd)
+ self.create_certdbs()
+ self.import_pkcs12(pkcs12_fname, pkcs12_pwd_fname)
+ server_certs = self.find_server_certs()
+ if len(server_certs) == 0:
+ raise RuntimeError("Could not find a suitable server cert in import in %s" % pkcs12_fname)
+
+ # We only handle one server cert
+ nickname = server_certs[0][0]
+
+ self.cacert_name = self.find_root_cert(nickname)
+ self.trust_root_cert(nickname)
+ self.create_pin_file()
+ self.export_ca_cert(self.cacert_name, False)
+
+ # This file implies that we have our own self-signed CA. Ensure
+ # that it no longer exists (from previous installs, for example).
+ try:
+ os.remove(CA_SERIALNO)
+ except:
+ pass
+
+ def backup_files(self):
+ self.fstore.backup_file(self.noise_fname)
+ self.fstore.backup_file(self.passwd_fname)
+ self.fstore.backup_file(self.certdb_fname)
+ self.fstore.backup_file(self.keydb_fname)
+ self.fstore.backup_file(self.secmod_fname)
+ self.fstore.backup_file(self.cacert_fname)
+ self.fstore.backup_file(self.pk12_fname)
+ self.fstore.backup_file(self.pin_fname)
+ self.fstore.backup_file(self.certreq_fname)
+ self.fstore.backup_file(self.certder_fname)
diff --git a/ipa-server/ipaserver/dsinstance.py b/ipa-server/ipaserver/dsinstance.py
new file mode 100644
index 00000000..e9826bf6
--- /dev/null
+++ b/ipa-server/ipaserver/dsinstance.py
@@ -0,0 +1,479 @@
+# Authors: Karl MacMillan <kmacmillan@mentalrootkit.com>
+# Simo Sorce <ssorce@redhat.com>
+#
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+import shutil
+import logging
+import pwd
+import glob
+import sys
+import os
+import re
+import time
+import tempfile
+import stat
+
+from ipa import ipautil
+
+import service
+import installutils
+import certs
+import ipaldap, ldap
+from ipaserver import ldapupdate
+
+SERVER_ROOT_64 = "/usr/lib64/dirsrv"
+SERVER_ROOT_32 = "/usr/lib/dirsrv"
+
+def realm_to_suffix(realm_name):
+ s = realm_name.split(".")
+ terms = ["dc=" + x.lower() for x in s]
+ return ",".join(terms)
+
+def find_server_root():
+ if ipautil.dir_exists(SERVER_ROOT_64):
+ return SERVER_ROOT_64
+ else:
+ return SERVER_ROOT_32
+
+def realm_to_serverid(realm_name):
+ return "-".join(realm_name.split("."))
+
+def config_dirname(serverid):
+ return "/etc/dirsrv/slapd-" + serverid + "/"
+
+def schema_dirname(serverid):
+ return config_dirname(serverid) + "/schema/"
+
+def erase_ds_instance_data(serverid):
+ try:
+ shutil.rmtree("/etc/dirsrv/slapd-%s" % serverid)
+ except:
+ pass
+ try:
+ shutil.rmtree("/usr/lib/dirsrv/slapd-%s" % serverid)
+ except:
+ pass
+ try:
+ shutil.rmtree("/usr/lib64/dirsrv/slapd-%s" % serverid)
+ except:
+ pass
+ try:
+ shutil.rmtree("/var/lib/dirsrv/slapd-%s" % serverid)
+ except:
+ pass
+ try:
+ shutil.rmtree("/var/lock/dirsrv/slapd-%s" % serverid)
+ except:
+ pass
+# try:
+# shutil.rmtree("/var/log/dirsrv/slapd-%s" % serverid)
+# except:
+# pass
+
+def check_existing_installation():
+ dirs = glob.glob("/etc/dirsrv/slapd-*")
+ if not dirs:
+ return []
+
+ serverids = []
+ for d in dirs:
+ serverids.append(os.path.basename(d).split("slapd-", 1)[1])
+
+ return serverids
+
+def check_ports():
+ ds_unsecure = installutils.port_available(389)
+ ds_secure = installutils.port_available(636)
+ return (ds_unsecure, ds_secure)
+
+def is_ds_running():
+ """The DS init script always returns 0 when requesting status so it cannot
+ be used to determine if the server is running. We have to look at the
+ output.
+ """
+ ret = True
+ try:
+ (sout, serr) = ipautil.run(["/sbin/service", "dirsrv", "status"])
+ if sout.find("is stopped") >= 0:
+ ret = False
+ except ipautil.CalledProcessError:
+ ret = False
+ return ret
+
+
+INF_TEMPLATE = """
+[General]
+FullMachineName= $FQHN
+SuiteSpotUserID= $USER
+ServerRoot= $SERVER_ROOT
+[slapd]
+ServerPort= 389
+ServerIdentifier= $SERVERID
+Suffix= $SUFFIX
+RootDN= cn=Directory Manager
+RootDNPwd= $PASSWORD
+InstallLdifFile= /var/lib/dirsrv/boot.ldif
+"""
+
+BASE_TEMPLATE = """
+dn: $SUFFIX
+objectClass: top
+objectClass: domain
+objectClass: pilotObject
+dc: $BASEDC
+info: IPA V1.0
+"""
+
+class DsInstance(service.Service):
+ def __init__(self, realm_name=None, domain_name=None, dm_password=None):
+ service.Service.__init__(self, "dirsrv")
+ self.realm_name = realm_name
+ self.dm_password = dm_password
+ self.sub_dict = None
+ self.domain = domain_name
+ self.serverid = None
+ self.host_name = None
+ self.pkcs12_info = None
+ self.ds_user = None
+ if realm_name:
+ self.suffix = realm_to_suffix(self.realm_name)
+ self.__setup_sub_dict()
+ else:
+ self.suffix = None
+
+ def create_instance(self, ds_user, realm_name, host_name, domain_name, dm_password, pkcs12_info=None):
+ self.ds_user = ds_user
+ self.realm_name = realm_name.upper()
+ self.serverid = realm_to_serverid(self.realm_name)
+ self.suffix = realm_to_suffix(self.realm_name)
+ self.host_name = host_name
+ self.dm_password = dm_password
+ self.domain = domain_name
+ self.pkcs12_info = pkcs12_info
+ self.__setup_sub_dict()
+
+ self.step("creating directory server user", self.__create_ds_user)
+ self.step("creating directory server instance", self.__create_instance)
+ self.step("adding default schema", self.__add_default_schemas)
+ self.step("enabling memberof plugin", self.__add_memberof_module)
+ self.step("enabling referential integrity plugin", self.__add_referint_module)
+ self.step("enabling distributed numeric assignment plugin", self.__add_dna_module)
+ self.step("enabling winsync plugin", self.__add_winsync_module)
+ self.step("configuring uniqueness plugin", self.__set_unique_attrs)
+ self.step("creating indices", self.__create_indices)
+ self.step("configuring ssl for ds instance", self.__enable_ssl)
+ self.step("configuring certmap.conf", self.__certmap_conf)
+ self.step("restarting directory server", self.__restart_instance)
+ self.step("adding default layout", self.__add_default_layout)
+ self.step("configuring Posix uid/gid generation as first master",
+ self.__config_uidgid_gen_first_master)
+ self.step("adding master entry as first master",
+ self.__add_master_entry_first_master)
+ self.step("initializing group membership",
+ self.init_memberof)
+
+ self.step("configuring directory to start on boot", self.__enable)
+
+ self.start_creation("Configuring directory server:")
+
+ def __enable(self):
+ self.backup_state("enabled", self.is_enabled())
+ self.chkconfig_on()
+
+ def __setup_sub_dict(self):
+ server_root = find_server_root()
+ self.sub_dict = dict(FQHN=self.host_name, SERVERID=self.serverid,
+ PASSWORD=self.dm_password, SUFFIX=self.suffix.lower(),
+ REALM=self.realm_name, USER=self.ds_user,
+ SERVER_ROOT=server_root, DOMAIN=self.domain,
+ TIME=int(time.time()))
+
+ def __create_ds_user(self):
+ user_exists = True
+ try:
+ pwd.getpwnam(self.ds_user)
+ logging.debug("ds user %s exists" % self.ds_user)
+ except KeyError:
+ user_exists = False
+ logging.debug("adding ds user %s" % self.ds_user)
+ args = ["/usr/sbin/useradd", "-c", "DS System User", "-d", "/var/lib/dirsrv", "-M", "-r", "-s", "/sbin/nologin", self.ds_user]
+ try:
+ ipautil.run(args)
+ logging.debug("done adding user")
+ except ipautil.CalledProcessError, e:
+ logging.critical("failed to add user %s" % e)
+
+ self.backup_state("user", self.ds_user)
+ self.backup_state("user_exists", user_exists)
+
+ def __create_instance(self):
+ self.backup_state("running", is_ds_running())
+ self.backup_state("serverid", self.serverid)
+
+ self.sub_dict['BASEDC'] = self.realm_name.split('.')[0].lower()
+ base_txt = ipautil.template_str(BASE_TEMPLATE, self.sub_dict)
+ logging.debug(base_txt)
+ base_fd = file("/var/lib/dirsrv/boot.ldif", "w")
+ base_fd.write(base_txt)
+ base_fd.flush()
+ base_fd.close()
+
+ inf_txt = ipautil.template_str(INF_TEMPLATE, self.sub_dict)
+ logging.debug("writing inf template")
+ inf_fd = ipautil.write_tmp_file(inf_txt)
+ inf_txt = re.sub(r"RootDNPwd=.*\n", "", inf_txt)
+ logging.debug(inf_txt)
+ if ipautil.file_exists("/usr/sbin/setup-ds.pl"):
+ args = ["/usr/sbin/setup-ds.pl", "--silent", "--logfile", "-", "-f", inf_fd.name]
+ logging.debug("calling setup-ds.pl")
+ else:
+ args = ["/usr/bin/ds_newinst.pl", inf_fd.name]
+ logging.debug("calling ds_newinst.pl")
+ try:
+ ipautil.run(args)
+ logging.debug("completed creating ds instance")
+ except ipautil.CalledProcessError, e:
+ logging.critical("failed to restart ds instance %s" % e)
+ logging.debug("restarting ds instance")
+ try:
+ self.restart()
+ logging.debug("done restarting ds instance")
+ except ipautil.CalledProcessError, e:
+ print "failed to restart ds instance", e
+ logging.debug("failed to restart ds instance %s" % e)
+ inf_fd.close()
+ os.remove("/var/lib/dirsrv/boot.ldif")
+
+ def __add_default_schemas(self):
+ shutil.copyfile(ipautil.SHARE_DIR + "60kerberos.ldif",
+ schema_dirname(self.serverid) + "60kerberos.ldif")
+ shutil.copyfile(ipautil.SHARE_DIR + "60samba.ldif",
+ schema_dirname(self.serverid) + "60samba.ldif")
+ shutil.copyfile(ipautil.SHARE_DIR + "60radius.ldif",
+ schema_dirname(self.serverid) + "60radius.ldif")
+ shutil.copyfile(ipautil.SHARE_DIR + "60ipaconfig.ldif",
+ schema_dirname(self.serverid) + "60ipaconfig.ldif")
+
+ def __restart_instance(self):
+ try:
+ self.restart()
+ if not is_ds_running():
+ logging.critical("Failed to restart the directory server. See the installation log for details.")
+ sys.exit(1)
+ except SystemExit, e:
+ raise e
+ except Exception, e:
+ # TODO: roll back here?
+ logging.critical("Failed to restart the directory server. See the installation log for details.")
+
+ def __ldap_mod(self, ldif, sub_dict = None):
+ fd = None
+ path = ipautil.SHARE_DIR + ldif
+
+ if not sub_dict is None:
+ txt = ipautil.template_file(path, sub_dict)
+ fd = ipautil.write_tmp_file(txt)
+ path = fd.name
+
+ [pw_fd, pw_name] = tempfile.mkstemp()
+ os.write(pw_fd, self.dm_password)
+ os.close(pw_fd)
+
+ args = ["/usr/bin/ldapmodify", "-h", "127.0.0.1", "-xv",
+ "-D", "cn=Directory Manager", "-y", pw_name, "-f", path]
+
+ try:
+ try:
+ ipautil.run(args)
+ except ipautil.CalledProcessError, e:
+ logging.critical("Failed to load %s: %s" % (ldif, str(e)))
+ finally:
+ os.remove(pw_name)
+
+ if not fd is None:
+ fd.close()
+
+ def __add_memberof_module(self):
+ self.__ldap_mod("memberof-conf.ldif")
+
+ def init_memberof(self):
+ self.__ldap_mod("memberof-task.ldif", self.sub_dict)
+
+ def apply_updates(self):
+ ld = ldapupdate.LDAPUpdate(dm_password=self.dm_password)
+ files = ld.get_all_files(ldapupdate.UPDATES_DIR)
+ ld.update(files)
+
+ def __add_referint_module(self):
+ self.__ldap_mod("referint-conf.ldif")
+
+ def __add_dna_module(self):
+ self.__ldap_mod("dna-conf.ldif")
+
+ def __set_unique_attrs(self):
+ self.__ldap_mod("unique-attributes.ldif", self.sub_dict)
+
+ def __config_uidgid_gen_first_master(self):
+ self.__ldap_mod("dna-posix.ldif", self.sub_dict)
+
+ def __add_master_entry_first_master(self):
+ self.__ldap_mod("master-entry.ldif", self.sub_dict)
+
+ def __add_winsync_module(self):
+ self.__ldap_mod("ipa-winsync-conf.ldif")
+
+ def __enable_ssl(self):
+ dirname = config_dirname(self.serverid)
+ ca = certs.CertDB(dirname)
+ if self.pkcs12_info:
+ ca.create_from_pkcs12(self.pkcs12_info[0], self.pkcs12_info[1])
+ server_certs = ca.find_server_certs()
+ if len(server_certs) == 0:
+ raise RuntimeError("Could not find a suitable server cert in import in %s" % pkcs12_info[0])
+
+ # We only handle one server cert
+ nickname = server_certs[0][0]
+ else:
+ ca.create_self_signed()
+ ca.create_server_cert("Server-Cert", "cn=%s,ou=Fedora Directory Server" % self.host_name)
+ nickname = "Server-Cert"
+
+ conn = ipaldap.IPAdmin("127.0.0.1")
+ conn.simple_bind_s("cn=directory manager", self.dm_password)
+
+ mod = [(ldap.MOD_REPLACE, "nsSSLClientAuth", "allowed"),
+ (ldap.MOD_REPLACE, "nsSSL3Ciphers",
+ "-rsa_null_md5,+rsa_rc4_128_md5,+rsa_rc4_40_md5,+rsa_rc2_40_md5,\
++rsa_des_sha,+rsa_fips_des_sha,+rsa_3des_sha,+rsa_fips_3des_sha,+fortezza,\
++fortezza_rc4_128_sha,+fortezza_null,+tls_rsa_export1024_with_rc4_56_sha,\
++tls_rsa_export1024_with_des_cbc_sha")]
+ conn.modify_s("cn=encryption,cn=config", mod)
+
+ mod = [(ldap.MOD_ADD, "nsslapd-security", "on"),
+ (ldap.MOD_REPLACE, "nsslapd-ssl-check-hostname", "off")]
+ conn.modify_s("cn=config", mod)
+
+ entry = ipaldap.Entry("cn=RSA,cn=encryption,cn=config")
+
+ entry.setValues("objectclass", "top", "nsEncryptionModule")
+ entry.setValues("cn", "RSA")
+ entry.setValues("nsSSLPersonalitySSL", nickname)
+ entry.setValues("nsSSLToken", "internal (software)")
+ entry.setValues("nsSSLActivation", "on")
+
+ conn.addEntry(entry)
+
+ conn.unbind()
+
+ def __add_default_layout(self):
+ self.__ldap_mod("bootstrap-template.ldif", self.sub_dict)
+
+ def __create_indices(self):
+ self.__ldap_mod("indices.ldif")
+
+ def __certmap_conf(self):
+ shutil.copyfile(ipautil.SHARE_DIR + "certmap.conf.template",
+ config_dirname(self.serverid) + "certmap.conf")
+
+ def change_admin_password(self, password):
+ logging.debug("Changing admin password")
+ dirname = config_dirname(self.serverid)
+ if ipautil.dir_exists("/usr/lib64/mozldap"):
+ app = "/usr/lib64/mozldap/ldappasswd"
+ else:
+ app = "/usr/lib/mozldap/ldappasswd"
+ args = [app,
+ "-D", "cn=Directory Manager", "-w", self.dm_password,
+ "-P", dirname+"/cert8.db", "-ZZZ", "-s", password,
+ "uid=admin,cn=users,cn=accounts,"+self.suffix]
+ try:
+ ipautil.run(args)
+ logging.debug("ldappasswd done")
+ except ipautil.CalledProcessError, e:
+ print "Unable to set admin password", e
+ logging.debug("Unable to set admin password %s" % e)
+
+ def uninstall(self):
+ running = self.restore_state("running")
+ enabled = self.restore_state("enabled")
+
+ if not running is None:
+ self.stop()
+
+ if not enabled is None and not enabled:
+ self.chkconfig_off()
+
+ serverid = self.restore_state("serverid")
+ if not serverid is None:
+ erase_ds_instance_data(serverid)
+
+ ds_user = self.restore_state("user")
+ user_exists = self.restore_state("user_exists")
+
+ if not ds_user is None and not user_exists is None and not user_exists:
+ try:
+ ipautil.run(["/usr/sbin/userdel", ds_user])
+ except ipautil.CalledProcessError, e:
+ logging.critical("failed to delete user %s" % e)
+
+ if self.restore_state("running"):
+ self.start()
+
+ # we could probably move this function into the service.Service
+ # class - it's very generic - all we need is a way to get an
+ # instance of a particular Service
+ def add_ca_cert(self, cacert_fname, cacert_name=''):
+ """Add a CA certificate to the directory server cert db. We
+ first have to shut down the directory server in case it has
+ opened the cert db read-only. Then we use the CertDB class
+ to add the CA cert. We have to provide a nickname, and we
+ do not use 'CA certificate' since that's the default, so
+ we use 'Imported CA' if none specified. Then we restart
+ the server."""
+ # first make sure we have a valid cacert_fname
+ try:
+ if not os.access(cacert_fname, os.R_OK):
+ logging.critical("The given CA cert file named [%s] could not be read" %
+ cacert_fname)
+ return False
+ except OSError, e:
+ logging.critical("The given CA cert file named [%s] could not be read: %s" %
+ (cacert_fname, str(e)))
+ return False
+ # ok - ca cert file can be read
+ # shutdown the server
+ self.stop()
+
+ dirname = config_dirname(realm_to_serverid(self.realm_name))
+ certdb = certs.CertDB(dirname)
+ if not cacert_name or len(cacert_name) == 0:
+ cacert_name = "Imported CA"
+ # we can't pass in the nickname, so we set the instance variable
+ certdb.cacert_name = cacert_name
+ status = True
+ try:
+ certdb.load_cacert(cacert_fname)
+ except ipalib.CalledProcessError, e:
+ logging.critical("Error importing CA cert file named [%s]: %s" %
+ (cacert_fname, str(e)))
+ status = False
+ # restart the directory server
+ self.start()
+
+ return status
diff --git a/ipa-server/ipaserver/httpinstance.py b/ipa-server/ipaserver/httpinstance.py
new file mode 100644
index 00000000..f5a903b3
--- /dev/null
+++ b/ipa-server/ipaserver/httpinstance.py
@@ -0,0 +1,231 @@
+# Authors: Rob Crittenden <rcritten@redhat.com>
+#
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+import os
+import os.path
+import subprocess
+import string
+import tempfile
+import logging
+import pwd
+import fileinput
+import sys
+import shutil
+
+import service
+import certs
+import dsinstance
+import installutils
+from ipa import sysrestore
+from ipa import ipautil
+
+HTTPD_DIR = "/etc/httpd"
+SSL_CONF = HTTPD_DIR + "/conf.d/ssl.conf"
+NSS_CONF = HTTPD_DIR + "/conf.d/nss.conf"
+NSS_DIR = HTTPD_DIR + "/alias"
+
+selinux_warning = """WARNING: could not set selinux boolean httpd_can_network_connect to true.
+The web interface may not function correctly until this boolean is
+successfully change with the command:
+ /usr/sbin/setsebool -P httpd_can_network_connect true
+Try updating the policycoreutils and selinux-policy packages.
+"""
+
+class WebGuiInstance(service.SimpleServiceInstance):
+ def __init__(self):
+ service.SimpleServiceInstance.__init__(self, "ipa_webgui")
+
+class HTTPInstance(service.Service):
+ def __init__(self, fstore = None):
+ service.Service.__init__(self, "httpd")
+ if fstore:
+ self.fstore = fstore
+ else:
+ self.fstore = sysrestore.FileStore('/var/lib/ipa/sysrestore')
+
+ def create_instance(self, realm, fqdn, domain_name, autoconfig=True, pkcs12_info=None):
+ self.fqdn = fqdn
+ self.realm = realm
+ self.domain = domain_name
+ self.pkcs12_info = pkcs12_info
+ self.sub_dict = { "REALM" : realm, "FQDN": fqdn, "DOMAIN" : self.domain }
+
+ self.step("disabling mod_ssl in httpd", self.__disable_mod_ssl)
+ self.step("Setting mod_nss port to 443", self.__set_mod_nss_port)
+ self.step("Adding URL rewriting rules", self.__add_include)
+ self.step("configuring httpd", self.__configure_http)
+ self.step("creating a keytab for httpd", self.__create_http_keytab)
+ self.step("Setting up ssl", self.__setup_ssl)
+ if autoconfig:
+ self.step("Setting up browser autoconfig", self.__setup_autoconfig)
+ self.step("configuring SELinux for httpd", self.__selinux_config)
+ self.step("restarting httpd", self.__start)
+ self.step("configuring httpd to start on boot", self.__enable)
+
+ self.start_creation("Configuring the web interface")
+
+ def __start(self):
+ self.backup_state("running", self.is_running())
+ self.restart()
+
+ def __enable(self):
+ self.backup_state("enabled", self.is_running())
+ self.chkconfig_on()
+
+ def __selinux_config(self):
+ selinux=0
+ try:
+ if (os.path.exists('/usr/sbin/selinuxenabled')):
+ ipautil.run(["/usr/sbin/selinuxenabled"])
+ selinux=1
+ except ipautil.CalledProcessError:
+ # selinuxenabled returns 1 if not enabled
+ pass
+
+ if selinux:
+ try:
+ # returns e.g. "httpd_can_network_connect --> off"
+ (stdout, stderr) = ipautils.run(["/usr/sbin/getsebool",
+ "httpd_can_network_connect"])
+ self.backup_state("httpd_can_network_connect", stdout.split()[2])
+ except:
+ pass
+
+ # Allow apache to connect to the turbogears web gui
+ # This can still fail even if selinux is enabled
+ try:
+ ipautil.run(["/usr/sbin/setsebool", "-P", "httpd_can_network_connect", "true"])
+ except:
+ self.print_msg(selinux_warning)
+
+ def __create_http_keytab(self):
+ http_principal = "HTTP/" + self.fqdn + "@" + self.realm
+ installutils.kadmin_addprinc(http_principal)
+ installutils.create_keytab("/etc/httpd/conf/ipa.keytab", http_principal)
+
+ pent = pwd.getpwnam("apache")
+ os.chown("/etc/httpd/conf/ipa.keytab", pent.pw_uid, pent.pw_gid)
+
+ def __configure_http(self):
+ http_txt = ipautil.template_file(ipautil.SHARE_DIR + "ipa.conf", self.sub_dict)
+ self.fstore.backup_file("/etc/httpd/conf.d/ipa.conf")
+ http_fd = open("/etc/httpd/conf.d/ipa.conf", "w")
+ http_fd.write(http_txt)
+ http_fd.close()
+
+ http_txt = ipautil.template_file(ipautil.SHARE_DIR + "ipa-rewrite.conf", self.sub_dict)
+ self.fstore.backup_file("/etc/httpd/conf.d/ipa-rewrite.conf")
+ http_fd = open("/etc/httpd/conf.d/ipa-rewrite.conf", "w")
+ http_fd.write(http_txt)
+ http_fd.close()
+
+ def __disable_mod_ssl(self):
+ if os.path.exists(SSL_CONF):
+ self.fstore.backup_file(SSL_CONF)
+ os.unlink(SSL_CONF)
+
+ def __set_mod_nss_port(self):
+ self.fstore.backup_file(NSS_CONF)
+ if installutils.update_file(NSS_CONF, '8443', '443') != 0:
+ print "Updating port in %s failed." % NSS_CONF
+
+ def __set_mod_nss_nickname(self, nickname):
+ installutils.set_directive(NSS_CONF, 'NSSNickname', nickname)
+
+ def __add_include(self):
+ """This should run after __set_mod_nss_port so is already backed up"""
+ if installutils.update_file(NSS_CONF, '</VirtualHost>', 'Include conf.d/ipa-rewrite.conf\n</VirtualHost>') != 0:
+ print "Adding Include conf.d/ipa-rewrite to %s failed." % NSS_CONF
+
+ def __setup_ssl(self):
+ ds_ca = certs.CertDB(dsinstance.config_dirname(dsinstance.realm_to_serverid(self.realm)))
+ ca = certs.CertDB(NSS_DIR)
+ if self.pkcs12_info:
+ ca.create_from_pkcs12(self.pkcs12_info[0], self.pkcs12_info[1], passwd="")
+ server_certs = ca.find_server_certs()
+ if len(server_certs) == 0:
+ raise RuntimeError("Could not find a suitable server cert in import in %s" % pkcs12_info[0])
+
+ # We only handle one server cert
+ nickname = server_certs[0][0]
+
+ self.__set_mod_nss_nickname(nickname)
+ else:
+ ca.create_from_cacert(ds_ca.cacert_fname)
+ ca.create_server_cert("Server-Cert", "cn=%s,ou=Apache Web Server" % self.fqdn, ds_ca)
+ ca.create_signing_cert("Signing-Cert", "cn=%s,ou=Signing Certificate,o=Identity Policy Audit" % self.fqdn, ds_ca)
+
+ # Fix the database permissions
+ os.chmod(NSS_DIR + "/cert8.db", 0640)
+ os.chmod(NSS_DIR + "/key3.db", 0640)
+ os.chmod(NSS_DIR + "/secmod.db", 0640)
+
+ pent = pwd.getpwnam("apache")
+ os.chown(NSS_DIR + "/cert8.db", 0, pent.pw_gid )
+ os.chown(NSS_DIR + "/key3.db", 0, pent.pw_gid )
+ os.chown(NSS_DIR + "/secmod.db", 0, pent.pw_gid )
+
+ def __setup_autoconfig(self):
+ prefs_txt = ipautil.template_file(ipautil.SHARE_DIR + "preferences.html.template", self.sub_dict)
+ prefs_fd = open("/usr/share/ipa/html/preferences.html", "w")
+ prefs_fd.write(prefs_txt)
+ prefs_fd.close()
+
+ # The signing cert is generated in __setup_ssl
+ ds_ca = certs.CertDB(dsinstance.config_dirname(dsinstance.realm_to_serverid(self.realm)))
+ ca = certs.CertDB(NSS_DIR)
+
+ # Publish the CA certificate
+ shutil.copy(ds_ca.cacert_fname, "/usr/share/ipa/html/ca.crt")
+ os.chmod("/usr/share/ipa/html/ca.crt", 0444)
+
+ tmpdir = tempfile.mkdtemp(prefix = "tmp-")
+ shutil.copy("/usr/share/ipa/html/preferences.html", tmpdir)
+ ca.run_signtool(["-k", "Signing-Cert",
+ "-Z", "/usr/share/ipa/html/configure.jar",
+ "-e", ".html",
+ tmpdir])
+ shutil.rmtree(tmpdir)
+
+ def uninstall(self):
+ running = self.restore_state("running")
+ enabled = self.restore_state("enabled")
+
+ if not running is None:
+ self.stop()
+
+ if not enabled is None and not enabled:
+ self.chkconfig_off()
+
+ for f in ["/etc/httpd/conf.d/ipa.conf", SSL_CONF, NSS_CONF]:
+ try:
+ self.fstore.restore_file(f)
+ except ValueError, error:
+ logging.debug(error)
+ pass
+
+ sebool_state = self.restore_state("httpd_can_network_connect")
+ if not sebool_state is None:
+ try:
+ ipautil.run(["/usr/sbin/setsebool", "-P", "httpd_can_network_connect", sebool_state])
+ except:
+ self.print_msg(selinux_warning)
+
+ if not running is None and running:
+ self.start()
diff --git a/ipa-server/ipaserver/installutils.py b/ipa-server/ipaserver/installutils.py
new file mode 100644
index 00000000..563b168e
--- /dev/null
+++ b/ipa-server/ipaserver/installutils.py
@@ -0,0 +1,248 @@
+# Authors: Simo Sorce <ssorce@redhat.com>
+#
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+import logging
+import socket
+import errno
+import getpass
+import os
+import re
+import fileinput
+import sys
+import time
+import struct
+import fcntl
+
+from ipa import ipautil
+from ipa import dnsclient
+
+def get_fqdn():
+ fqdn = ""
+ try:
+ fqdn = socket.getfqdn()
+ except:
+ try:
+ fqdn = socket.gethostname()
+ except:
+ fqdn = ""
+ return fqdn
+
+def verify_fqdn(host_name,no_host_dns=False):
+
+ if len(host_name.split(".")) < 2 or host_name == "localhost.localdomain":
+ raise RuntimeError("Invalid hostname: " + host_name)
+
+ try:
+ hostaddr = socket.getaddrinfo(host_name, None)
+ except:
+ raise RuntimeError("Unable to resolve host name, check /etc/hosts or DNS name resolution")
+
+ if len(hostaddr) == 0:
+ raise RuntimeError("Unable to resolve host name, check /etc/hosts or DNS name resolution")
+
+ for a in hostaddr:
+ if a[4][0] == '127.0.0.1' or a[4][0] == '::1':
+ raise RuntimeError("The IPA Server hostname cannot resolve to localhost (%s). A routable IP address must be used. Check /etc/hosts to see if %s is an alias for %s" % (a[4][0], host_name, a[4][0]))
+ try:
+ revname = socket.gethostbyaddr(a[4][0])[0]
+ except:
+ raise RuntimeError("Unable to resolve the reverse ip address, check /etc/hosts or DNS name resolution")
+ if revname != host_name:
+ raise RuntimeError("The host name %s does not match the reverse lookup %s" % (host_name, revname))
+
+ if no_host_dns:
+ print "Warning: skipping DNS resolution of host", host_name
+ return
+
+ # Verify this is NOT a CNAME
+ rs = dnsclient.query(host_name+".", dnsclient.DNS_C_IN, dnsclient.DNS_T_CNAME)
+ if len(rs) != 0:
+ for rsn in rs:
+ if rsn.dns_type == dnsclient.DNS_T_CNAME:
+ raise RuntimeError("The IPA Server Hostname cannot be a CNAME, only A names are allowed.")
+
+ # Verify that it is a DNS A record
+ rs = dnsclient.query(host_name+".", dnsclient.DNS_C_IN, dnsclient.DNS_T_A)
+ if len(rs) == 0:
+ print "Warning: Hostname (%s) not found in DNS" % host_name
+ return
+
+ rec = None
+ for rsn in rs:
+ if rsn.dns_type == dnsclient.DNS_T_A:
+ rec = rsn
+ break
+
+ if rec == None:
+ print "Warning: Hostname (%s) not found in DNS" % host_name
+ return
+
+ # Compare the forward and reverse
+ forward = rec.dns_name
+
+ addr = socket.inet_ntoa(struct.pack('<L',rec.rdata.address))
+ ipaddr = socket.inet_ntoa(struct.pack('!L',rec.rdata.address))
+
+ addr = addr + ".in-addr.arpa."
+ rs = dnsclient.query(addr, dnsclient.DNS_C_IN, dnsclient.DNS_T_PTR)
+ if len(rs) == 0:
+ raise RuntimeError("Cannot find Reverse Address for %s (%s)" % (host_name, addr))
+
+ rev = None
+ for rsn in rs:
+ if rsn.dns_type == dnsclient.DNS_T_PTR:
+ rev = rsn
+ break
+
+ if rev == None:
+ raise RuntimeError("Cannot find Reverse Address for %s (%s)" % (host_name, addr))
+
+ reverse = rev.rdata.ptrdname
+
+ if forward != reverse:
+ raise RuntimeError("The DNS forward record %s does not match the reverse address %s" % (forward, reverse))
+
+def port_available(port):
+ """Try to bind to a port on the wildcard host
+ Return 1 if the port is available
+ Return 0 if the port is in use
+ """
+ rv = 1
+
+ try:
+ s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ fcntl.fcntl(s, fcntl.F_SETFD, fcntl.FD_CLOEXEC)
+ s.bind(('', port))
+ s.close()
+ except socket.error, e:
+ if e[0] == errno.EADDRINUSE:
+ rv = 0
+
+ if rv:
+ try:
+ s = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
+ fcntl.fcntl(s, fcntl.F_SETFD, fcntl.FD_CLOEXEC)
+ s.bind(('', port))
+ s.close()
+ except socket.error, e:
+ if e[0] == errno.EADDRINUSE:
+ rv = 0
+
+ return rv
+
+def standard_logging_setup(log_filename, debug=False):
+ old_umask = os.umask(077)
+ # Always log everything (i.e., DEBUG) to the log
+ # file.
+ logging.basicConfig(level=logging.DEBUG,
+ format='%(asctime)s %(levelname)s %(message)s',
+ filename=log_filename,
+ filemode='w')
+ os.umask(old_umask)
+
+ console = logging.StreamHandler()
+ # If the debug option is set, also log debug messages to the console
+ if debug:
+ console.setLevel(logging.DEBUG)
+ else:
+ # Otherwise, log critical and error messages
+ console.setLevel(logging.ERROR)
+ formatter = logging.Formatter('%(name)-12s: %(levelname)-8s %(message)s')
+ console.setFormatter(formatter)
+ logging.getLogger('').addHandler(console)
+
+def get_password(prompt):
+ if os.isatty(sys.stdin.fileno()):
+ return getpass.getpass(prompt)
+ else:
+ return sys.stdin.readline().rstrip()
+
+def read_password(user, confirm=True, validate=True):
+ correct = False
+ pwd = ""
+ while not correct:
+ pwd = get_password(user + " password: ")
+ if not pwd:
+ continue
+ if validate and len(pwd) < 8:
+ print "Password must be at least 8 characters long"
+ continue
+ if not confirm:
+ correct = True
+ continue
+ pwd_confirm = get_password("Password (confirm): ")
+ if pwd != pwd_confirm:
+ print "Password mismatch!"
+ print ""
+ else:
+ correct = True
+ print ""
+ return pwd
+
+def update_file(filename, orig, subst):
+ if os.path.exists(filename):
+ pattern = "%s" % re.escape(orig)
+ p = re.compile(pattern)
+ for line in fileinput.input(filename, inplace=1):
+ if not p.search(line):
+ sys.stdout.write(line)
+ else:
+ sys.stdout.write(p.sub(subst, line))
+ fileinput.close()
+ return 0
+ else:
+ print "File %s doesn't exist." % filename
+ return 1
+
+def set_directive(filename, directive, value):
+ """Set a name/value pair directive in a configuration file.
+
+ This has only been tested with nss.conf
+ """
+ fd = open(filename)
+ file = []
+ for line in fd:
+ if directive in line:
+ file.append('%s "%s"\n' % (directive, value))
+ else:
+ file.append(line)
+ fd.close()
+
+ fd = open(filename, "w")
+ fd.write("".join(file))
+ fd.close()
+
+def kadmin(command):
+ ipautil.run(["/usr/kerberos/sbin/kadmin.local", "-q", command])
+
+def kadmin_addprinc(principal):
+ kadmin("addprinc -randkey " + principal)
+
+def kadmin_modprinc(principal, options):
+ kadmin("modprinc " + options + " " + principal)
+
+def create_keytab(path, principal):
+ try:
+ if ipautil.file_exists(path):
+ os.remove(path)
+ except os.error:
+ logging.critical("Failed to remove %s." % path)
+
+ kadmin("ktadd -k " + path + " " + principal)
+
diff --git a/ipa-server/ipaserver/ipaldap.py b/ipa-server/ipaserver/ipaldap.py
new file mode 100644
index 00000000..c2dbe4e2
--- /dev/null
+++ b/ipa-server/ipaserver/ipaldap.py
@@ -0,0 +1,701 @@
+# Authors: Rich Megginson <richm@redhat.com>
+# Rob Crittenden <rcritten@redhat.com
+#
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+import sys
+import os
+import os.path
+import popen2
+import base64
+import urllib
+import urllib2
+import socket
+import ldif
+import re
+import string
+import ldap
+import cStringIO
+import time
+import operator
+import struct
+import ldap.sasl
+from ldap.controls import LDAPControl,DecodeControlTuples,EncodeControlTuples
+from ldap.ldapobject import SimpleLDAPObject
+from ipa import ipaerror, ipautil
+
+# Global variable to define SASL auth
+sasl_auth = ldap.sasl.sasl({},'GSSAPI')
+
+class Entry:
+ """This class represents an LDAP Entry object. An LDAP entry consists of a DN
+ and a list of attributes. Each attribute consists of a name and a list of
+ values. In python-ldap, entries are returned as a list of 2-tuples.
+ Instance variables:
+ dn - string - the string DN of the entry
+ data - CIDict - case insensitive dict of the attributes and values"""
+
+ def __init__(self,entrydata):
+ """data is the raw data returned from the python-ldap result method, which is
+ a search result entry or a reference or None.
+ If creating a new empty entry, data is the string DN."""
+ if entrydata:
+ if isinstance(entrydata,tuple):
+ self.dn = entrydata[0]
+ self.data = ipautil.CIDict(entrydata[1])
+ elif isinstance(entrydata,str) or isinstance(entrydata,unicode):
+ self.dn = entrydata
+ self.data = ipautil.CIDict()
+ else:
+ self.dn = ''
+ self.data = ipautil.CIDict()
+
+ def __nonzero__(self):
+ """This allows us to do tests like if entry: returns false if there is no data,
+ true otherwise"""
+ return self.data != None and len(self.data) > 0
+
+ def hasAttr(self,name):
+ """Return True if this entry has an attribute named name, False otherwise"""
+ return self.data and self.data.has_key(name)
+
+ def __getattr__(self,name):
+ """If name is the name of an LDAP attribute, return the first value for that
+ attribute - equivalent to getValue - this allows the use of
+ entry.cn
+ instead of
+ entry.getValue('cn')
+ This also allows us to return None if an attribute is not found rather than
+ throwing an exception"""
+ return self.getValue(name)
+
+ def getValues(self,name):
+ """Get the list (array) of values for the attribute named name"""
+ return self.data.get(name)
+
+ def getValue(self,name):
+ """Get the first value for the attribute named name"""
+ return self.data.get(name,[None])[0]
+
+ def setValue(self,name,*value):
+ """Value passed in may be a single value, several values, or a single sequence.
+ For example:
+ ent.setValue('name', 'value')
+ ent.setValue('name', 'value1', 'value2', ..., 'valueN')
+ ent.setValue('name', ['value1', 'value2', ..., 'valueN'])
+ ent.setValue('name', ('value1', 'value2', ..., 'valueN'))
+ Since *value is a tuple, we may have to extract a list or tuple from that
+ tuple as in the last two examples above"""
+ if isinstance(value[0],list) or isinstance(value[0],tuple):
+ self.data[name] = value[0]
+ else:
+ self.data[name] = value
+
+ setValues = setValue
+
+ def toTupleList(self):
+ """Convert the attrs and values to a list of 2-tuples. The first element
+ of the tuple is the attribute name. The second element is either a
+ single value or a list of values."""
+ return self.data.items()
+
+ def __str__(self):
+ """Convert the Entry to its LDIF representation"""
+ return self.__repr__()
+
+ # the ldif class base64 encodes some attrs which I would rather see in raw form - to
+ # encode specific attrs as base64, add them to the list below
+ ldif.safe_string_re = re.compile('^$')
+ base64_attrs = ['nsstate', 'krbprincipalkey', 'krbExtraData']
+
+ def __repr__(self):
+ """Convert the Entry to its LDIF representation"""
+ sio = cStringIO.StringIO()
+ # what's all this then? the unparse method will currently only accept
+ # a list or a dict, not a class derived from them. self.data is a
+ # cidict, so unparse barfs on it. I've filed a bug against python-ldap,
+ # but in the meantime, we have to convert to a plain old dict for printing
+ # I also don't want to see wrapping, so set the line width really high (1000)
+ newdata = {}
+ newdata.update(self.data)
+ ldif.LDIFWriter(sio,Entry.base64_attrs,1000).unparse(self.dn,newdata)
+ return sio.getvalue()
+
+def wrapper(f,name):
+ """This is the method that wraps all of the methods of the superclass. This seems
+ to need to be an unbound method, that's why it's outside of IPAdmin. Perhaps there
+ is some way to do this with the new classmethod or staticmethod of 2.4.
+ Basically, we replace every call to a method in SimpleLDAPObject (the superclass
+ of IPAdmin) with a call to inner. The f argument to wrapper is the bound method
+ of IPAdmin (which is inherited from the superclass). Bound means that it will implicitly
+ be called with the self argument, it is not in the args list. name is the name of
+ the method to call. If name is a method that returns entry objects (e.g. result),
+ we wrap the data returned by an Entry class. If name is a method that takes an entry
+ argument, we extract the raw data from the entry object to pass in."""
+ def inner(*args, **kargs):
+ if name == 'result':
+ type, data = f(*args, **kargs)
+ # data is either a 2-tuple or a list of 2-tuples
+ # print data
+ if data:
+ if isinstance(data,tuple):
+ return type, Entry(data)
+ elif isinstance(data,list):
+ return type, [Entry(x) for x in data]
+ else:
+ raise TypeError, "unknown data type %s returned by result" % type(data)
+ else:
+ return type, data
+ elif name.startswith('add'):
+ # the first arg is self
+ # the second and third arg are the dn and the data to send
+ # We need to convert the Entry into the format used by
+ # python-ldap
+ ent = args[0]
+ if isinstance(ent,Entry):
+ return f(ent.dn, ent.toTupleList(), *args[2:])
+ else:
+ return f(*args, **kargs)
+ else:
+ return f(*args, **kargs)
+ return inner
+
+class LDIFConn(ldif.LDIFParser):
+ def __init__(
+ self,
+ input_file,
+ ignored_attr_types=None,max_entries=0,process_url_schemes=None
+ ):
+ """
+ See LDIFParser.__init__()
+
+ Additional Parameters:
+ all_records
+ List instance for storing parsed records
+ """
+ self.dndict = {} # maps dn to Entry
+ self.dnlist = [] # contains entries in order read
+ myfile = input_file
+ if isinstance(input_file,str) or isinstance(input_file,unicode):
+ myfile = open(input_file, "r")
+ ldif.LDIFParser.__init__(self,myfile,ignored_attr_types,max_entries,process_url_schemes)
+ self.parse()
+ if isinstance(input_file,str) or isinstance(input_file,unicode):
+ myfile.close()
+
+ def handle(self,dn,entry):
+ """
+ Append single record to dictionary of all records.
+ """
+ if not dn:
+ dn = ''
+ newentry = Entry((dn, entry))
+ self.dndict[IPAdmin.normalizeDN(dn)] = newentry
+ self.dnlist.append(newentry)
+
+ def get(self,dn):
+ ndn = IPAdmin.normalizeDN(dn)
+ return self.dndict.get(ndn, Entry(None))
+
+class IPAdmin(SimpleLDAPObject):
+ CFGSUFFIX = "o=NetscapeRoot"
+ DEFAULT_USER_ID = "nobody"
+
+ def getDseAttr(self,attrname):
+ conffile = self.confdir + '/dse.ldif'
+ dseldif = LDIFConn(conffile)
+ cnconfig = dseldif.get("cn=config")
+ if cnconfig:
+ return cnconfig.getValue(attrname)
+ return None
+
+ def __initPart2(self):
+ if self.binddn and len(self.binddn) and not hasattr(self,'sroot'):
+ try:
+ ent = self.getEntry('cn=config', ldap.SCOPE_BASE, '(objectclass=*)',
+ [ 'nsslapd-instancedir', 'nsslapd-errorlog',
+ 'nsslapd-certdir', 'nsslapd-schemadir' ])
+ self.errlog = ent.getValue('nsslapd-errorlog')
+ self.confdir = ent.getValue('nsslapd-certdir')
+ if not self.confdir:
+ self.confdir = ent.getValue('nsslapd-schemadir')
+ if self.confdir:
+ self.confdir = os.path.dirname(self.confdir)
+ instdir = ent.getValue('nsslapd-instancedir')
+ ent = self.getEntry('cn=config,cn=ldbm database,cn=plugins,cn=config',
+ ldap.SCOPE_BASE, '(objectclass=*)',
+ [ 'nsslapd-directory' ])
+ self.dbdir = os.path.dirname(ent.getValue('nsslapd-directory'))
+ except (ldap.INSUFFICIENT_ACCESS, ldap.CONNECT_ERROR):
+ pass # usually means
+ except ldap.OPERATIONS_ERROR, e:
+ pass # usually means this is Active Directory
+ except ldap.LDAPError, e:
+ print "caught exception ", e
+ raise
+
+ def __localinit__(self):
+ """If a CA certificate is provided then it is assumed that we are
+ doing SSL client authentication with proxy auth.
+
+ If a CA certificate is not present then it is assumed that we are
+ using a forwarded kerberos ticket for SASL auth. SASL provides
+ its own encryption.
+ """
+ if self.cacert is not None:
+ SimpleLDAPObject.__init__(self,'ldaps://%s:%d' % (self.host,self.port))
+ else:
+ SimpleLDAPObject.__init__(self,'ldap://%s:%d' % (self.host,self.port))
+
+ def __init__(self,host,port=389,cacert=None,bindcert=None,bindkey=None,proxydn=None,debug=None):
+ """We just set our instance variables and wrap the methods - the real
+ work is done in __localinit__ and __initPart2 - these are separated
+ out this way so that we can call them from places other than
+ instance creation e.g. when we just need to reconnect, not create a
+ new instance"""
+ if debug and debug.lower() == "on":
+ ldap.set_option(ldap.OPT_DEBUG_LEVEL,255)
+ if cacert is not None:
+ ldap.set_option(ldap.OPT_X_TLS_CACERTFILE,cacert)
+ if bindcert is not None:
+ ldap.set_option(ldap.OPT_X_TLS_CERTFILE,bindcert)
+ if bindkey is not None:
+ ldap.set_option(ldap.OPT_X_TLS_KEYFILE,bindkey)
+
+ self.__wrapmethods()
+ self.port = port
+ self.host = host
+ self.cacert = cacert
+ self.bindcert = bindcert
+ self.bindkey = bindkey
+ self.proxydn = proxydn
+ self.suffixes = {}
+ self.__localinit__()
+
+ def __str__(self):
+ return self.host + ":" + str(self.port)
+
+ def __get_server_controls__(self):
+ """Create the proxy user server control. The control has the form
+ 0x04 = Octet String
+ 4|0x80 sets the length of the string length field at 4 bytes
+ the struct() gets us the length in bytes of string self.proxydn
+ self.proxydn is the proxy dn to send"""
+
+ import sys
+
+ if self.proxydn is not None:
+ proxydn = chr(0x04) + chr(4|0x80) + struct.pack('l', socket.htonl(len(self.proxydn))) + self.proxydn;
+
+ # Create the proxy control
+ sctrl=[]
+ sctrl.append(LDAPControl('2.16.840.1.113730.3.4.18',True,proxydn))
+ else:
+ sctrl=None
+
+ return sctrl
+
+ def toLDAPURL(self):
+ return "ldap://%s:%d/" % (self.host,self.port)
+
+ def set_proxydn(self, proxydn):
+ self.proxydn = proxydn
+
+ def set_krbccache(self, krbccache, principal):
+ if krbccache is not None:
+ os.environ["KRB5CCNAME"] = krbccache
+ self.sasl_interactive_bind_s("", sasl_auth)
+ self.principal = principal
+ self.proxydn = None
+
+ def do_simple_bind(self, binddn="cn=directory manager", bindpw=""):
+ self.binddn = binddn
+ self.bindpwd = bindpw
+ self.simple_bind_s(binddn, bindpw)
+ self.__initPart2()
+
+ def getEntry(self,*args):
+ """This wraps the search function. It is common to just get one entry"""
+
+ sctrl = self.__get_server_controls__()
+
+ if sctrl is not None:
+ self.set_option(ldap.OPT_SERVER_CONTROLS, sctrl)
+
+ try:
+ res = self.search(*args)
+ type, obj = self.result(res)
+ except ldap.NO_SUCH_OBJECT:
+ raise ipaerror.gen_exception(ipaerror.LDAP_NOT_FOUND,
+ notfound(args))
+ except ldap.LDAPError, e:
+ raise ipaerror.gen_exception(ipaerror.LDAP_DATABASE_ERROR, None, e)
+
+ if not obj:
+ raise ipaerror.gen_exception(ipaerror.LDAP_NOT_FOUND,
+ notfound(args))
+ elif isinstance(obj,Entry):
+ return obj
+ else: # assume list/tuple
+ return obj[0]
+
+ def getList(self,*args):
+ """This wraps the search function to find all users."""
+
+ sctrl = self.__get_server_controls__()
+ if sctrl is not None:
+ self.set_option(ldap.OPT_SERVER_CONTROLS, sctrl)
+
+ try:
+ res = self.search(*args)
+ type, obj = self.result(res)
+ except (ldap.ADMINLIMIT_EXCEEDED, ldap.SIZELIMIT_EXCEEDED), e:
+ raise ipaerror.gen_exception(ipaerror.LDAP_DATABASE_ERROR,
+ "Too many results returned by search", e)
+ except ldap.LDAPError, e:
+ raise ipaerror.gen_exception(ipaerror.LDAP_DATABASE_ERROR, None, e)
+
+ if not obj:
+ raise ipaerror.gen_exception(ipaerror.LDAP_NOT_FOUND,
+ notfound(args))
+
+ all_users = []
+ for s in obj:
+ all_users.append(s)
+
+ return all_users
+
+ def getListAsync(self,*args):
+ """This version performs an asynchronous search, to allow
+ results even if we hit a limit.
+
+ It returns a list: counter followed by the results.
+ If the results are truncated, counter will be set to -1.
+ """
+
+ sctrl = self.__get_server_controls__()
+ if sctrl is not None:
+ self.set_option(ldap.OPT_SERVER_CONTROLS, sctrl)
+
+ entries = []
+ partial = 0
+
+ try:
+ msgid = self.search_ext(*args)
+ type, result_list = self.result(msgid, 0)
+ while result_list:
+ for result in result_list:
+ entries.append(result)
+ type, result_list = self.result(msgid, 0)
+ except (ldap.ADMINLIMIT_EXCEEDED, ldap.SIZELIMIT_EXCEEDED,
+ ldap.TIMELIMIT_EXCEEDED), e:
+ partial = 1
+ except ldap.LDAPError, e:
+ raise ipaerror.gen_exception(ipaerror.LDAP_DATABASE_ERROR, None, e)
+
+ if not entries:
+ raise ipaerror.gen_exception(ipaerror.LDAP_NOT_FOUND,
+ notfound(args))
+
+ if partial == 1:
+ counter = -1
+ else:
+ counter = len(entries)
+
+ return [counter] + entries
+
+ def addEntry(self,*args):
+ """This wraps the add function. It assumes that the entry is already
+ populated with all of the desired objectclasses and attributes"""
+
+ sctrl = self.__get_server_controls__()
+
+ try:
+ if sctrl is not None:
+ self.set_option(ldap.OPT_SERVER_CONTROLS, sctrl)
+ self.add_s(*args)
+ except ldap.ALREADY_EXISTS:
+ raise ipaerror.gen_exception(ipaerror.LDAP_DUPLICATE)
+ except ldap.LDAPError, e:
+ raise ipaerror.gen_exception(ipaerror.LDAP_DATABASE_ERROR, None, e)
+ return "Success"
+
+ def updateRDN(self, dn, newrdn):
+ """Wrap the modrdn function."""
+
+ sctrl = self.__get_server_controls__()
+
+ if dn == newrdn:
+ # no need to report an error
+ return "Success"
+
+ try:
+ if sctrl is not None:
+ self.set_option(ldap.OPT_SERVER_CONTROLS, sctrl)
+ self.modrdn_s(dn, newrdn, delold=1)
+ except ldap.LDAPError, e:
+ raise ipaerror.gen_exception(ipaerror.LDAP_DATABASE_ERROR, None, e)
+ return "Success"
+
+ def updateEntry(self,dn,olduser,newuser):
+ """This wraps the mod function. It assumes that the entry is already
+ populated with all of the desired objectclasses and attributes"""
+
+ sctrl = self.__get_server_controls__()
+
+ modlist = self.generateModList(olduser, newuser)
+
+ if len(modlist) == 0:
+ raise ipaerror.gen_exception(ipaerror.LDAP_EMPTY_MODLIST)
+
+ try:
+ if sctrl is not None:
+ self.set_option(ldap.OPT_SERVER_CONTROLS, sctrl)
+ self.modify_s(dn, modlist)
+ # this is raised when a 'delete' attribute isn't found.
+ # it indicates the previous attribute was removed by another
+ # update, making the olduser stale.
+ except ldap.NO_SUCH_ATTRIBUTE:
+ raise ipaerror.gen_exception(ipaerror.LDAP_MIDAIR_COLLISION)
+ except ldap.LDAPError, e:
+ raise ipaerror.gen_exception(ipaerror.LDAP_DATABASE_ERROR, None, e)
+ return "Success"
+
+ def generateModList(self, old_entry, new_entry):
+ """A mod list generator that computes more precise modification lists
+ than the python-ldap version. This version purposely generates no
+ REPLACE operations, to deal with multi-user updates more properly."""
+ modlist = []
+
+ old_entry = ipautil.CIDict(old_entry)
+ new_entry = ipautil.CIDict(new_entry)
+
+ keys = set(map(string.lower, old_entry.keys()))
+ keys.update(map(string.lower, new_entry.keys()))
+
+ for key in keys:
+ new_values = new_entry.get(key, [])
+ if not(isinstance(new_values,list) or isinstance(new_values,tuple)):
+ new_values = [new_values]
+ new_values = filter(lambda value:value!=None, new_values)
+ new_values = set(new_values)
+
+ old_values = old_entry.get(key, [])
+ if not(isinstance(old_values,list) or isinstance(old_values,tuple)):
+ old_values = [old_values]
+ old_values = filter(lambda value:value!=None, old_values)
+ old_values = set(old_values)
+
+ adds = list(new_values.difference(old_values))
+ removes = list(old_values.difference(new_values))
+
+ if len(removes) > 0:
+ modlist.append((ldap.MOD_DELETE, key, removes))
+ if len(adds) > 0:
+ modlist.append((ldap.MOD_ADD, key, adds))
+
+ return modlist
+
+ def inactivateEntry(self,dn,has_key):
+ """Rather than deleting entries we mark them as inactive.
+ has_key defines whether the entry already has nsAccountlock
+ set so we can determine which type of mod operation to run."""
+
+ sctrl = self.__get_server_controls__()
+ modlist=[]
+
+ if has_key == True:
+ operation = ldap.MOD_REPLACE
+ else:
+ operation = ldap.MOD_ADD
+
+ modlist.append((operation, "nsAccountlock", "true"))
+
+ try:
+ if sctrl is not None:
+ self.set_option(ldap.OPT_SERVER_CONTROLS, sctrl)
+ self.modify_s(dn, modlist)
+ except ldap.LDAPError, e:
+ raise ipaerror.gen_exception(ipaerror.LDAP_DATABASE_ERROR, None, e)
+ return "Success"
+
+ def deleteEntry(self,*args):
+ """This wraps the delete function. Use with caution."""
+
+ sctrl = self.__get_server_controls__()
+
+ try:
+ if sctrl is not None:
+ self.set_option(ldap.OPT_SERVER_CONTROLS, sctrl)
+ self.delete_s(*args)
+ except ldap.LDAPError, e:
+ raise ipaerror.gen_exception(ipaerror.LDAP_DATABASE_ERROR, None, e)
+ return "Success"
+
+ def modifyPassword(self,dn,oldpass,newpass):
+ """Set the user password using RFC 3062, LDAP Password Modify Extended
+ Operation. This ends up calling the IPA password slapi plugin
+ handler so the Kerberos password gets set properly.
+
+ oldpass is not mandatory
+ """
+
+ sctrl = self.__get_server_controls__()
+
+ try:
+ if sctrl is not None:
+ self.set_option(ldap.OPT_SERVER_CONTROLS, sctrl)
+ self.passwd_s(dn, oldpass, newpass)
+ except ldap.LDAPError, e:
+ raise ipaerror.gen_exception(ipaerror.LDAP_DATABASE_ERROR, None, e)
+ return "Success"
+
+ def __wrapmethods(self):
+ """This wraps all methods of SimpleLDAPObject, so that we can intercept
+ the methods that deal with entries. Instead of using a raw list of tuples
+ of lists of hashes of arrays as the entry object, we want to wrap entries
+ in an Entry class that provides some useful methods"""
+ for name in dir(self.__class__.__bases__[0]):
+ attr = getattr(self, name)
+ if callable(attr):
+ setattr(self, name, wrapper(attr, name))
+
+ def exportLDIF(self, file, suffix, forrepl=False, verbose=False):
+ cn = "export" + str(int(time.time()))
+ dn = "cn=%s, cn=export, cn=tasks, cn=config" % cn
+ entry = Entry(dn)
+ entry.setValues('objectclass', 'top', 'extensibleObject')
+ entry.setValues('cn', cn)
+ entry.setValues('nsFilename', file)
+ entry.setValues('nsIncludeSuffix', suffix)
+ if forrepl:
+ entry.setValues('nsExportReplica', 'true')
+
+ rc = self.startTaskAndWait(entry, verbose)
+
+ if rc:
+ if verbose:
+ print "Error: export task %s for file %s exited with %d" % (cn,file,rc)
+ else:
+ if verbose:
+ print "Export task %s for file %s completed successfully" % (cn,file)
+ return rc
+
+ def waitForEntry(self, dn, timeout=7200, attr='', quiet=True):
+ scope = ldap.SCOPE_BASE
+ filter = "(objectclass=*)"
+ attrlist = []
+ if attr:
+ filter = "(%s=*)" % attr
+ attrlist.append(attr)
+ timeout += int(time.time())
+
+ if isinstance(dn,Entry):
+ dn = dn.dn
+
+ # wait for entry and/or attr to show up
+ if not quiet:
+ sys.stdout.write("Waiting for %s %s:%s " % (self,dn,attr))
+ sys.stdout.flush()
+ entry = None
+ while not entry and int(time.time()) < timeout:
+ try:
+ entry = self.getEntry(dn, scope, filter, attrlist)
+ except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND):
+ pass # found entry, but no attr
+ except ldap.NO_SUCH_OBJECT:
+ pass # no entry yet
+ except ldap.LDAPError, e: # badness
+ print "\nError reading entry", dn, e
+ break
+ if not entry:
+ if not quiet:
+ sys.stdout.write(".")
+ sys.stdout.flush()
+ time.sleep(1)
+
+ if not entry and int(time.time()) > timeout:
+ print "\nwaitForEntry timeout for %s for %s" % (self,dn)
+ elif entry and not quiet:
+ print "\nThe waited for entry is:", entry
+ elif not entry:
+ print "\nError: could not read entry %s from %s" % (dn,self)
+
+ return entry
+
+ def addSchema(self, attr, val):
+ dn = "cn=schema"
+ self.modify_s(dn, [(ldap.MOD_ADD, attr, val)])
+
+ def addAttr(self, *args):
+ return self.addSchema('attributeTypes', args)
+
+ def addObjClass(self, *args):
+ return self.addSchema('objectClasses', args)
+
+ ###########################
+ # Static methods start here
+ ###########################
+ def normalizeDN(dn):
+ # not great, but will do until we use a newer version of python-ldap
+ # that has DN utilities
+ ary = ldap.explode_dn(dn.lower())
+ return ",".join(ary)
+ normalizeDN = staticmethod(normalizeDN)
+
+ def getfqdn(name=''):
+ return socket.getfqdn(name)
+ getfqdn = staticmethod(getfqdn)
+
+ def getdomainname(name=''):
+ fqdn = IPAdmin.getfqdn(name)
+ index = fqdn.find('.')
+ if index >= 0:
+ return fqdn[index+1:]
+ else:
+ return fqdn
+ getdomainname = staticmethod(getdomainname)
+
+ def getdefaultsuffix(name=''):
+ dm = IPAdmin.getdomainname(name)
+ if dm:
+ return "dc=" + dm.replace('.', ', dc=')
+ else:
+ return 'dc=localdomain'
+ getdefaultsuffix = staticmethod(getdefaultsuffix)
+
+ def is_a_dn(dn):
+ """Returns True if the given string is a DN, False otherwise."""
+ return (dn.find("=") > 0)
+ is_a_dn = staticmethod(is_a_dn)
+
+
+def notfound(args):
+ """Return a string suitable for displaying as an error when a
+ search returns no results.
+
+ This just returns whatever is after the equals sign"""
+ if len(args) > 2:
+ filter = args[2]
+ try:
+ target = re.match(r'\(.*=(.*)\)', filter).group(1)
+ except:
+ target = filter
+ return "%s not found" % str(target)
+ else:
+ return args[0]
diff --git a/ipa-server/ipaserver/krbinstance.py b/ipa-server/ipaserver/krbinstance.py
new file mode 100644
index 00000000..25284430
--- /dev/null
+++ b/ipa-server/ipaserver/krbinstance.py
@@ -0,0 +1,428 @@
+# Authors: Simo Sorce <ssorce@redhat.com>
+#
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+import subprocess
+import string
+import tempfile
+import shutil
+import logging
+import fileinput
+import re
+import sys
+import os
+import pwd
+import socket
+import shutil
+
+import service
+import installutils
+from ipa import sysrestore
+from ipa import ipautil
+from ipa import ipaerror
+
+import ipaldap
+
+import ldap
+from ldap import LDAPError
+from ldap import ldapobject
+
+from pyasn1.type import univ, namedtype
+import pyasn1.codec.ber.encoder
+import pyasn1.codec.ber.decoder
+import struct
+import base64
+
+KRBMKEY_DENY_ACI = """
+(targetattr = "krbMKey")(version 3.0; acl "No external access"; deny (all) userdn != "ldap:///uid=kdc,cn=sysaccounts,cn=etc,$SUFFIX";)
+"""
+
+def update_key_val_in_file(filename, key, val):
+ if os.path.exists(filename):
+ pattern = "^[\s#]*%s\s*=\s*%s\s*" % (re.escape(key), re.escape(val))
+ p = re.compile(pattern)
+ for line in fileinput.input(filename):
+ if p.search(line):
+ fileinput.close()
+ return
+ fileinput.close()
+
+ pattern = "^[\s#]*%s\s*=" % re.escape(key)
+ p = re.compile(pattern)
+ for line in fileinput.input(filename, inplace=1):
+ if not p.search(line):
+ sys.stdout.write(line)
+ fileinput.close()
+ f = open(filename, "a")
+ f.write("%s=%s\n" % (key, val))
+ f.close()
+
+class KpasswdInstance(service.SimpleServiceInstance):
+ def __init__(self):
+ service.SimpleServiceInstance.__init__(self, "ipa_kpasswd")
+
+class KrbInstance(service.Service):
+ def __init__(self, fstore=None):
+ service.Service.__init__(self, "krb5kdc")
+ self.ds_user = None
+ self.fqdn = None
+ self.realm = None
+ self.domain = None
+ self.host = None
+ self.admin_password = None
+ self.master_password = None
+ self.suffix = None
+ self.kdc_password = None
+ self.sub_dict = None
+
+ self.kpasswd = KpasswdInstance()
+
+ if fstore:
+ self.fstore = fstore
+ else:
+ self.fstore = sysrestore.FileStore('/var/lib/ipa/sysrestore')
+
+ def __common_setup(self, ds_user, realm_name, host_name, domain_name, admin_password):
+ self.ds_user = ds_user
+ self.fqdn = host_name
+ self.realm = realm_name.upper()
+ self.host = host_name.split(".")[0]
+ self.ip = socket.gethostbyname(host_name)
+ self.domain = domain_name
+ self.suffix = ipautil.realm_to_suffix(self.realm)
+ self.kdc_password = ipautil.ipa_generate_password()
+ self.admin_password = admin_password
+
+ self.__setup_sub_dict()
+
+ # get a connection to the DS
+ try:
+ self.conn = ipaldap.IPAdmin(self.fqdn)
+ self.conn.do_simple_bind(bindpw=self.admin_password)
+ except Exception, e:
+ logging.critical("Could not connect to the Directory Server on %s" % self.fqdn)
+ raise e
+
+ self.backup_state("running", self.is_running())
+ try:
+ self.stop()
+ except:
+ # It could have been not running
+ pass
+
+ def __common_post_setup(self):
+ self.step("starting the KDC", self.__start_instance)
+ self.step("configuring KDC to start on boot", self.__enable)
+
+ def create_instance(self, ds_user, realm_name, host_name, domain_name, admin_password, master_password):
+ self.master_password = master_password
+
+ self.__common_setup(ds_user, realm_name, host_name, domain_name, admin_password)
+
+ self.step("setting KDC account password", self.__configure_kdc_account_password)
+ self.step("adding sasl mappings to the directory", self.__configure_sasl_mappings)
+ self.step("adding kerberos entries to the DS", self.__add_krb_entries)
+ self.step("adding default ACIs", self.__add_default_acis)
+ self.step("configuring KDC", self.__create_instance)
+ self.step("adding default keytypes", self.__add_default_keytypes)
+ self.step("creating a keytab for the directory", self.__create_ds_keytab)
+ self.step("creating a keytab for the machine", self.__create_host_keytab)
+ self.step("exporting the kadmin keytab", self.__export_kadmin_changepw_keytab)
+ self.step("adding the password extension to the directory", self.__add_pwd_extop_module)
+ self.step("adding the kerberos master key to the directory", self.__add_master_key)
+
+ self.__common_post_setup()
+
+ self.start_creation("Configuring Kerberos KDC")
+
+ self.kpasswd.create_instance()
+
+ def create_replica(self, ds_user, realm_name, host_name, domain_name, admin_password, ldap_passwd_filename, kpasswd_filename):
+ self.__copy_ldap_passwd(ldap_passwd_filename)
+ self.__copy_kpasswd_keytab(kpasswd_filename)
+
+ self.__common_setup(ds_user, realm_name, host_name, domain_name, admin_password)
+
+ self.step("adding sasl mappings to the directory", self.__configure_sasl_mappings)
+ self.step("writing stash file from DS", self.__write_stash_from_ds)
+ self.step("configuring KDC", self.__create_replica_instance)
+ self.step("creating a keytab for the directory", self.__create_ds_keytab)
+ self.step("creating a keytab for the machine", self.__create_host_keytab)
+ self.step("adding the password extension to the directory", self.__add_pwd_extop_module)
+
+ self.__common_post_setup()
+
+ self.start_creation("Configuring Kerberos KDC")
+
+ self.kpasswd.create_instance()
+
+ def __copy_ldap_passwd(self, filename):
+ self.fstore.backup_file("/var/kerberos/krb5kdc/ldappwd")
+ shutil.copy(filename, "/var/kerberos/krb5kdc/ldappwd")
+ os.chmod("/var/kerberos/krb5kdc/ldappwd", 0600)
+
+ def __copy_kpasswd_keytab(self, filename):
+ self.fstore.backup_file("/var/kerberos/krb5kdc/kpasswd.keytab")
+ shutil.copy(filename, "/var/kerberos/krb5kdc/kpasswd.keytab")
+ os.chmod("/var/kerberos/krb5kdc/kpasswd.keytab", 0600)
+
+
+ def __configure_kdc_account_password(self):
+ hexpwd = ''
+ for x in self.kdc_password:
+ hexpwd += (hex(ord(x))[2:])
+ self.fstore.backup_file("/var/kerberos/krb5kdc/ldappwd")
+ pwd_fd = open("/var/kerberos/krb5kdc/ldappwd", "w")
+ pwd_fd.write("uid=kdc,cn=sysaccounts,cn=etc,"+self.suffix+"#{HEX}"+hexpwd+"\n")
+ pwd_fd.close()
+ os.chmod("/var/kerberos/krb5kdc/ldappwd", 0600)
+
+ def __enable(self):
+ self.backup_state("enabled", self.is_enabled())
+ self.chkconfig_on()
+
+ def __start_instance(self):
+ try:
+ self.start()
+ except:
+ logging.critical("krb5kdc service failed to start")
+
+ def __setup_sub_dict(self):
+ self.sub_dict = dict(FQDN=self.fqdn,
+ IP=self.ip,
+ PASSWORD=self.kdc_password,
+ SUFFIX=self.suffix,
+ DOMAIN=self.domain,
+ HOST=self.host,
+ REALM=self.realm)
+
+ def __ldap_mod(self, ldif):
+ txt = ipautil.template_file(ipautil.SHARE_DIR + ldif, self.sub_dict)
+ fd = ipautil.write_tmp_file(txt)
+
+ [pw_fd, pw_name] = tempfile.mkstemp()
+ os.write(pw_fd, self.admin_password)
+ os.close(pw_fd)
+
+ args = ["/usr/bin/ldapmodify", "-h", "127.0.0.1", "-xv",
+ "-D", "cn=Directory Manager", "-y", pw_name, "-f", fd.name]
+
+ try:
+ try:
+ ipautil.run(args)
+ except ipautil.CalledProcessError, e:
+ logging.critical("Failed to load %s: %s" % (ldif, str(e)))
+ finally:
+ os.remove(pw_name)
+
+ fd.close()
+
+ def __configure_sasl_mappings(self):
+ # we need to remove any existing SASL mappings in the directory as otherwise they
+ # they may conflict. There is no way to define the order they are used in atm.
+
+ # FIXME: for some reason IPAdmin dies here, so we switch
+ # it out for a regular ldapobject.
+ conn = self.conn
+ self.conn = ldapobject.SimpleLDAPObject("ldap://127.0.0.1/")
+ self.conn.bind("cn=directory manager", self.admin_password)
+ try:
+ msgid = self.conn.search("cn=mapping,cn=sasl,cn=config", ldap.SCOPE_ONELEVEL, "(objectclass=nsSaslMapping)")
+ res = self.conn.result(msgid)
+ for r in res[1]:
+ mid = self.conn.delete_s(r[0])
+ #except LDAPError, e:
+ # logging.critical("Error during SASL mapping removal: %s" % str(e))
+ except Exception, e:
+ logging.critical("Could not connect to the Directory Server on %s" % self.fqdn)
+ raise e
+ print type(e)
+ print dir(e)
+ raise e
+
+ self.conn = conn
+
+ entry = ipaldap.Entry("cn=Full Principal,cn=mapping,cn=sasl,cn=config")
+ entry.setValues("objectclass", "top", "nsSaslMapping")
+ entry.setValues("cn", "Full Principal")
+ entry.setValues("nsSaslMapRegexString", '\(.*\)@\(.*\)')
+ entry.setValues("nsSaslMapBaseDNTemplate", self.suffix)
+ entry.setValues("nsSaslMapFilterTemplate", '(krbPrincipalName=\\1@\\2)')
+
+ try:
+ self.conn.add_s(entry)
+ except ldap.ALREADY_EXISTS:
+ logging.critical("failed to add Full Principal Sasl mapping")
+ raise e
+
+ entry = ipaldap.Entry("cn=Name Only,cn=mapping,cn=sasl,cn=config")
+ entry.setValues("objectclass", "top", "nsSaslMapping")
+ entry.setValues("cn", "Name Only")
+ entry.setValues("nsSaslMapRegexString", '\(.*\)')
+ entry.setValues("nsSaslMapBaseDNTemplate", self.suffix)
+ entry.setValues("nsSaslMapFilterTemplate", '(krbPrincipalName=\\1@%s)' % self.realm)
+
+ try:
+ self.conn.add_s(entry)
+ except ldap.ALREADY_EXISTS:
+ logging.critical("failed to add Name Only Sasl mapping")
+ raise e
+
+ def __add_krb_entries(self):
+ self.__ldap_mod("kerberos.ldif")
+
+ def __add_default_acis(self):
+ self.__ldap_mod("default-aci.ldif")
+
+ def __add_default_keytypes(self):
+ self.__ldap_mod("default-keytypes.ldif")
+
+ def __create_replica_instance(self):
+ self.__create_instance(replica=True)
+
+ def __template_file(self, path):
+ template = os.path.join(ipautil.SHARE_DIR, os.path.basename(path) + ".template")
+ conf = ipautil.template_file(template, self.sub_dict)
+ self.fstore.backup_file(path)
+ fd = open(path, "w+")
+ fd.write(conf)
+ fd.close()
+
+ def __create_instance(self, replica=False):
+ self.__template_file("/var/kerberos/krb5kdc/kdc.conf")
+ self.__template_file("/etc/krb5.conf")
+ self.__template_file("/usr/share/ipa/html/krb5.ini")
+ self.__template_file("/usr/share/ipa/html/krb.con")
+ self.__template_file("/usr/share/ipa/html/krbrealm.con")
+
+ if not replica:
+ #populate the directory with the realm structure
+ args = ["/usr/kerberos/sbin/kdb5_ldap_util", "-D", "uid=kdc,cn=sysaccounts,cn=etc,"+self.suffix, "-w", self.kdc_password, "create", "-s", "-P", self.master_password, "-r", self.realm, "-subtrees", self.suffix, "-sscope", "sub"]
+ try:
+ ipautil.run(args)
+ except ipautil.CalledProcessError, e:
+ print "Failed to populate the realm structure in kerberos", e
+
+ def __write_stash_from_ds(self):
+ try:
+ entry = self.conn.getEntry("cn=%s, cn=kerberos, %s" % (self.realm, self.suffix), ldap.SCOPE_SUBTREE)
+ except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND), e:
+ logging.critical("Could not find master key in DS")
+ raise e
+
+ krbMKey = pyasn1.codec.ber.decoder.decode(entry.krbmkey)
+ keytype = int(krbMKey[0][1][0])
+ keydata = str(krbMKey[0][1][1])
+
+ format = '=hi%ss' % len(keydata)
+ s = struct.pack(format, keytype, len(keydata), keydata)
+ try:
+ fd = open("/var/kerberos/krb5kdc/.k5."+self.realm, "w")
+ fd.write(s)
+ fd.close()
+ except os.error, e:
+ logging.critical("failed to write stash file")
+ raise e
+
+ #add the password extop module
+ def __add_pwd_extop_module(self):
+ self.__ldap_mod("pwd-extop-conf.ldif")
+
+ def __add_master_key(self):
+ #get the Master Key from the stash file
+ try:
+ stash = open("/var/kerberos/krb5kdc/.k5."+self.realm, "r")
+ keytype = struct.unpack('h', stash.read(2))[0]
+ keylen = struct.unpack('i', stash.read(4))[0]
+ keydata = stash.read(keylen)
+ except os.error:
+ logging.critical("Failed to retrieve Master Key from Stash file: %s")
+ #encode it in the asn.1 attribute
+ MasterKey = univ.Sequence()
+ MasterKey.setComponentByPosition(0, univ.Integer(keytype))
+ MasterKey.setComponentByPosition(1, univ.OctetString(keydata))
+ krbMKey = univ.Sequence()
+ krbMKey.setComponentByPosition(0, univ.Integer(0)) #we have no kvno
+ krbMKey.setComponentByPosition(1, MasterKey)
+ asn1key = pyasn1.codec.ber.encoder.encode(krbMKey)
+
+ dn = "cn="+self.realm+",cn=kerberos,"+self.suffix
+ #protect the master key by adding an appropriate deny rule along with the key
+ mod = [(ldap.MOD_ADD, 'aci', ipautil.template_str(KRBMKEY_DENY_ACI, self.sub_dict)),
+ (ldap.MOD_ADD, 'krbMKey', str(asn1key))]
+ try:
+ self.conn.modify_s(dn, mod)
+ except ldap.TYPE_OR_VALUE_EXISTS, e:
+ logging.critical("failed to add master key to kerberos database\n")
+ raise e
+
+ def __create_ds_keytab(self):
+ ldap_principal = "ldap/" + self.fqdn + "@" + self.realm
+ installutils.kadmin_addprinc(ldap_principal)
+
+ self.fstore.backup_file("/etc/dirsrv/ds.keytab")
+ installutils.create_keytab("/etc/dirsrv/ds.keytab", ldap_principal)
+
+ self.fstore.backup_file("/etc/sysconfig/dirsrv")
+ update_key_val_in_file("/etc/sysconfig/dirsrv", "export KRB5_KTNAME", "/etc/dirsrv/ds.keytab")
+ pent = pwd.getpwnam(self.ds_user)
+ os.chown("/etc/dirsrv/ds.keytab", pent.pw_uid, pent.pw_gid)
+
+ def __create_host_keytab(self):
+ host_principal = "host/" + self.fqdn + "@" + self.realm
+ installutils.kadmin_addprinc(host_principal)
+
+ self.fstore.backup_file("/etc/krb5.keytab")
+ installutils.create_keytab("/etc/krb5.keytab", host_principal)
+
+ # Make sure access is strictly reserved to root only for now
+ os.chown("/etc/krb5.keytab", 0, 0)
+ os.chmod("/etc/krb5.keytab", 0600)
+
+ def __export_kadmin_changepw_keytab(self):
+ installutils.kadmin_modprinc("kadmin/changepw", "+requires_preauth")
+
+ self.fstore.backup_file("/var/kerberos/krb5kdc/kpasswd.keytab")
+ installutils.create_keytab("/var/kerberos/krb5kdc/kpasswd.keytab", "kadmin/changepw")
+
+ self.fstore.backup_file("/etc/sysconfig/ipa_kpasswd")
+ update_key_val_in_file("/etc/sysconfig/ipa_kpasswd", "export KRB5_KTNAME", "/var/kerberos/krb5kdc/kpasswd.keytab")
+
+ def uninstall(self):
+ self.kpasswd.uninstall()
+
+ running = self.restore_state("running")
+ enabled = self.restore_state("enabled")
+
+ try:
+ self.stop()
+ except:
+ pass
+
+ for f in ["/var/kerberos/krb5kdc/ldappwd", "/var/kerberos/krb5kdc/kdc.conf", "/etc/krb5.conf"]:
+ try:
+ self.fstore.restore_file(f)
+ except ValueError, error:
+ logging.debug(error)
+ pass
+
+ if not enabled is None and not enabled:
+ self.chkconfig_off()
+
+ if not running is None and running:
+ self.start()
diff --git a/ipa-server/ipaserver/ldapupdate.py b/ipa-server/ipaserver/ldapupdate.py
new file mode 100755
index 00000000..cdf23125
--- /dev/null
+++ b/ipa-server/ipaserver/ldapupdate.py
@@ -0,0 +1,593 @@
+# Authors: Rob Crittenden <rcritten@redhat.com>
+#
+# Copyright (C) 2008 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+# Documentation can be found at http://freeipa.org/page/LdapUpdate
+
+# TODO
+# save undo files?
+
+UPDATES_DIR="/usr/share/ipa/updates/"
+
+import sys
+from ipaserver import ipaldap, installutils
+from ipa import entity, ipaerror, ipautil
+import ldap
+import logging
+import krbV
+import platform
+import shlex
+import time
+import random
+import os
+import fnmatch
+
+class BadSyntax(Exception):
+ def __init__(self, value):
+ self.value = value
+ def __str__(self):
+ return repr(self.value)
+
+class LDAPUpdate:
+ def __init__(self, dm_password, sub_dict={}, live_run=True):
+ """dm_password = Directory Manager password
+ sub_dict = substitution dictionary
+ live_run = Apply the changes or just test
+ """
+ self.sub_dict = sub_dict
+ self.live_run = live_run
+ self.dm_password = dm_password
+ self.conn = None
+ self.modified = False
+
+ krbctx = krbV.default_context()
+
+ fqdn = installutils.get_fqdn()
+ if fqdn is None:
+ raise RuntimeError("Unable to determine hostname")
+
+ domain = ipautil.get_domain_name()
+ libarch = self.__identify_arch()
+ suffix = ipautil.realm_to_suffix(krbctx.default_realm)
+
+ if not self.sub_dict.get("REALM"):
+ self.sub_dict["REALM"] = krbctx.default_realm
+ if not self.sub_dict.get("FQDN"):
+ self.sub_dict["FQDN"] = fqdn
+ if not self.sub_dict.get("DOMAIN"):
+ self.sub_dict["DOMAIN"] = domain
+ if not self.sub_dict.get("SUFFIX"):
+ self.sub_dict["SUFFIX"] = suffix
+ if not self.sub_dict.get("LIBARCH"):
+ self.sub_dict["LIBARCH"] = libarch
+ if not self.sub_dict.get("TIME"):
+ self.sub_dict["TIME"] = int(time.time())
+
+ # Try out the password
+ try:
+ conn = ipaldap.IPAdmin(fqdn)
+ conn.do_simple_bind(bindpw=self.dm_password)
+ conn.unbind()
+ except ldap.CONNECT_ERROR, e:
+ raise RuntimeError("Unable to connect to LDAP server %s" % fqdn)
+ except ldap.SERVER_DOWN, e:
+ raise RuntimeError("Unable to connect to LDAP server %s" % fqdn)
+ except ldap.INVALID_CREDENTIALS, e :
+ raise RuntimeError("The password provided is incorrect for LDAP server %s" % fqdn)
+
+ def __detail_error(self, detail):
+ """IPA returns two errors back. One a generic one indicating the broad
+ problem and a detailed message back as well which should have come
+ from LDAP. This function will parse that into a human-readable
+ string.
+ """
+ msg = ""
+ desc = detail[0].get('desc')
+ info = detail[0].get('info')
+
+ if desc:
+ msg = desc
+ if info:
+ msg = msg + " " + info
+
+ return msg
+
+ def __identify_arch(self):
+ """On multi-arch systems some libraries may be in /lib64, /usr/lib64,
+ etc. Determine if a suffix is needed based on the current
+ architecture.
+ """
+ bits = platform.architecture()[0]
+
+ if bits == "64bit":
+ return "64"
+ else:
+ return ""
+
+ def __template_str(self, s):
+ try:
+ return ipautil.template_str(s, self.sub_dict)
+ except KeyError, e:
+ raise BadSyntax("Unknown template keyword %s" % e)
+
+ def __remove_quotes(self, line):
+ """Remove leading and trailng double or single quotes"""
+ if line.startswith('"'):
+ line = line[1:]
+ if line.endswith('"'):
+ line = line[:-1]
+ if line.startswith("'"):
+ line = line[1:]
+ if line.endswith("'"):
+ line = line[:-1]
+
+ return line
+
+ def __parse_values(self, line):
+ """Parse a comma-separated string into separate values and convert them
+ into a list. This should handle quoted-strings with embedded commas
+ """
+ lexer = shlex.shlex(line)
+ lexer.wordchars = lexer.wordchars + ".()-"
+ l = []
+ v = ""
+ for token in lexer:
+ if token != ',':
+ if v:
+ v = v + " " + token
+ else:
+ v = token
+ else:
+ l.append(self.__remove_quotes(v))
+ v = ""
+
+ l.append(self.__remove_quotes(v))
+
+ return l
+
+ def read_file(self, filename):
+ if filename == '-':
+ fd = sys.stdin
+ else:
+ fd = open(filename)
+ text = fd.readlines()
+ if fd != sys.stdin: fd.close()
+ return text
+
+ def __entry_to_entity(self, ent):
+ """Tne Entry class is a bare LDAP entry. The Entity class has a lot more
+ helper functions that we need, so convert to dict and then to Entity.
+ """
+ entry = dict(ent.data)
+ entry['dn'] = ent.dn
+ for key,value in entry.iteritems():
+ if isinstance(value,list) or isinstance(value,tuple):
+ if len(value) == 0:
+ entry[key] = ''
+ elif len(value) == 1:
+ entry[key] = value[0]
+ return entity.Entity(entry)
+
+ def __combine_updates(self, dn_list, all_updates, update):
+ """Combine a new update with the list of total updates
+
+ Updates are stored in 2 lists:
+ dn_list: contains a unique list of DNs in the updates
+ all_updates: the actual updates that need to be applied
+
+ We want to apply the updates from the shortest to the longest
+ path so if new child and parent entries are in different updates
+ we can be sure the parent gets written first. This also lets
+ us apply any schema first since it is in the very short cn=schema.
+ """
+ dn = update.get('dn')
+ dns = ldap.explode_dn(dn.lower())
+ l = len(dns)
+ if dn_list.get(l):
+ if dn not in dn_list[l]:
+ dn_list[l].append(dn)
+ else:
+ dn_list[l] = [dn]
+ if not all_updates.get(dn):
+ all_updates[dn] = update
+ return all_updates
+
+ e = all_updates[dn]
+ e['updates'] = e['updates'] + update['updates']
+
+ all_updates[dn] = e
+
+ return all_updates
+
+ def parse_update_file(self, data, all_updates, dn_list):
+ """Parse the update file into a dictonary of lists and apply the update
+ for each DN in the file."""
+ valid_keywords = ["default", "add", "remove", "only"]
+ update = {}
+ d = ""
+ index = ""
+ dn = None
+ lcount = 0
+ for line in data:
+ # Strip out \n and extra white space
+ lcount = lcount + 1
+
+ # skip comments and empty lines
+ line = line.rstrip()
+ if line.startswith('#') or line == '': continue
+
+ if line.lower().startswith('dn:'):
+ if dn is not None:
+ all_updates = self.__combine_updates(dn_list, all_updates, update)
+
+ update = {}
+ dn = line[3:].strip()
+ update['dn'] = self.__template_str(dn)
+ else:
+ if dn is None:
+ raise BadSyntax, "dn is not defined in the update"
+
+ if line.startswith(' '):
+ v = d[len(d) - 1]
+ v = v + " " + line.strip()
+ d[len(d) - 1] = v
+ update[index] = d
+ continue
+ line = line.strip()
+ values = line.split(':', 2)
+ if len(values) != 3:
+ raise BadSyntax, "Bad formatting on line %d: %s" % (lcount,line)
+
+ index = values[0].strip().lower()
+
+ if index not in valid_keywords:
+ raise BadSyntax, "Unknown keyword %s" % index
+
+ attr = values[1].strip()
+ value = values[2].strip()
+ value = self.__template_str(value)
+
+ new_value = ""
+ if index == "default":
+ new_value = attr + ":" + value
+ else:
+ new_value = index + ":" + attr + ":" + value
+ index = "updates"
+
+ d = update.get(index, [])
+
+ d.append(new_value)
+
+ update[index] = d
+
+ if dn is not None:
+ all_updates = self.__combine_updates(dn_list, all_updates, update)
+
+ return (all_updates, dn_list)
+
+ def create_index_task(self, attribute):
+ """Create a task to update an index for an attribute"""
+
+ r = random.SystemRandom()
+
+ # Refresh the time to make uniqueness more probable. Add on some
+ # randomness for good measure.
+ self.sub_dict['TIME'] = int(time.time()) + r.randint(0,10000)
+
+ cn = self.__template_str("indextask_$TIME")
+ dn = "cn=%s, cn=index, cn=tasks, cn=config" % cn
+
+ e = ipaldap.Entry(dn)
+
+ e.setValues('objectClass', ['top', 'extensibleObject'])
+ e.setValue('cn', cn)
+ e.setValue('nsInstance', 'userRoot')
+ e.setValues('nsIndexAttribute', attribute)
+
+ logging.info("Creating task to index attribute: %s", attribute)
+ logging.debug("Task id: %s", dn)
+
+ if self.live_run:
+ self.conn.addEntry(e.dn, e.toTupleList())
+
+ return dn
+
+ def monitor_index_task(self, dn):
+ """Give a task DN monitor it and wait until it has completed (or failed)
+ """
+
+ if not self.live_run:
+ # If not doing this live there is nothing to monitor
+ return
+
+ # Pause for a moment to give the task time to be created
+ time.sleep(1)
+
+ attrlist = ['nstaskstatus', 'nstaskexitcode']
+ entry = None
+
+ while True:
+ try:
+ entry = self.conn.getEntry(dn, ldap.SCOPE_BASE, "(objectclass=*)", attrlist)
+ except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND):
+ logging.error("Task not found: %s", dn)
+ return
+ except ipaerror.exception_for(ipaerror.LDAP_DATABASE_ERROR), e:
+ logging.error("Task lookup failure %s: %s", e, self.__detail_error(e.detail))
+ return
+
+ status = entry.getValue('nstaskstatus')
+ if status is None:
+ # task doesn't have a status yet
+ time.sleep(1)
+ continue
+
+ if status.lower().find("finished") > -1:
+ logging.info("Indexing finished")
+ break
+
+ logging.debug("Indexing in progress")
+ time.sleep(1)
+
+ return
+
+ def __create_default_entry(self, dn, default):
+ """Create the default entry from the values provided.
+
+ The return type is entity.Entity
+ """
+ entry = ipaldap.Entry(dn)
+
+ if not default:
+ # This means that the entire entry needs to be created with add
+ return self.__entry_to_entity(entry)
+
+ for line in default:
+ # We already do syntax-parsing so this is safe
+ (k, v) = line.split(':',1)
+ e = entry.getValues(k)
+ if e:
+ # multi-valued attribute
+ e = list(e)
+ e.append(v)
+ else:
+ e = v
+ entry.setValues(k, e)
+
+ return self.__entry_to_entity(entry)
+
+ def __get_entry(self, dn):
+ """Retrieve an object from LDAP.
+
+ The return type is ipaldap.Entry
+ """
+ searchfilter="objectclass=*"
+ sattrs = ["*"]
+ scope = ldap.SCOPE_BASE
+
+ return self.conn.getList(dn, scope, searchfilter, sattrs)
+
+ def __apply_updates(self, updates, entry):
+ """updates is a list of changes to apply
+ entry is the thing to apply them to
+
+ returns the modified entry
+ """
+ if not updates:
+ return entry
+
+ only = {}
+ for u in updates:
+ # We already do syntax-parsing so this is safe
+ (utype, k, values) = u.split(':',2)
+
+ values = self.__parse_values(values)
+
+ e = entry.getValues(k)
+ if not isinstance(e, list):
+ if e is None:
+ e = []
+ else:
+ e = [e]
+
+ for v in values:
+ if utype == 'remove':
+ logging.debug("remove: '%s' from %s, current value %s", v, k, e)
+ try:
+ e.remove(v)
+ except ValueError:
+ logging.warn("remove: '%s' not in %s", v, k)
+ pass
+ entry.setValues(k, e)
+ logging.debug('remove: updated value %s', e)
+ elif utype == 'add':
+ logging.debug("add: '%s' to %s, current value %s", v, k, e)
+ # Remove it, ignoring errors so we can blindly add it later
+ try:
+ e.remove(v)
+ except ValueError:
+ pass
+ e.append(v)
+ logging.debug('add: updated value %s', e)
+ entry.setValues(k, e)
+ elif utype == 'only':
+ logging.debug("only: set %s to '%s', current value %s", k, v, e)
+ if only.get(k):
+ e.append(v)
+ else:
+ e = [v]
+ only[k] = True
+ entry.setValues(k, e)
+ logging.debug('only: updated value %s', e)
+
+ self.print_entity(entry)
+
+ return entry
+
+ def print_entity(self, e, message=None):
+ """The entity object currently lacks a str() method"""
+ logging.debug("---------------------------------------------")
+ if message:
+ logging.debug("%s", message)
+ logging.debug("dn: " + e.dn)
+ attr = e.attrList()
+ for a in attr:
+ value = e.getValues(a)
+ if isinstance(value,str):
+ logging.debug(a + ": " + value)
+ else:
+ logging.debug(a + ": ")
+ for l in value:
+ logging.debug("\t" + l)
+ def is_schema_updated(self, s):
+ """Compare the schema in 's' with the current schema in the DS to
+ see if anything has changed. This should account for syntax
+ differences (like added parens that make no difference but are
+ detected as a change by generateModList()).
+
+ This doesn't handle re-ordering of attributes. They are still
+ detected as changes, so foo $ bar != bar $ foo.
+
+ return True if the schema has changed
+ return False if it has not
+ """
+ s = ldap.schema.SubSchema(s)
+ s = s.ldap_entry()
+
+ # Get a fresh copy and convert into a SubSchema
+ n = self.__get_entry("cn=schema")[0]
+ n = dict(n.data)
+ n = ldap.schema.SubSchema(n)
+ n = n.ldap_entry()
+
+ if s == n:
+ return False
+ else:
+ return True
+
+ def __update_record(self, update):
+ found = False
+
+ new_entry = self.__create_default_entry(update.get('dn'),
+ update.get('default'))
+
+ try:
+ e = self.__get_entry(new_entry.dn)
+ if len(e) > 1:
+ # we should only ever get back one entry
+ raise BadSyntax, "More than 1 entry returned on a dn search!? %s" % new_entry.dn
+ entry = self.__entry_to_entity(e[0])
+ found = True
+ logging.info("Updating existing entry: %s", entry.dn)
+ except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND):
+ # Doesn't exist, start with the default entry
+ entry = new_entry
+ logging.info("New entry: %s", entry.dn)
+ except ipaerror.exception_for(ipaerror.LDAP_DATABASE_ERROR):
+ # Doesn't exist, start with the default entry
+ entry = new_entry
+ logging.info("New entry, using default value: %s", entry.dn)
+
+ self.print_entity(entry)
+
+ # Bring this entry up to date
+ entry = self.__apply_updates(update.get('updates'), entry)
+
+ self.print_entity(entry, "Final value")
+
+ if not found:
+ # New entries get their orig_data set to the entry itself. We want to
+ # empty that so that everything appears new when generating the
+ # modlist
+ # entry.orig_data = {}
+ try:
+ if self.live_run:
+ self.conn.addEntry(entry.dn, entry.toTupleList())
+ except Exception, e:
+ logging.error("Add failure %s: %s", e, self.__detail_error(e.detail))
+ else:
+ # Update LDAP
+ try:
+ updated = False
+ changes = self.conn.generateModList(entry.origDataDict(), entry.toDict())
+ if (entry.dn == "cn=schema"):
+ updated = self.is_schema_updated(entry.toDict())
+ else:
+ if len(changes) > 1:
+ updated = True
+ logging.debug("%s" % changes)
+ if self.live_run and updated:
+ self.conn.updateEntry(entry.dn, entry.origDataDict(), entry.toDict())
+ logging.info("Done")
+ except ipaerror.exception_for(ipaerror.LDAP_EMPTY_MODLIST), e:
+ logging.info("Entry already up-to-date")
+ updated = False
+ except ipaerror.exception_for(ipaerror.LDAP_DATABASE_ERROR), e:
+ logging.error("Update failed: %s: %s", e, self.__detail_error(e.detail))
+ updated = False
+
+ if ("cn=index" in entry.dn and
+ "cn=userRoot" in entry.dn):
+ taskid = self.create_index_task(entry.cn)
+ self.monitor_index_task(taskid)
+
+ if updated:
+ self.modified = True
+ return
+
+ def get_all_files(self, root, recursive=False):
+ """Get all update files"""
+ f = []
+ for path, subdirs, files in os.walk(root):
+ for name in files:
+ if fnmatch.fnmatch(name, "*.update"):
+ f.append(os.path.join(path, name))
+ if not recursive:
+ break
+ return f
+
+ def update(self, files):
+ """Execute the update. files is a list of the update files to use.
+
+ returns True if anything was changed, otherwise False
+ """
+
+ try:
+ self.conn = ipaldap.IPAdmin(self.sub_dict['FQDN'])
+ self.conn.do_simple_bind(bindpw=self.dm_password)
+ all_updates = {}
+ dn_list = {}
+ for f in files:
+ try:
+ logging.info("Parsing file %s" % f)
+ data = self.read_file(f)
+ except Exception, e:
+ print e
+ sys.exit(1)
+
+ (all_updates, dn_list) = self.parse_update_file(data, all_updates, dn_list)
+
+ sortedkeys = dn_list.keys()
+ sortedkeys.sort()
+ for k in sortedkeys:
+ for dn in dn_list[k]:
+ self.__update_record(all_updates[dn])
+ finally:
+ if self.conn: self.conn.unbind()
+
+ return self.modified
diff --git a/ipa-server/ipaserver/ntpinstance.py b/ipa-server/ipaserver/ntpinstance.py
new file mode 100644
index 00000000..e2ec6065
--- /dev/null
+++ b/ipa-server/ipaserver/ntpinstance.py
@@ -0,0 +1,107 @@
+# Authors: Karl MacMillan <kmacmillan@redhat.com>
+#
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+import shutil
+import logging
+
+import service
+from ipa import sysrestore
+from ipa import ipautil
+
+class NTPInstance(service.Service):
+ def __init__(self, fstore=None):
+ service.Service.__init__(self, "ntpd")
+
+ if fstore:
+ self.fstore = fstore
+ else:
+ self.fstore = sysrestore.FileStore('/var/lib/ipa/sysrestore')
+
+ def __write_config(self):
+ # The template sets the config to point towards ntp.pool.org, but
+ # they request that software not point towards the default pool.
+ # We use the OS variable to point it towards either the rhel
+ # or fedora pools. Other distros should be added in the future
+ # or we can get our own pool.
+ os = ""
+ if ipautil.file_exists("/etc/fedora-release"):
+ os = "fedora"
+ elif ipautil.file_exists("/etc/redhat-release"):
+ os = "rhel"
+
+ sub_dict = { }
+ sub_dict["SERVERA"] = "0.%s.pool.ntp.org" % os
+ sub_dict["SERVERB"] = "1.%s.pool.ntp.org" % os
+ sub_dict["SERVERC"] = "2.%s.pool.ntp.org" % os
+
+ ntp_conf = ipautil.template_file(ipautil.SHARE_DIR + "ntp.conf.server.template", sub_dict)
+ ntp_sysconf = ipautil.template_file(ipautil.SHARE_DIR + "ntpd.sysconfig.template", {})
+
+ self.fstore.backup_file("/etc/ntp.conf")
+ self.fstore.backup_file("/etc/sysconfig/ntpd")
+
+ fd = open("/etc/ntp.conf", "w")
+ fd.write(ntp_conf)
+ fd.close()
+
+ fd = open("/etc/sysconfig/ntpd", "w")
+ fd.write(ntp_sysconf)
+ fd.close()
+
+ def __stop(self):
+ self.backup_state("running", self.is_running())
+ self.stop()
+
+ def __start(self):
+ self.start()
+
+ def __enable(self):
+ self.backup_state("enabled", self.is_enabled())
+ self.chkconfig_on()
+
+ def create_instance(self):
+
+ # we might consider setting the date manually using ntpd -qg in case
+ # the current time is very far off.
+
+ self.step("stopping ntpd", self.__stop)
+ self.step("writing configuration", self.__write_config)
+ self.step("configuring ntpd to start on boot", self.__enable)
+ self.step("starting ntpd", self.__start)
+
+ self.start_creation("Configuring ntpd")
+
+ def uninstall(self):
+ running = self.restore_state("running")
+ enabled = self.restore_state("enabled")
+
+ if not running is None:
+ self.stop()
+
+ try:
+ self.fstore.restore_file("/etc/ntp.conf")
+ except ValueError, error:
+ logging.debug(error)
+ pass
+
+ if not enabled is None and not enabled:
+ self.chkconfig_off()
+
+ if not running is None and running:
+ self.start()
diff --git a/ipa-server/ipaserver/replication.py b/ipa-server/ipaserver/replication.py
new file mode 100644
index 00000000..8477bd18
--- /dev/null
+++ b/ipa-server/ipaserver/replication.py
@@ -0,0 +1,532 @@
+# Authors: Karl MacMillan <kmacmillan@mentalrootkit.com>
+#
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+import time, logging
+
+import ipaldap, ldap, dsinstance
+from ldap import modlist
+from ipa import ipaerror
+
+DIRMAN_CN = "cn=directory manager"
+CACERT="/usr/share/ipa/html/ca.crt"
+# the default container used by AD for user entries
+WIN_USER_CONTAINER="cn=Users"
+# the default container used by IPA for user entries
+IPA_USER_CONTAINER="cn=users,cn=accounts"
+PORT = 636
+TIMEOUT = 120
+
+IPA_REPLICA = 1
+WINSYNC = 2
+
+class ReplicationManager:
+ """Manage replication agreements between DS servers, and sync
+ agreements with Windows servers"""
+ def __init__(self, hostname, dirman_passwd):
+ self.hostname = hostname
+ self.dirman_passwd = dirman_passwd
+
+ self.conn = ipaldap.IPAdmin(hostname, port=PORT, cacert=CACERT)
+ self.conn.do_simple_bind(bindpw=dirman_passwd)
+
+ self.repl_man_passwd = dirman_passwd
+
+ # these are likely constant, but you could change them
+ # at runtime if you really want
+ self.repl_man_dn = "cn=replication manager,cn=config"
+ self.repl_man_cn = "replication manager"
+ self.suffix = ""
+
+ def _get_replica_id(self, conn, master_conn):
+ """
+ Returns the replica ID which is unique for each backend.
+
+ conn is the connection we are trying to get the replica ID for.
+ master_conn is the master we are going to replicate with.
+ """
+ # First see if there is already one set
+ dn = self.replica_dn()
+ try:
+ replica = conn.search_s(dn, ldap.SCOPE_BASE, "objectclass=*")[0]
+ if replica.getValue('nsDS5ReplicaId'):
+ return int(replica.getValue('nsDS5ReplicaId'))
+ except ldap.NO_SUCH_OBJECT:
+ pass
+
+ # Ok, either the entry doesn't exist or the attribute isn't set
+ # so get it from the other master
+ retval = -1
+ dn = "cn=replication, cn=etc, %s" % self.suffix
+ try:
+ replica = master_conn.search_s(dn, ldap.SCOPE_BASE, "objectclass=*")[0]
+ if not replica.getValue('nsDS5ReplicaId'):
+ logging.debug("Unable to retrieve nsDS5ReplicaId from remote server")
+ raise RuntimeError("Unable to retrieve nsDS5ReplicaId from remote server")
+ except ldap.NO_SUCH_OBJECT:
+ logging.debug("Unable to retrieve nsDS5ReplicaId from remote server")
+ raise
+
+ # Now update the value on the master
+ retval = int(replica.getValue('nsDS5ReplicaId'))
+ mod = [(ldap.MOD_REPLACE, 'nsDS5ReplicaId', str(retval + 1))]
+
+ try:
+ master_conn.modify_s(dn, mod)
+ except Exception, e:
+ logging.debug("Problem updating nsDS5ReplicaID %s" % e)
+ raise
+
+ return retval
+
+ def find_replication_dns(self, conn):
+ filt = "(|(objectclass=nsDSWindowsReplicationAgreement)(objectclass=nsds5ReplicationAgreement))"
+ try:
+ ents = conn.search_s("cn=mapping tree,cn=config", ldap.SCOPE_SUBTREE, filt)
+ except ldap.NO_SUCH_OBJECT:
+ return []
+ return [ent.dn for ent in ents]
+
+ def add_replication_manager(self, conn, passwd=None):
+ """
+ Create a pseudo user to use for replication. If no password
+ is provided the directory manager password will be used.
+ """
+
+ if passwd:
+ self.repl_man_passwd = passwd
+
+ ent = ipaldap.Entry(self.repl_man_dn)
+ ent.setValues("objectclass", "top", "person")
+ ent.setValues("cn", self.repl_man_cn)
+ ent.setValues("userpassword", self.repl_man_passwd)
+ ent.setValues("sn", "replication manager pseudo user")
+
+ try:
+ conn.add_s(ent)
+ except ldap.ALREADY_EXISTS:
+ # should we set the password here?
+ pass
+
+ def delete_replication_manager(self, conn, dn="cn=replication manager,cn=config"):
+ try:
+ conn.delete_s(dn)
+ except ldap.NO_SUCH_OBJECT:
+ pass
+
+ def get_replica_type(self, master=True):
+ if master:
+ return "3"
+ else:
+ return "2"
+
+ def replica_dn(self):
+ return 'cn=replica, cn="%s", cn=mapping tree, cn=config' % self.suffix
+
+ def local_replica_config(self, conn, replica_id):
+ dn = self.replica_dn()
+
+ try:
+ conn.getEntry(dn, ldap.SCOPE_BASE)
+ # replication is already configured
+ return
+ except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND):
+ pass
+
+ replica_type = self.get_replica_type()
+
+ entry = ipaldap.Entry(dn)
+ entry.setValues('objectclass', "top", "nsds5replica", "extensibleobject")
+ entry.setValues('cn', "replica")
+ entry.setValues('nsds5replicaroot', self.suffix)
+ entry.setValues('nsds5replicaid', str(replica_id))
+ entry.setValues('nsds5replicatype', replica_type)
+ entry.setValues('nsds5flags', "1")
+ entry.setValues('nsds5replicabinddn', [self.repl_man_dn])
+ entry.setValues('nsds5replicalegacyconsumer', "off")
+
+ conn.add_s(entry)
+
+ def setup_changelog(self, conn):
+ dn = "cn=changelog5, cn=config"
+ dirpath = conn.dbdir + "/cldb"
+ entry = ipaldap.Entry(dn)
+ entry.setValues('objectclass', "top", "extensibleobject")
+ entry.setValues('cn', "changelog5")
+ entry.setValues('nsslapd-changelogdir', dirpath)
+ try:
+ conn.add_s(entry)
+ except ldap.ALREADY_EXISTS:
+ return
+
+ def setup_chaining_backend(self, conn):
+ chaindn = "cn=chaining database, cn=plugins, cn=config"
+ benamebase = "chaindb"
+ urls = [self.to_ldap_url(conn)]
+ cn = ""
+ benum = 1
+ done = False
+ while not done:
+ try:
+ cn = benamebase + str(benum) # e.g. localdb1
+ dn = "cn=" + cn + ", " + chaindn
+ entry = ipaldap.Entry(dn)
+ entry.setValues('objectclass', 'top', 'extensibleObject', 'nsBackendInstance')
+ entry.setValues('cn', cn)
+ entry.setValues('nsslapd-suffix', self.suffix)
+ entry.setValues('nsfarmserverurl', urls)
+ entry.setValues('nsmultiplexorbinddn', self.repl_man_dn)
+ entry.setValues('nsmultiplexorcredentials', self.repl_man_passwd)
+
+ self.conn.add_s(entry)
+ done = True
+ except ldap.ALREADY_EXISTS:
+ benum += 1
+ except ldap.LDAPError, e:
+ print "Could not add backend entry " + dn, e
+ raise
+
+ return cn
+
+ def to_ldap_url(self, conn):
+ return "ldap://%s:%d/" % (conn.host, conn.port)
+
+ def setup_chaining_farm(self, conn):
+ try:
+ conn.modify_s(self.suffix, [(ldap.MOD_ADD, 'aci',
+ [ "(targetattr = \"*\")(version 3.0; acl \"Proxied authorization for database links\"; allow (proxy) userdn = \"ldap:///%s\";)" % self.repl_man_dn ])])
+ except ldap.TYPE_OR_VALUE_EXISTS:
+ logging.debug("proxy aci already exists in suffix %s on %s" % (self.suffix, conn.host))
+
+ def get_mapping_tree_entry(self):
+ try:
+ entry = self.conn.getEntry("cn=mapping tree,cn=config", ldap.SCOPE_ONELEVEL,
+ "(cn=\"%s\")" % (self.suffix))
+ except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND), e:
+ logging.debug("failed to find mappting tree entry for %s" % self.suffix)
+ raise e
+
+ return entry
+
+
+ def enable_chain_on_update(self, bename):
+ mtent = self.get_mapping_tree_entry()
+ dn = mtent.dn
+
+ plgent = self.conn.getEntry("cn=Multimaster Replication Plugin,cn=plugins,cn=config",
+ ldap.SCOPE_BASE, "(objectclass=*)", ['nsslapd-pluginPath'])
+ path = plgent.getValue('nsslapd-pluginPath')
+
+ mod = [(ldap.MOD_REPLACE, 'nsslapd-state', 'backend'),
+ (ldap.MOD_ADD, 'nsslapd-backend', bename),
+ (ldap.MOD_ADD, 'nsslapd-distribution-plugin', path),
+ (ldap.MOD_ADD, 'nsslapd-distribution-funct', 'repl_chain_on_update')]
+
+ try:
+ self.conn.modify_s(dn, mod)
+ except ldap.TYPE_OR_VALUE_EXISTS:
+ logging.debug("chainOnUpdate already enabled for %s" % self.suffix)
+
+ def setup_chain_on_update(self, other_conn):
+ chainbe = self.setup_chaining_backend(other_conn)
+ self.enable_chain_on_update(chainbe)
+
+ def add_passsync_user(self, conn, password):
+ pass_dn = "uid=passsync,cn=sysaccounts,cn=etc,%s" % self.suffix
+ print "The user for the Windows PassSync service is %s" % pass_dn
+ try:
+ conn.getEntry(pass_dn, ldap.SCOPE_BASE)
+ print "Windows PassSync entry exists, not resetting password"
+ return
+ except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND):
+ pass
+
+ # The user doesn't exist, add it
+ entry = ipaldap.Entry(pass_dn)
+ entry.setValues("objectclass", ["account", "simplesecurityobject"])
+ entry.setValues("uid", "passsync")
+ entry.setValues("userPassword", password)
+ conn.add_s(entry)
+
+ # Add it to the list of users allowed to bypass password policy
+ extop_dn = "cn=ipa_pwd_extop,cn=plugins,cn=config"
+ entry = conn.getEntry(extop_dn, ldap.SCOPE_BASE)
+ pass_mgrs = entry.getValues('passSyncManagersDNs')
+ if not pass_mgrs:
+ pass_mgrs = []
+ if not isinstance(pass_mgrs, list):
+ pass_mgrs = [pass_mgrs]
+ pass_mgrs.append(pass_dn)
+ mod = [(ldap.MOD_REPLACE, 'passSyncManagersDNs', pass_mgrs)]
+ conn.modify_s(extop_dn, mod)
+
+ # And finally grant it permission to write passwords
+ mod = [(ldap.MOD_ADD, 'aci',
+ ['(targetattr = "userPassword || krbPrincipalKey || sambaLMPassword || sambaNTPassword || passwordHistory")(version 3.0; acl "Windows PassSync service can write passwords"; allow (write) userdn="ldap:///%s";)' % pass_dn])]
+ try:
+ conn.modify_s(self.suffix, mod)
+ except ldap.TYPE_OR_VALUE_EXISTS:
+ logging.debug("passsync aci already exists in suffix %s on %s" % (self.suffix, conn.host))
+
+ def setup_winsync_agmt(self, entry, **kargs):
+ entry.setValues("objectclass", "nsDSWindowsReplicationAgreement")
+ entry.setValues("nsds7WindowsReplicaSubtree",
+ kargs.get("win_subtree",
+ WIN_USER_CONTAINER + "," + self.suffix))
+ entry.setValues("nsds7DirectoryReplicaSubtree",
+ kargs.get("ds_subtree",
+ IPA_USER_CONTAINER + "," + self.suffix))
+ # for now, just sync users and ignore groups
+ entry.setValues("nsds7NewWinUserSyncEnabled", kargs.get('newwinusers', 'true'))
+ entry.setValues("nsds7NewWinGroupSyncEnabled", kargs.get('newwingroups', 'false'))
+ windomain = ''
+ if kargs.has_key('windomain'):
+ windomain = kargs['windomain']
+ else:
+ windomain = '.'.join(ldap.explode_dn(self.suffix, 1))
+ entry.setValues("nsds7WindowsDomain", windomain)
+
+ def agreement_dn(self, hostname, port=PORT):
+ cn = "meTo%s%d" % (hostname, port)
+ dn = "cn=%s, %s" % (cn, self.replica_dn())
+
+ return (cn, dn)
+
+ def setup_agreement(self, a, b, **kargs):
+ cn, dn = self.agreement_dn(b.host)
+ try:
+ a.getEntry(dn, ldap.SCOPE_BASE)
+ return
+ except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND):
+ pass
+
+ iswinsync = kargs.get("winsync", False)
+ repl_man_dn = kargs.get("binddn", self.repl_man_dn)
+ repl_man_passwd = kargs.get("bindpw", self.repl_man_passwd)
+ port = kargs.get("port", PORT)
+
+ entry = ipaldap.Entry(dn)
+ entry.setValues('objectclass', "nsds5replicationagreement")
+ entry.setValues('cn', cn)
+ entry.setValues('nsds5replicahost', b.host)
+ entry.setValues('nsds5replicaport', str(port))
+ entry.setValues('nsds5replicatimeout', str(TIMEOUT))
+ entry.setValues('nsds5replicabinddn', repl_man_dn)
+ entry.setValues('nsds5replicacredentials', repl_man_passwd)
+ entry.setValues('nsds5replicabindmethod', 'simple')
+ entry.setValues('nsds5replicaroot', self.suffix)
+ entry.setValues('nsds5replicaupdateschedule', '0000-2359 0123456')
+ entry.setValues('nsds5replicatransportinfo', 'SSL')
+ entry.setValues('nsDS5ReplicatedAttributeList', '(objectclass=*) $ EXCLUDE memberOf')
+ entry.setValues('description', "me to %s%d" % (b.host, port))
+ if iswinsync:
+ self.setup_winsync_agmt(entry, **kargs)
+
+ a.add_s(entry)
+
+ entry = a.waitForEntry(entry)
+
+ def delete_agreement(self, hostname):
+ cn, dn = self.agreement_dn(hostname)
+ return self.conn.deleteEntry(dn)
+
+ def check_repl_init(self, conn, agmtdn):
+ done = False
+ hasError = 0
+ attrlist = ['cn', 'nsds5BeginReplicaRefresh', 'nsds5replicaUpdateInProgress',
+ 'nsds5ReplicaLastInitStatus', 'nsds5ReplicaLastInitStart',
+ 'nsds5ReplicaLastInitEnd']
+ entry = conn.getEntry(agmtdn, ldap.SCOPE_BASE, "(objectclass=*)", attrlist)
+ if not entry:
+ print "Error reading status from agreement", agmtdn
+ hasError = 1
+ else:
+ refresh = entry.nsds5BeginReplicaRefresh
+ inprogress = entry.nsds5replicaUpdateInProgress
+ status = entry.nsds5ReplicaLastInitStatus
+ if not refresh: # done - check status
+ if not status:
+ print "No status yet"
+ elif status.find("replica busy") > -1:
+ print "[%s] reports: Replica Busy! Status: [%s]" % (conn.host, status)
+ done = True
+ hasError = 2
+ elif status.find("Total update succeeded") > -1:
+ print "Update succeeded"
+ done = True
+ elif inprogress.lower() == 'true':
+ print "Update in progress yet not in progress"
+ else:
+ print "[%s] reports: Update failed! Status: [%s]" % (conn.host, status)
+ hasError = 1
+ done = True
+ else:
+ print "Update in progress"
+
+ return done, hasError
+
+ def check_repl_update(self, conn, agmtdn):
+ done = False
+ hasError = 0
+ attrlist = ['cn', 'nsds5replicaUpdateInProgress',
+ 'nsds5ReplicaLastUpdateStatus', 'nsds5ReplicaLastUpdateStart',
+ 'nsds5ReplicaLastUpdateEnd']
+ entry = conn.getEntry(agmtdn, ldap.SCOPE_BASE, "(objectclass=*)", attrlist)
+ if not entry:
+ print "Error reading status from agreement", agmtdn
+ hasError = 1
+ else:
+ inprogress = entry.nsds5replicaUpdateInProgress
+ status = entry.nsds5ReplicaLastUpdateStatus
+ start = entry.nsds5ReplicaLastUpdateStart
+ end = entry.nsds5ReplicaLastUpdateEnd
+ # incremental update is done if inprogress is false and end >= start
+ done = inprogress and inprogress.lower() == 'false' and start and end and (start <= end)
+ logging.info("Replication Update in progress: %s: status: %s: start: %s: end: %s" %
+ (inprogress, status, start, end))
+ if not done and status: # check for errors
+ # status will usually be a number followed by a string
+ # number != 0 means error
+ rc, msg = status.split(' ', 1)
+ if rc != '0':
+ hasError = 1
+ done = True
+
+ return done, hasError
+
+ def wait_for_repl_init(self, conn, agmtdn):
+ done = False
+ haserror = 0
+ while not done and not haserror:
+ time.sleep(1) # give it a few seconds to get going
+ done, haserror = self.check_repl_init(conn, agmtdn)
+ return haserror
+
+ def wait_for_repl_update(self, conn, agmtdn, maxtries=600):
+ done = False
+ haserror = 0
+ while not done and not haserror and maxtries > 0:
+ time.sleep(1) # give it a few seconds to get going
+ done, haserror = self.check_repl_update(conn, agmtdn)
+ maxtries -= 1
+ if maxtries == 0: # too many tries
+ print "Error: timeout: could not determine agreement status: please check your directory server logs for possible errors"
+ haserror = 1
+ return haserror
+
+ def start_replication(self, other_conn, conn=None):
+ print "Starting replication, please wait until this has completed."
+ if conn == None:
+ conn = self.conn
+ cn, dn = self.agreement_dn(conn.host)
+
+ mod = [(ldap.MOD_ADD, 'nsds5BeginReplicaRefresh', 'start')]
+ other_conn.modify_s(dn, mod)
+
+ return self.wait_for_repl_init(other_conn, dn)
+
+ def basic_replication_setup(self, conn, replica_id):
+ self.add_replication_manager(conn)
+ self.local_replica_config(conn, replica_id)
+ self.setup_changelog(conn)
+
+ def setup_replication(self, other_hostname, realm_name, **kargs):
+ """
+ NOTES:
+ - the directory manager password needs to be the same on
+ both directories. Or use the optional binddn and bindpw
+ """
+ iswinsync = kargs.get("winsync", False)
+ oth_port = kargs.get("port", PORT)
+ oth_cacert = kargs.get("cacert", CACERT)
+ oth_binddn = kargs.get("binddn", DIRMAN_CN)
+ oth_bindpw = kargs.get("bindpw", self.dirman_passwd)
+ # note - there appears to be a bug in python-ldap - it does not
+ # allow connections using two different CA certs
+ other_conn = ipaldap.IPAdmin(other_hostname, port=oth_port, cacert=oth_cacert)
+ try:
+ other_conn.do_simple_bind(binddn=oth_binddn, bindpw=oth_bindpw)
+ except Exception, e:
+ if iswinsync:
+ logging.info("Could not validate connection to remote server %s:%d - continuing" %
+ (other_hostname, oth_port))
+ logging.info("The error was: %s" % e)
+ else:
+ raise e
+
+ self.suffix = ipaldap.IPAdmin.normalizeDN(dsinstance.realm_to_suffix(realm_name))
+
+ if not iswinsync:
+ local_id = self._get_replica_id(self.conn, other_conn)
+ else:
+ # there is no other side to get a replica ID from
+ local_id = self._get_replica_id(self.conn, self.conn)
+ self.basic_replication_setup(self.conn, local_id)
+
+ if not iswinsync:
+ other_id = self._get_replica_id(other_conn, other_conn)
+ self.basic_replication_setup(other_conn, other_id)
+ self.setup_agreement(other_conn, self.conn)
+ self.setup_agreement(self.conn, other_conn)
+ return self.start_replication(other_conn)
+ else:
+ self.add_passsync_user(self.conn, kargs.get("passsync"))
+ self.setup_agreement(self.conn, other_conn, **kargs)
+ logging.info("Added new sync agreement, waiting for it to become ready . . .")
+ cn, dn = self.agreement_dn(other_hostname)
+ self.wait_for_repl_update(self.conn, dn, 30)
+ logging.info("Agreement is ready, starting replication . . .")
+ return self.start_replication(self.conn, other_conn)
+
+ def initialize_replication(self, dn, conn):
+ mod = [(ldap.MOD_ADD, 'nsds5BeginReplicaRefresh', 'start')]
+ try:
+ conn.modify_s(dn, mod)
+ except ldap.ALREADY_EXISTS:
+ return
+
+ def force_synch(self, dn, schedule, conn):
+ newschedule = '2358-2359 0'
+
+ # On the remote chance of a match. We force a synch to happen right
+ # now by changing the schedule to something else and quickly changing
+ # it back.
+ if newschedule == schedule:
+ newschedule = '2358-2359 1'
+ logging.info("Changing agreement %s schedule to %s to force synch" %
+ (dn, newschedule))
+ mod = [(ldap.MOD_REPLACE, 'nsDS5ReplicaUpdateSchedule', [ newschedule ])]
+ conn.modify_s(dn, mod)
+ time.sleep(1)
+ logging.info("Changing agreement %s to restore original schedule %s" %
+ (dn, schedule))
+ mod = [(ldap.MOD_REPLACE, 'nsDS5ReplicaUpdateSchedule', [ schedule ])]
+ conn.modify_s(dn, mod)
+
+ def get_agreement_type(self, hostname):
+ cn, dn = self.agreement_dn(hostname)
+
+ entry = self.conn.getEntry(dn, ldap.SCOPE_BASE)
+
+ objectclass = entry.getValues("objectclass")
+
+ for o in objectclass:
+ if o.lower() == "nsdswindowsreplicationagreement":
+ return WINSYNC
+
+ return IPA_REPLICA
diff --git a/ipa-server/ipaserver/service.py b/ipa-server/ipaserver/service.py
new file mode 100644
index 00000000..b9f6c505
--- /dev/null
+++ b/ipa-server/ipaserver/service.py
@@ -0,0 +1,169 @@
+# Authors: Karl MacMillan <kmacmillan@mentalrootkit.com>
+#
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+import logging, sys
+from ipa import sysrestore
+from ipa import ipautil
+
+
+def stop(service_name):
+ ipautil.run(["/sbin/service", service_name, "stop"])
+
+def start(service_name):
+ ipautil.run(["/sbin/service", service_name, "start"])
+
+def restart(service_name):
+ ipautil.run(["/sbin/service", service_name, "restart"])
+
+def is_running(service_name):
+ ret = True
+ try:
+ ipautil.run(["/sbin/service", service_name, "status"])
+ except ipautil.CalledProcessError:
+ ret = False
+ return ret
+
+def chkconfig_on(service_name):
+ ipautil.run(["/sbin/chkconfig", service_name, "on"])
+
+def chkconfig_off(service_name):
+ ipautil.run(["/sbin/chkconfig", service_name, "off"])
+
+def chkconfig_add(service_name):
+ ipautil.run(["/sbin/chkconfig", "--add", service_name])
+
+def chkconfig_del(service_name):
+ ipautil.run(["/sbin/chkconfig", "--del", service_name])
+
+def is_enabled(service_name):
+ (stdout, stderr) = ipautil.run(["/sbin/chkconfig", "--list", service_name])
+
+ runlevels = {}
+ for runlevel in range(0, 7):
+ runlevels[runlevel] = False
+
+ for line in stdout.split("\n"):
+ parts = line.split()
+ if parts[0] == service_name:
+ for s in parts[1:]:
+ (runlevel, status) = s.split(":")[0:2]
+ try:
+ runlevels[int(runlevel)] = status == "on"
+ except ValueError:
+ pass
+ break
+
+ return (runlevels[3] and runlevels[4] and runlevels[5])
+
+def print_msg(message, output_fd=sys.stdout):
+ logging.debug(message)
+ output_fd.write(message)
+ output_fd.write("\n")
+
+
+class Service:
+ def __init__(self, service_name, sstore=None):
+ self.service_name = service_name
+ self.steps = []
+ self.output_fd = sys.stdout
+
+ if sstore:
+ self.sstore = sstore
+ else:
+ self.sstore = sysrestore.StateFile('/var/lib/ipa/sysrestore')
+
+ def set_output(self, fd):
+ self.output_fd = fd
+
+ def stop(self):
+ stop(self.service_name)
+
+ def start(self):
+ start(self.service_name)
+
+ def restart(self):
+ restart(self.service_name)
+
+ def is_running(self):
+ return is_running(self.service_name)
+
+ def chkconfig_add(self):
+ chkconfig_add(self.service_name)
+
+ def chkconfig_del(self):
+ chkconfig_del(self.service_name)
+
+ def chkconfig_on(self):
+ chkconfig_on(self.service_name)
+
+ def chkconfig_off(self):
+ chkconfig_off(self.service_name)
+
+ def is_enabled(self):
+ return is_enabled(self.service_name)
+
+ def backup_state(self, key, value):
+ self.sstore.backup_state(self.service_name, key, value)
+
+ def restore_state(self, key):
+ return self.sstore.restore_state(self.service_name, key)
+
+ def print_msg(self, message):
+ print_msg(message, self.output_fd)
+
+ def step(self, message, method):
+ self.steps.append((message, method))
+
+ def start_creation(self, message):
+ self.print_msg(message)
+
+ step = 0
+ for (message, method) in self.steps:
+ self.print_msg(" [%d/%d]: %s" % (step+1, len(self.steps), message))
+ method()
+ step += 1
+
+ self.print_msg("done configuring %s." % self.service_name)
+
+ self.steps = []
+
+class SimpleServiceInstance(Service):
+ def create_instance(self):
+ self.step("starting %s " % self.service_name, self.__start)
+ self.step("configuring %s to start on boot" % self.service_name, self.__enable)
+ self.start_creation("Configuring %s" % self.service_name)
+
+ def __start(self):
+ self.backup_state("running", self.is_running())
+ self.restart()
+
+ def __enable(self):
+ self.chkconfig_add()
+ self.backup_state("enabled", self.is_enabled())
+ self.chkconfig_on()
+
+ def uninstall(self):
+ running = self.restore_state("running")
+ enabled = not self.restore_state("enabled")
+
+ if not running is None and not running:
+ self.stop()
+ if not enabled is None and not enabled:
+ self.chkconfig_off()
+ self.chkconfig_del()
diff --git a/ipa-server/man/Makefile.am b/ipa-server/man/Makefile.am
new file mode 100644
index 00000000..244b06b8
--- /dev/null
+++ b/ipa-server/man/Makefile.am
@@ -0,0 +1,27 @@
+# This file will be processed with automake-1.7 to create Makefile.in
+
+AUTOMAKE_OPTIONS = 1.7
+
+NULL=
+
+man1_MANS = \
+ ipa-replica-install.1 \
+ ipa-replica-manage.1 \
+ ipa-replica-prepare.1 \
+ ipa-server-certinstall.1 \
+ ipa-server-install.1 \
+ ipa-ldap-updater.1 \
+ ipa-compat-manage.1
+
+man8_MANS = \
+ ipactl.8 \
+ ipa_kpasswd.8 \
+ ipa_webgui.8
+
+install-data-hook:
+ @for i in $(man1_MANS) ; do gzip -f $(DESTDIR)$(man1dir)/$$i ; done
+ @for i in $(man8_MANS) ; do gzip -f $(DESTDIR)$(man8dir)/$$i ; done
+
+MAINTAINERCLEANFILES = \
+ Makefile.in \
+ $(NULL)
diff --git a/ipa-server/man/ipa-compat-manage.1 b/ipa-server/man/ipa-compat-manage.1
new file mode 100644
index 00000000..767384a4
--- /dev/null
+++ b/ipa-server/man/ipa-compat-manage.1
@@ -0,0 +1,45 @@
+.\" A man page for ipa-ldap-updater
+.\" Copyright (C) 2008 Red Hat, Inc.
+.\"
+.\" This is free software; you can redistribute it and/or modify it under
+.\" the terms of the GNU Library General Public License as published by
+.\" the Free Software Foundation; version 2 only
+.\"
+.\" This program is distributed in the hope that it will be useful, but
+.\" WITHOUT ANY WARRANTY; without even the implied warranty of
+.\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+.\" General Public License for more details.
+.\"
+.\" You should have received a copy of the GNU Library General Public
+.\" License along with this program; if not, write to the Free Software
+.\" Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+.\"
+.\" Author: Simo Sorce <ssorce@redhat.com>
+.\"
+.TH "ipa-compat-manage" "1" "Dec 2 2008" "freeipa" ""
+.SH "NAME"
+ipa\-compat\-manage \- Enables or disables the schema compatibility plugin
+.SH "SYNOPSIS"
+ipa\-compat\-manage [options] <enable|disable>
+.SH "DESCRIPTION"
+Run the command with the \fBenable\fR option to enable the compat plugin.
+
+Run the command with the \fBdisable\fR option to disable the compat plugin.
+
+In both cases the user will be prompted to provide the Directory Manager's password unless option \fB\-y\fR is used.
+
+Directory Server will need to be restarted after the schema compatibility plugin has been enabled.
+
+.SH "OPTIONS"
+.TP
+\fB\-d\fR, \fB\-\-debug\fR
+Enable debug logging when more verbose output is needed
+.TP
+\fB\-y\fR \fIfile\fR
+File containing the Directory Manager password
+.SH "EXIT STATUS"
+0 if the command was successful
+
+1 if an error occurred
+
+2 if the plugin is already in the required status (enabled or disabled)
diff --git a/ipa-server/man/ipa-ldap-updater.1 b/ipa-server/man/ipa-ldap-updater.1
new file mode 100644
index 00000000..453ac758
--- /dev/null
+++ b/ipa-server/man/ipa-ldap-updater.1
@@ -0,0 +1,78 @@
+.\" A man page for ipa-ldap-updater
+.\" Copyright (C) 2008 Red Hat, Inc.
+.\"
+.\" This is free software; you can redistribute it and/or modify it under
+.\" the terms of the GNU Library General Public License as published by
+.\" the Free Software Foundation; version 2 only
+.\"
+.\" This program is distributed in the hope that it will be useful, but
+.\" WITHOUT ANY WARRANTY; without even the implied warranty of
+.\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+.\" General Public License for more details.
+.\"
+.\" You should have received a copy of the GNU Library General Public
+.\" License along with this program; if not, write to the Free Software
+.\" Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+.\"
+.\" Author: Rob Crittenden <rcritten@redhat.com>
+.\"
+.TH "ipa-ldap-updater" "1" "Sep 12 2008" "freeipa" ""
+.SH "NAME"
+ipa\-ldap\-updater \- Update the IPA LDAP configuration
+.SH "SYNOPSIS"
+ipa\-ldap\-updater [options] input_file(s)
+ipa\-ldap\-updater [options]
+.SH "DESCRIPTION"
+Run with no file arguments, ipa\-ldap\-updater will process all files with the extension .update in /usr/share/ipa/updates.
+
+An update file describes an LDAP entry and a set of operations to be performed on that entry. It can be used to add new entries or modify existing entries. It cannot remove entries, just specific values in a given attribute.
+
+Blank lines and lines beginning with # are ignored.
+
+There are 4 keywords:
+
+ * default: the starting value
+ * add: add a value (or values) to an attribute
+ * remove: remove a value (or values) from an attribute
+ * only: set an attribute to this
+
+Values is a comma\-separated field so multi\-values may be added at one time. Double or single quotes may be put around individual values that contain embedded commas.
+
+The difference between the default and add keywords is if the DN of the entry exists then default is ignored. So for updating something like schema, which will be under cn=schema, you must always use add (because cn=schema is guaranteed to exist). It will not re\-add the same information again and again.
+
+It alsos provide some things that can be templated such as architecture (for plugin paths), realm and domain name.
+
+The available template variables are:
+
+ * $REALM \- the kerberos realm (EXAMPLE.COM)
+ * $FQDN \- the fully\-qualified domain name of the IPA server being updated (ipa.example.com)
+ * $DOMAIN \- the domain name (example.com)
+ * $SUFFIX \- the IPA LDAP suffix (dc=example,dc=com)
+ * $LIBARCH \- set to 64 on x86_64 systems to be used for plugin paths
+ * $TIME \- an integer representation of current time
+
+A few rules:
+
+ 1. Only one rule per line
+ 2. Each line stands alone (e.g. an only followed by an only results in the last only being used)
+ 3. adding a value that exists is ok. The request is ignored, duplicate values are not added
+ 4. removing a value that doesn't exist is ok. It is simply ignored.
+ 5. If a DN doesn't exist it is created from the 'default' entry and all updates are applied
+ 6. If a DN does exist the default values are skipped
+ 7. Only the first rule on a line is respected
+.SH "OPTIONS"
+.TP
+\fB\-d\fR, \fB\-\-debug
+Enable debug logging when more verbose output is needed
+.TP
+\fB\-t\fR, \fB\-\-test\fR
+Run through the update without changing anything. If changes are available then the command returns 2. If no updates are available it returns 0.
+.TP
+\fB\-y\fR
+File containing the Directory Manager password
+.SH "EXIT STATUS"
+0 if the command was successful
+
+1 if an error occurred
+
+2 if run with in test mode (\-t) and updates are available
diff --git a/ipa-server/man/ipa-replica-install.1 b/ipa-server/man/ipa-replica-install.1
new file mode 100644
index 00000000..674afd12
--- /dev/null
+++ b/ipa-server/man/ipa-replica-install.1
@@ -0,0 +1,41 @@
+.\" A man page for ipa-replica-install
+.\" Copyright (C) 2008 Red Hat, Inc.
+.\"
+.\" This is free software; you can redistribute it and/or modify it under
+.\" the terms of the GNU Library General Public License as published by
+.\" the Free Software Foundation; version 2 only
+.\"
+.\" This program is distributed in the hope that it will be useful, but
+.\" WITHOUT ANY WARRANTY; without even the implied warranty of
+.\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+.\" General Public License for more details.
+.\"
+.\" You should have received a copy of the GNU Library General Public
+.\" License along with this program; if not, write to the Free Software
+.\" Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+.\"
+.\" Author: Rob Crittenden <rcritten@redhat.com>
+.\"
+.TH "ipa-replica-install" "1" "Mar 14 2008" "freeipa" ""
+.SH "NAME"
+ipa\-replica\-install \- Create an IPA replica
+.SH "SYNOPSIS"
+ipa\-replica\-install [\fIOPTION\fR]... replica_file
+.SH "DESCRIPTION"
+Configures a new IPA server that is a replica of the server that generated it. Once it has been created it is an exact copy of the original IPA server and is an equal master. Changes made to any master are automatically replicated to other masters.
+
+The replica_file is created using the ipa\-replica\-prepare utility.
+.SH "OPTIONS"
+.TP
+\fB\-d\fR, \fB\-\-debug
+Enable debug logging when more verbose output is needed
+.TP
+\fB\-n\fR, \fB\-\-no\-ntp\fR
+Do not configure NTP
+.TP
+\fB\-p\fR, \fB\-\-password\fR=\fIDM_PASSWORD\fR
+Directory Manager (existing master) password
+.SH "EXIT STATUS"
+0 if the command was successful
+
+1 if an error occurred
diff --git a/ipa-server/man/ipa-replica-manage.1 b/ipa-server/man/ipa-replica-manage.1
new file mode 100644
index 00000000..810cf1de
--- /dev/null
+++ b/ipa-server/man/ipa-replica-manage.1
@@ -0,0 +1,70 @@
+.\" A man page for ipa-replica-manage
+.\" Copyright (C) 2008 Red Hat, Inc.
+.\"
+.\" This is free software; you can redistribute it and/or modify it under
+.\" the terms of the GNU Library General Public License as published by
+.\" the Free Software Foundation; version 2 only
+.\"
+.\" This program is distributed in the hope that it will be useful, but
+.\" WITHOUT ANY WARRANTY; without even the implied warranty of
+.\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+.\" General Public License for more details.
+.\"
+.\" You should have received a copy of the GNU Library General Public
+.\" License along with this program; if not, write to the Free Software
+.\" Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+.\"
+.\" Author: Rob Crittenden <rcritten@redhat.com>
+.\"
+.TH "ipa-replica-manage" "1" "Mar 14 2008" "freeipa" ""
+.SH "NAME"
+ipa\-replica\-manage \- Manage an IPA replica
+.SH "SYNOPSIS"
+ipa\-replica\-manage [\fIOPTION\fR]... [add|del|list|init|synch] [SERVER]
+.SH "DESCRIPTION"
+Manages the replication agreements of an IPA server.
+.TP
+add \- Adds a new replication agreement between two existing IPA servers
+.TP
+del \- Removes a replication agreement
+.TP
+list \- Lists the hostnames that HOST IPA server has agreements with
+.TP
+init \- Forces a full initialization of the IPA server on SERVER from HOST
+.TP
+synch \- Immediately flush any data to be replicated to SERVER
+.SH "OPTIONS"
+.TP
+\fB\-H HOST\fR, \fB\-\-host\fR=\fIHOST\fR
+The IPA server to manage
+.TP
+\fB\-p DM_PASSWORD\fR, \fB\-\-password\fR=\fIDM_PASSWORD\fR
+The Directory Manager password to use for authentication
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+Provide additional information
+.TP
+\fB\-\-winsync\fR
+Specifies to create/use a Windows Sync Agreement
+.TP
+\fB\-\-port\fR=\fISERVER_PORT\fR
+Port number of other server (default is 636, the LDAPS port)
+.TP
+\fB\-\-binddn\fR=\fIADMIN_DN\fR
+Bind DN to use with remote server (default is cn=Directory Manager) - Be careful to quote this value on the command line
+.TP
+\fB--bindpw\fR=\fIADMIN_PWD\fR
+Password for Bind DN to use with remote server (default is the DM_PASSWORD above)
+.TP
+\fB\-\-cacert\fR=\fI/path/to/cacertfile\fR
+Full path and filename of CA certificate to use with TLS/SSL to the remote server - this CA certificate will be installed in the directory server's certificate database
+.TP
+\fB\-\-win-subtree\fR=\fIcn=Users,dc=example,dc=com\fR
+DN of Windows subtree containing the users you want to sync (default cn=Users,<domain suffix> - this is typically what Windows AD uses as the default value) - Be careful to quote this value on the command line
+.TP
+\fB\-\-passsync\fR=\fIPASSSYNC_PWD\fR
+Password for the Windows PassSync user.
+.SH "EXIT STATUS"
+0 if the command was successful
+
+1 if an error occurred
diff --git a/ipa-server/man/ipa-replica-prepare.1 b/ipa-server/man/ipa-replica-prepare.1
new file mode 100644
index 00000000..8eb49444
--- /dev/null
+++ b/ipa-server/man/ipa-replica-prepare.1
@@ -0,0 +1,48 @@
+.\" A man page for ipa-replica-prepare
+.\" Copyright (C) 2008 Red Hat, Inc.
+.\"
+.\" This is free software; you can redistribute it and/or modify it under
+.\" the terms of the GNU Library General Public License as published by
+.\" the Free Software Foundation; version 2 only
+.\"
+.\" This program is distributed in the hope that it will be useful, but
+.\" WITHOUT ANY WARRANTY; without even the implied warranty of
+.\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+.\" General Public License for more details.
+.\"
+.\" You should have received a copy of the GNU Library General Public
+.\" License along with this program; if not, write to the Free Software
+.\" Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+.\"
+.\" Author: Rob Crittenden <rcritten@redhat.com>
+.\"
+.TH "ipa-replica-prepare" "1" "Mar 14 2008" "freeipa" ""
+.SH "NAME"
+ipa\-replica\-prepare \- Create an IPA replica file
+.SH "SYNOPSIS"
+ipa\-replica\-prepare [\fIOPTION\fR]... hostname
+.SH "DESCRIPTION"
+Generates a replica file that may be used with ipa\-replica\-install to create a replica of an IPA server.
+
+A replica can only be created on an IPA server installed with ipa\-server\-install (the first server).
+
+You must provide the fully\-qualified hostname of the machine you want to install the replica on and a host\-specific replica_file will be created. It is host\-specific because SSL server certificates are generated as part of the process and they are specific to a particular hostname.
+
+Once the file has been created it will be named replica\-hostname. This file can then be moved across the network to the target machine and a new IPA replica setup by running ipa\-replica\-install replica\-hostname.
+.SH "OPTIONS"
+.TP
+\fB\-\-dirsrv_pkcs12\fR=\fIFILE\fR
+PKCS#12 file containing the Directory Server SSL Certificate
+.TP
+\fB\-\-http_pkcs12\fR=\fIFILE\fR
+PKCS#12 file containing the Apache Server SSL Certificate
+.TP
+\fB\-\-dirsrv_pin\fR=\fIDIRSRV_PIN\fR
+The password of the Directory Server PKCS#12 file
+.TP
+\fB\-\-http_pin\fR=\fIHTTP_PIN\fR
+The password of the Apache Server PKCS#12 file
+.SH "EXIT STATUS"
+0 if the command was successful
+
+1 if an error occurred
diff --git a/ipa-server/man/ipa-server-certinstall.1 b/ipa-server/man/ipa-server-certinstall.1
new file mode 100644
index 00000000..946ab9f8
--- /dev/null
+++ b/ipa-server/man/ipa-server-certinstall.1
@@ -0,0 +1,48 @@
+.\" A man page for ipa-server-certinstall
+.\" Copyright (C) 2008 Red Hat, Inc.
+.\"
+.\" This is free software; you can redistribute it and/or modify it under
+.\" the terms of the GNU Library General Public License as published by
+.\" the Free Software Foundation; version 2 only
+.\"
+.\" This program is distributed in the hope that it will be useful, but
+.\" WITHOUT ANY WARRANTY; without even the implied warranty of
+.\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+.\" General Public License for more details.
+.\"
+.\" You should have received a copy of the GNU Library General Public
+.\" License along with this program; if not, write to the Free Software
+.\" Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+.\"
+.\" Author: Rob Crittenden <rcritten@redhat.com>
+.\"
+.TH "ipa-server-certinstall" "1" "Mar 14 2008" "freeipa" ""
+.SH "NAME"
+ipa\-server\-certinstall \- Install new SSL server certificates
+.SH "SYNOPSIS"
+ipa\-server\-certinstall [\fIOPTION\fR]... PKCS12_FILE
+.SH "DESCRIPTION"
+Replace the current SSL Directory and/or Apache server certificate(s) with the certificate in the PKCS#12 file.
+
+PKCS#12 is a file format used to safely transport SSL certificates and public/private keypairs.
+
+They may be generated and managed using the NSS pk12util command or the OpenSSL pkcs12 command.
+
+The service(s) are not automatically restarted. In order to use the newly installed certificate(s) you will need to manually restart the Directory and/or Apache servers.
+.SH "OPTIONS"
+.TP
+\fB\-d\fR, \fB\-\-dirsrv\fR
+Install the certificate on the Directory Server
+.TP
+\fB\-w\fR, \fB\-\-http\fR
+Install the certificate in the Apache Web Server
+.TP
+\fB\-\-dirsrv_pin\fR=\fIDIRSRV_PIN\fR
+The password of the Directory Server PKCS#12 file
+.TP
+\fB\-\-http_pin\fR=\fIHTTP_PIN\fR
+The password of the Apache Server PKCS#12 file
+.SH "EXIT STATUS"
+0 if the installation was successful
+
+1 if an error occurred
diff --git a/ipa-server/man/ipa-server-install.1 b/ipa-server/man/ipa-server-install.1
new file mode 100644
index 00000000..8854f4e5
--- /dev/null
+++ b/ipa-server/man/ipa-server-install.1
@@ -0,0 +1,81 @@
+.\" A man page for ipa-server-install
+.\" Copyright (C) 2008 Red Hat, Inc.
+.\"
+.\" This is free software; you can redistribute it and/or modify it under
+.\" the terms of the GNU Library General Public License as published by
+.\" the Free Software Foundation; version 2 only
+.\"
+.\" This program is distributed in the hope that it will be useful, but
+.\" WITHOUT ANY WARRANTY; without even the implied warranty of
+.\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+.\" General Public License for more details.
+.\"
+.\" You should have received a copy of the GNU Library General Public
+.\" License along with this program; if not, write to the Free Software
+.\" Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+.\"
+.\" Author: Rob Crittenden <rcritten@redhat.com>
+.\"
+.TH "ipa-server-install" "1" "Mar 14 2008" "freeipa" ""
+.SH "NAME"
+ipa\-server\-install \- Configure an IPA server
+.SH "SYNOPSIS"
+ipa\-server\-install [\fIOPTION\fR]...
+.SH "DESCRIPTION"
+Configures the services needed by an IPA server. This includes setting up a Kerberos Key Distribution Center (KDC) with an LDAP back\-end, configuring Apache, configuring NTP and starting some IPA\-provided services: ipa_kpasswd and ipa_webgui.
+.SH "OPTIONS"
+.TP
+\fB\-u\fR, \fB\-\-user\fR=\fIDS_USER\fR
+The user that the Directory Server will run as
+.TP
+\fB\-r\fR, \fB\-\-realm\fR=\fIREALM_NAME\fR
+The Kerberos realm name for the IPA server
+.TP
+\fB\-n\fR, \fB\-\-domain\fR=\fIDOMAIN_NAME\fR
+Your DNS domain name
+.TP
+\fB\-p\fR, \fB\-\-ds\-password\fR=\fIDM_PASSWORD\fR
+The password to be used by the Directory Server for the Directory Manager user
+.TP
+\fB\-P\fR, \fB\-\-master\-password\fR=\fIMASTER_PASSWORD\fR
+The kerberos master password (normally autogenerated)
+.TP
+\fB\-a\fR, \fB\-\-admin\-password\fR=\fIADMIN_PASSWORD\fR
+The password for the IPA admin user
+.TP
+\fB\-d\fR, \fB\-\-debug\fR
+Enable debug logging when more verbose output is needed
+.TP
+\fB\-\-hostname\fR=\fIHOST_NAME\fR
+The fully\-qualified DNS name of this server
+.TP
+\fB\-\-ip\-address\fR=\fIIP_ADDRESS\fR
+The IP address of this server
+.TP
+\fB\-U\fR, \fB\-\-unattended\fR
+An unattended installation that will never prompt for user input
+.TP
+\fB\-\-setup\-bind\fR
+Generate a DNS zone file that contains auto\-discovery records for this IPA server
+.TP
+\fB\-n\fR, \fB\-\-no\-ntp\fR
+Do not configure NTP
+\fB\-U\fR, \fB\-\-uninstall\fR
+Uninstall an existing IPA installation
+.TP
+\fB\-\-dirsrv_pkcs12\fR=\fIFILE\fR
+PKCS#12 file containing the Directory Server SSL Certificate
+.TP
+\fB\-\-http_pkcs12\fR=\fIFILE\fR
+PKCS#12 file containing the Apache Server SSL Certificate
+.TP
+\fB\-\-dirsrv_pin\fR=\fIDIRSRV_PIN\fR
+The password of the Directory Server PKCS#12 file
+.TP
+\fB\-\-http_pin\fR=\fIHTTP_PIN\fR
+The password of the Apache Server PKCS#12 file
+.PP
+.SH "EXIT STATUS"
+0 if the installation was successful
+
+1 if an error occurred
diff --git a/ipa-server/man/ipa_kpasswd.8 b/ipa-server/man/ipa_kpasswd.8
new file mode 100644
index 00000000..f2ba3dd9
--- /dev/null
+++ b/ipa-server/man/ipa_kpasswd.8
@@ -0,0 +1,36 @@
+.\" A man page for ipa_kpasswd
+.\" Copyright (C) 2008 Red Hat, Inc.
+.\"
+.\" This is free software; you can redistribute it and/or modify it under
+.\" the terms of the GNU Library General Public License as published by
+.\" the Free Software Foundation; version 2 only
+.\"
+.\" This program is distributed in the hope that it will be useful, but
+.\" WITHOUT ANY WARRANTY; without even the implied warranty of
+.\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+.\" General Public License for more details.
+.\"
+.\" You should have received a copy of the GNU Library General Public
+.\" License along with this program; if not, write to the Free Software
+.\" Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+.\"
+.\" Author: Rob Crittenden <rcritten@redhat.com>
+.\"
+.TH "ipa_kpasswd" "8" "Mar 14 2008" "freeipa" ""
+.SH "NAME"
+ipa_kpasswd \- Proxy Kerberos password change requests
+.SH "SYNOPSIS"
+ipa_kpasswd
+.SH "DESCRIPTION"
+Implementation of the kpasswd protocol (RFC 3244).
+
+It is used to proxy password change operations to Directory Server.
+.SH "ENVIRONMENT VARIABLES"
+.TP
+KRB5_KTNAME
+Location of the keytab to be used by ipa_kpasswd
+.TP
+IPA_KPASSWD_DEBUG
+Enable additional syslog output from ipa_kpasswd. Setting greater than 0 gets basic output. Setting higher than 100 gets more.
+.SH "EXIT STATUS"
+\-1 if an error occurred
diff --git a/ipa-server/man/ipa_webgui.8 b/ipa-server/man/ipa_webgui.8
new file mode 100644
index 00000000..20545363
--- /dev/null
+++ b/ipa-server/man/ipa_webgui.8
@@ -0,0 +1,37 @@
+.\" A man page for ipa_webgui
+.\" Copyright (C) 2008 Red Hat, Inc.
+.\"
+.\" This is free software; you can redistribute it and/or modify it under
+.\" the terms of the GNU Library General Public License as published by
+.\" the Free Software Foundation; version 2 only
+.\"
+.\" This program is distributed in the hope that it will be useful, but
+.\" WITHOUT ANY WARRANTY; without even the implied warranty of
+.\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+.\" General Public License for more details.
+.\"
+.\" You should have received a copy of the GNU Library General Public
+.\" License along with this program; if not, write to the Free Software
+.\" Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+.\"
+.\" Author: Rob Crittenden <rcritten@redhat.com>
+.\"
+.TH "ipa_webgui" "8" "Mar 14 2008" "freeipa" ""
+.SH "NAME"
+ipa_webgui \- Start the IPA Web User Interface
+.SH "SYNOPSIS"
+ipa_webgui [\fIOPTION\fR]...
+
+.SH "DESCRIPTION"
+Used to start the TurboGears web user interface for IPA
+.SH "OPTIONS"
+.TP
+\fB\-f\fR, \fB\-\-foreground\fR
+Remain in the foreground instead of becoming a daemon.
+.TP
+\fB\-d\fR, \fB\-\-debug\fR
+.TP
+Increase the amount of logging and print it to stdout instead of logging to /var/log/ipa_error.log
+
+.SH "EXIT STATUS"
+1 if an error occurred
diff --git a/ipa-server/man/ipactl.8 b/ipa-server/man/ipactl.8
new file mode 100644
index 00000000..a4797f96
--- /dev/null
+++ b/ipa-server/man/ipactl.8
@@ -0,0 +1,37 @@
+.\" A man page for ipactl
+.\" Copyright (C) 2008 Red Hat, Inc.
+.\"
+.\" This is free software; you can redistribute it and/or modify it under
+.\" the terms of the GNU Library General Public License as published by
+.\" the Free Software Foundation; version 2 only
+.\"
+.\" This program is distributed in the hope that it will be useful, but
+.\" WITHOUT ANY WARRANTY; without even the implied warranty of
+.\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+.\" General Public License for more details.
+.\"
+.\" You should have received a copy of the GNU Library General Public
+.\" License along with this program; if not, write to the Free Software
+.\" Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+.\"
+.\" Author: Rob Crittenden <rcritten@redhat.com>
+.\"
+.TH "ipactl" "8" "Mar 14 2008" "freeipa" ""
+.SH "NAME"
+ipactl \- IPA Server Control Interface
+.SH "SYNOPSIS"
+ipactl \fIcommand\fR
+.SH "DESCRIPTION"
+A tool to help an administer control an IPA environment.
+
+IPA glues several discrete services together to work in concert and the order that these services are started and stopped is important. ipactl ensures that they are started and stopped in the correct order.
+.SH "OPTIONS"
+.TP
+start
+Start all of the services that make up IPA
+.TP
+stop
+Stop all of the services that make up IPA
+.TP
+restart
+Stop then start all of the services that make up IPA
diff --git a/ipa-server/selinux/Makefile b/ipa-server/selinux/Makefile
new file mode 100644
index 00000000..a662d2fd
--- /dev/null
+++ b/ipa-server/selinux/Makefile
@@ -0,0 +1,28 @@
+SUBDIRS = ipa_webgui ipa_kpasswd
+POLICY_MAKEFILE = /usr/share/selinux/devel/Makefile
+POLICY_DIR = $(DESTDIR)/usr/share/selinux/targeted
+
+all:
+ if [ ! -e $(POLICY_MAKEFILE) ]; then echo "You need to install the SELinux development tools (selinux-policy-devel)" && exit 1; fi
+
+ @for subdir in $(SUBDIRS); do \
+ (cd $$subdir && $(MAKE) -f $(POLICY_MAKEFILE) $@) || exit 1; \
+ done
+
+clean:
+ @for subdir in $(SUBDIRS); do \
+ (cd $$subdir && $(MAKE) -f $(POLICY_MAKEFILE) $@) || exit 1; \
+ done
+
+distclean: clean
+ rm -f ipa-server-selinux.spec
+
+maintainer-clean: distclean
+
+install: all
+ install -d $(POLICY_DIR)
+ install -m 644 ipa_webgui/ipa_webgui.pp $(POLICY_DIR)
+ install -m 644 ipa_kpasswd/ipa_kpasswd.pp $(POLICY_DIR)
+
+load:
+ /usr/sbin/semodule -i ipa_webgui/ipa_webgui.pp ipa_kpasswd/ipa_kpasswd.pp
diff --git a/ipa-server/selinux/ipa-server-selinux.spec.in b/ipa-server/selinux/ipa-server-selinux.spec.in
new file mode 100644
index 00000000..3387553a
--- /dev/null
+++ b/ipa-server/selinux/ipa-server-selinux.spec.in
@@ -0,0 +1,86 @@
+%define POLICYCOREUTILSVER 1.33.12-1
+
+Name: ipa-server-selinux
+Version: __VERSION__
+Release: __RELEASE__%{?dist}
+Summary: IPA server SELinux policies
+
+Group: System Environment/Base
+License: GPLv2
+URL: http://www.freeipa.org
+Source0: ipa-server-%{version}.tgz
+BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n)
+BuildArch: noarch
+
+BuildRequires: selinux-policy-devel m4 make policycoreutils >= %{POLICYCOREUTILSVER}
+Requires(pre): policycoreutils >= %{POLICYCOREUTILSVER} libsemanage
+
+%description
+SELinux policy for ipa-server
+
+%prep
+%setup -n ipa-server-%{version} -q
+
+%build
+cd selinux
+make
+
+%clean
+%{__rm} -fR %{buildroot}
+
+%install
+%{__rm} -fR %{buildroot}
+cd selinux
+install -d %{buildroot}/%{_usr}/share/selinux/targeted/
+make DESTDIR=%{buildroot} install
+
+%files
+%{_usr}/share/selinux/targeted/ipa_webgui.pp
+%{_usr}/share/selinux/targeted/ipa_kpasswd.pp
+
+
+%define saveFileContext() \
+if [ -s /etc/selinux/config ]; then \
+ . %{_sysconfdir}/selinux/config; \
+ FILE_CONTEXT=%{_sysconfdir}/selinux/%1/contexts/files/file_contexts; \
+ if [ "${SELINUXTYPE}" == %1 -a -f ${FILE_CONTEXT} ]; then \
+ cp -f ${FILE_CONTEXT} ${FILE_CONTEXT}.%{name}; \
+ fi \
+fi;
+
+%define relabel() \
+. %{_sysconfdir}/selinux/config; \
+FILE_CONTEXT=%{_sysconfdir}/selinux/%1/contexts/files/file_contexts; \
+selinuxenabled; \
+if [ $? == 0 -a "${SELINUXTYPE}" == %1 -a -f ${FILE_CONTEXT}.%{name} ]; then \
+ fixfiles -C ${FILE_CONTEXT}.%{name} restore; \
+ rm -f ${FILE_CONTEXT}.%name; \
+fi;
+
+%pre
+%saveFileContext targeted
+
+%post
+semodule -s targeted -i /usr/share/selinux/targeted/ipa_webgui.pp /usr/share/selinux/targeted/ipa_kpasswd.pp
+%relabel targeted
+
+%preun
+if [ $1 = 0 ]; then
+%saveFileContext targeted
+fi
+
+%postun
+if [ $1 = 0 ]; then
+semodule -s targeted -r ipa_webgui ipa_kpasswd
+%relabel targeted
+fi
+
+%changelog
+* Thu Apr 3 2008 Rob Crittenden <rcritten@redhat.com> - 1.0.0-1
+- Version bump for release
+
+* Thu Feb 21 2008 Rob Crittenden <rcritten@redhat.com> - 0.99.0-1
+- Version bump for release
+
+* Thu Jan 17 2008 Karl MacMillan <kmacmill@redhat.com> - 0.6.0-1
+- Initial version
diff --git a/ipa-server/selinux/ipa_kpasswd/ipa_kpasswd.fc b/ipa-server/selinux/ipa_kpasswd/ipa_kpasswd.fc
new file mode 100644
index 00000000..2dcf827d
--- /dev/null
+++ b/ipa-server/selinux/ipa_kpasswd/ipa_kpasswd.fc
@@ -0,0 +1,9 @@
+#
+# /usr
+#
+/usr/sbin/ipa_kpasswd -- gen_context(system_u:object_r:ipa_kpasswd_exec_t,s0)
+
+#
+# /var
+#
+/var/cache/ipa/kpasswd(/.*)? gen_context(system_u:object_r:ipa_kpasswd_ccache_t,s0)
diff --git a/ipa-server/selinux/ipa_kpasswd/ipa_kpasswd.te b/ipa-server/selinux/ipa_kpasswd/ipa_kpasswd.te
new file mode 100644
index 00000000..b5203a4e
--- /dev/null
+++ b/ipa-server/selinux/ipa_kpasswd/ipa_kpasswd.te
@@ -0,0 +1,71 @@
+policy_module(ipa_kpasswd, 1.0)
+
+########################################
+#
+# Declarations
+#
+
+type ipa_kpasswd_t;
+type ipa_kpasswd_exec_t;
+type ipa_kpasswd_var_run_t;
+type ipa_kpasswd_ccache_t;
+init_daemon_domain(ipa_kpasswd_t, ipa_kpasswd_exec_t)
+
+########################################
+#
+# IPA kpasswd local policy
+#
+
+allow ipa_kpasswd_t self:capability { sys_nice dac_override };
+allow ipa_kpasswd_t self:tcp_socket create_stream_socket_perms;
+allow ipa_kpasswd_t self:udp_socket create_socket_perms;
+
+files_read_etc_files(ipa_kpasswd_t)
+files_search_usr(ipa_kpasswd_t)
+
+files_pid_file(ipa_kpasswd_var_run_t);
+allow ipa_kpasswd_t ipa_kpasswd_var_run_t:file manage_file_perms;
+files_pid_filetrans(ipa_kpasswd_t,ipa_kpasswd_var_run_t,file)
+
+auth_use_nsswitch(ipa_kpasswd_t)
+
+libs_use_ld_so(ipa_kpasswd_t)
+libs_use_shared_libs(ipa_kpasswd_t)
+
+logging_send_syslog_msg(ipa_kpasswd_t)
+
+miscfiles_read_localization(ipa_kpasswd_t)
+
+kerberos_use(ipa_kpasswd_t)
+kerberos_manage_host_rcache(ipa_kpasswd_t)
+kerberos_read_kdc_config(ipa_kpasswd_t)
+
+kernel_read_system_state(ipa_kpasswd_t)
+
+# /var/cache/ipa/kpasswd
+files_type(ipa_kpasswd_ccache_t)
+manage_dirs_pattern(ipa_kpasswd_t, ipa_kpasswd_ccache_t, ipa_kpasswd_ccache_t)
+manage_files_pattern(ipa_kpasswd_t, ipa_kpasswd_ccache_t, ipa_kpasswd_ccache_t)
+files_var_filetrans(ipa_kpasswd_t, ipa_kpasswd_ccache_t,dir)
+
+kernel_read_network_state(ipa_kpasswd_t)
+kernel_read_network_state_symlinks(ipa_kpasswd_t)
+
+corenet_tcp_sendrecv_all_if(ipa_kpasswd_t)
+corenet_udp_sendrecv_all_if(ipa_kpasswd_t)
+corenet_raw_sendrecv_all_if(ipa_kpasswd_t)
+corenet_tcp_sendrecv_all_nodes(ipa_kpasswd_t)
+corenet_udp_sendrecv_all_nodes(ipa_kpasswd_t)
+corenet_raw_sendrecv_all_nodes(ipa_kpasswd_t)
+corenet_tcp_sendrecv_all_ports(ipa_kpasswd_t)
+corenet_udp_sendrecv_all_ports(ipa_kpasswd_t)
+corenet_non_ipsec_sendrecv(ipa_kpasswd_t)
+corenet_tcp_bind_all_nodes(ipa_kpasswd_t)
+corenet_udp_bind_all_nodes(ipa_kpasswd_t)
+corenet_tcp_bind_kerberos_admin_port(ipa_kpasswd_t)
+corenet_udp_bind_kerberos_admin_port(ipa_kpasswd_t)
+require {
+ type krb5kdc_conf_t;
+};
+
+allow ipa_kpasswd_t krb5kdc_conf_t:dir search_dir_perms;
diff --git a/ipa-server/selinux/ipa_webgui/ipa_webgui.fc b/ipa-server/selinux/ipa_webgui/ipa_webgui.fc
new file mode 100644
index 00000000..c9dfb2b5
--- /dev/null
+++ b/ipa-server/selinux/ipa_webgui/ipa_webgui.fc
@@ -0,0 +1,11 @@
+#
+# /usr
+#
+/usr/sbin/ipa_webgui -- gen_context(system_u:object_r:ipa_webgui_exec_t,s0)
+
+
+#
+# /var
+#
+/var/log/ipa_error\.log -- gen_context(system_u:object_r:ipa_webgui_log_t,s0)
+/var/cache/ipa/sessions(/.*)? gen_context(system_u:object_r:ipa_cache_t,s0)
diff --git a/ipa-server/selinux/ipa_webgui/ipa_webgui.te b/ipa-server/selinux/ipa_webgui/ipa_webgui.te
new file mode 100644
index 00000000..a9818d82
--- /dev/null
+++ b/ipa-server/selinux/ipa_webgui/ipa_webgui.te
@@ -0,0 +1,97 @@
+policy_module(ipa_webgui, 1.0)
+
+########################################
+#
+# Declarations
+#
+
+require {
+ type sbin_t;
+}
+type ipa_webgui_t;
+type ipa_webgui_exec_t;
+type ipa_webgui_var_run_t;
+type ipa_cache_t;
+files_type(ipa_cache_t)
+init_daemon_domain(ipa_webgui_t, ipa_webgui_exec_t)
+
+type ipa_webgui_log_t;
+logging_log_file(ipa_webgui_log_t)
+
+########################################
+#
+# IPA webgui local policy
+#
+
+allow ipa_webgui_t self:tcp_socket create_stream_socket_perms;
+allow ipa_webgui_t self:udp_socket create_socket_perms;
+allow ipa_webgui_t self:process setfscreate;
+
+# This is how the kerberos credential cache is passed to
+# the ipa_webgui process. Unfortunately, the kerberos
+# libraries seem to insist that it be open rw. To top it
+# all off there is no interface for this either.
+require {
+ type httpd_tmp_t;
+}
+allow ipa_webgui_t httpd_tmp_t:file read_file_perms;
+dontaudit ipa_webgui_t httpd_tmp_t:file write;
+
+apache_search_sys_content(ipa_webgui_t)
+apache_read_config(ipa_webgui_t)
+
+corecmd_list_bin(ipa_webgui_t)
+
+miscfiles_read_localization(ipa_webgui_t)
+
+files_list_usr(ipa_webgui_t)
+files_read_etc_files(ipa_webgui_t)
+files_read_usr_files(ipa_webgui_t)
+files_read_usr_symlinks(ipa_webgui_t)
+files_search_etc(ipa_webgui_t)
+files_search_tmp(ipa_webgui_t)
+
+files_pid_file(ipa_webgui_var_run_t)
+allow ipa_webgui_t ipa_webgui_var_run_t:file manage_file_perms;
+files_pid_filetrans(ipa_webgui_t,ipa_webgui_var_run_t,file)
+
+kerberos_read_config(ipa_webgui_t)
+
+kernel_read_system_state(ipa_webgui_t)
+
+auth_use_nsswitch(ipa_webgui_t)
+
+libs_use_ld_so(ipa_webgui_t)
+libs_use_shared_libs(ipa_webgui_t)
+
+logging_search_logs(ipa_webgui_t)
+logging_log_filetrans(ipa_webgui_t,ipa_webgui_log_t,file)
+allow ipa_webgui_t ipa_webgui_log_t:file rw_file_perms;
+
+allow ipa_webgui_t self:capability { setgid setuid };
+
+# /var/cache/ipa/sessions
+files_type(ipa_cache_t)
+manage_dirs_pattern(ipa_webgui_t, ipa_cache_t, ipa_cache_t)
+manage_files_pattern(ipa_webgui_t, ipa_cache_t, ipa_cache_t)
+files_var_filetrans(ipa_webgui_t, ipa_cache_t,dir)
+
+userdom_dontaudit_search_sysadm_home_dirs(ipa_webgui_t)
+
+corenet_tcp_sendrecv_all_if(ipa_webgui_t)
+corenet_udp_sendrecv_all_if(ipa_webgui_t)
+corenet_raw_sendrecv_all_if(ipa_webgui_t)
+corenet_tcp_sendrecv_all_nodes(ipa_webgui_t)
+corenet_udp_sendrecv_all_nodes(ipa_webgui_t)
+corenet_raw_sendrecv_all_nodes(ipa_webgui_t)
+corenet_tcp_sendrecv_all_ports(ipa_webgui_t)
+corenet_udp_sendrecv_all_ports(ipa_webgui_t)
+corenet_non_ipsec_sendrecv(ipa_webgui_t)
+corenet_tcp_bind_all_nodes(ipa_webgui_t)
+corenet_udp_bind_all_nodes(ipa_webgui_t)
+corenet_tcp_bind_http_cache_port(ipa_webgui_t)
+corenet_tcp_connect_http_cache_port(ipa_webgui_t)
+corenet_tcp_connect_ldap_port(ipa_webgui_t)
+
+corecmd_search_sbin(ipa_webgui_t)
+allow ipa_webgui_t sbin_t:dir read;
diff --git a/ipa-server/version.m4.in b/ipa-server/version.m4.in
new file mode 100644
index 00000000..5ddc8cea
--- /dev/null
+++ b/ipa-server/version.m4.in
@@ -0,0 +1 @@
+define([IPA_VERSION], [__VERSION__])
diff --git a/ipa-server/xmlrpc-server/Makefile.am b/ipa-server/xmlrpc-server/Makefile.am
new file mode 100644
index 00000000..49457ba4
--- /dev/null
+++ b/ipa-server/xmlrpc-server/Makefile.am
@@ -0,0 +1,38 @@
+NULL =
+
+SUBDIRS = \
+ test \
+ $(NULL)
+
+htmldir = $(IPA_DATA_DIR)/html
+html_DATA = \
+ ssbrowser.html \
+ unauthorized.html \
+ $(NULL)
+
+coredir = $(pythondir)/ipaserver
+core_PYTHON = \
+ attrs.py \
+ funcs.py \
+ $(NULL)
+
+serverdir = $(IPA_DATA_DIR)/ipaserver
+server_PYTHON = \
+ ipaxmlrpc.py \
+ $(NULL)
+
+appdir = $(IPA_DATA_DIR)
+app_DATA = \
+ ipa.conf \
+ ipa-rewrite.conf \
+ $(NULL)
+
+EXTRA_DIST = \
+ README \
+ $(app_DATA) \
+ $(html_DATA) \
+ $(NULL)
+
+MAINTAINERCLEANFILES = \
+ *~ \
+ Makefile.in
diff --git a/ipa-server/xmlrpc-server/README b/ipa-server/xmlrpc-server/README
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/ipa-server/xmlrpc-server/README
diff --git a/ipa-server/xmlrpc-server/attrs.py b/ipa-server/xmlrpc-server/attrs.py
new file mode 100644
index 00000000..415744a2
--- /dev/null
+++ b/ipa-server/xmlrpc-server/attrs.py
@@ -0,0 +1,53 @@
+# Authors: Rob Crittenden <rcritten@redhat.com>
+#
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+attr_label_list = {
+ "givenname":"First Name",
+ "sn":"Last Name",
+ "cn":"Full Name",
+ "title":"Job Title",
+ "displayname":"Display Name",
+ "initials":"Initials",
+ "uid":"Login",
+ "krbprincipalkey":"Password",
+ "uidnumber":"UID",
+ "gidnumber":"GID",
+ "homedirectory":"Home Directory",
+ "loginshell":"Login Shell",
+ "gecos":"GECOS",
+ "mail":"E-mail Address",
+ "telephonenumber":"Work Number",
+ "facsimiletelephonenumber":"Fax Number",
+ "mobile":"Cell Number",
+ "homephone":"Home Number",
+ "street":"Street Address",
+ "l":"City",
+ "st":"State",
+ "postalcode":"ZIP",
+ "ou":"Org Unit",
+ "businesscategory":"Tags",
+ "description":"Description",
+ "employeetype":"Employee Type",
+ "manager":"Manager",
+ "roomnumber":"Room Number",
+ "secretary":"Secretary",
+ "carlicense":"Car License",
+ "labelduri":"Home Page",
+ "nsaccountlock":"Account Status"
+}
diff --git a/ipa-server/xmlrpc-server/funcs.py b/ipa-server/xmlrpc-server/funcs.py
new file mode 100644
index 00000000..cf9e7de5
--- /dev/null
+++ b/ipa-server/xmlrpc-server/funcs.py
@@ -0,0 +1,2291 @@
+# Authors: Rob Crittenden <rcritten@redhat.com>
+#
+# Copyright (C) 2007 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+import sys
+
+import krbV
+import ldap
+import ldap.dn
+import ipaserver.dsinstance
+import ipaserver.ipaldap
+import copy
+from ipaserver import attrs
+from ipa import version
+from ipa import ipaerror
+from ipa import ipautil
+from urllib import quote,unquote
+from ipa import radius_util
+from ipa import dnsclient
+
+import string
+from types import *
+import re
+import logging
+import subprocess
+
+try:
+ from threading import Lock
+except ImportError:
+ from dummy_threading import Lock
+
+# Need a global to store this between requests
+_LDAPPool = None
+
+ACIContainer = "cn=accounts"
+DefaultUserContainer = "cn=users,cn=accounts"
+DefaultGroupContainer = "cn=groups,cn=accounts"
+DefaultServiceContainer = "cn=services,cn=accounts"
+
+#
+# Apache runs in multi-process mode so each process will have its own
+# connection. This could theoretically drive the total number of connections
+# very high but since this represents just the administrative interface
+# this is not anticipated.
+#
+# The pool consists of two things, a dictionary keyed on the principal name
+# that contains the connection and a list that is used to keep track of the
+# order. If the list fills up just pop the top entry off and you've got
+# the least recently used.
+
+# maxsize = 0 means no limit
+class IPAConnPool:
+ def __init__(self, maxsize = 0):
+ self._dict = {}
+ self._lru = []
+ self._lock = Lock()
+ self._maxsize = maxsize
+ self._ctx = krbV.default_context()
+
+ def getConn(self, host, port, krbccache=None, debug=None):
+ conn = None
+
+ ccache = krbV.CCache(name=krbccache, context=self._ctx)
+ cprinc = ccache.principal()
+
+ conn = ipaserver.ipaldap.IPAdmin(host,port,None,None,None,debug)
+
+ # This will bind the connection
+ try:
+ conn.set_krbccache(krbccache, cprinc.name)
+ except ldap.UNWILLING_TO_PERFORM:
+ raise ipaerror.gen_exception(ipaerror.CONNECTION_UNWILLING)
+ except Exception, e:
+ raise ipaerror.gen_exception(ipaerror.CONNECTION_NO_CONN, nested_exception=e)
+
+ return conn
+
+ def releaseConn(self, conn):
+ if conn is None:
+ return
+
+ conn.unbind_s()
+
+class IPAServer:
+
+ def __init__(self):
+ global _LDAPPool
+ # FIXME, this needs to be auto-discovered
+ self.host = 'localhost'
+ self.port = 389
+ self.sslport = 636
+ self.bindcert = "/usr/share/ipa/cert.pem"
+ self.bindkey = "/usr/share/ipa/key.pem"
+ self.bindca = "/usr/share/ipa/cacert.asc"
+ self.krbctx = krbV.default_context()
+ self.realm = self.krbctx.default_realm
+
+ if _LDAPPool is None:
+ _LDAPPool = IPAConnPool(128)
+ self.basedn = ipautil.realm_to_suffix(self.realm)
+ self.accountsdn = "cn=accounts," + self.basedn
+ self.scope = ldap.SCOPE_SUBTREE
+ self.princ = None
+ self.krbccache = None
+
+ def set_principal(self, princ):
+ self.princ = princ
+
+ def set_krbccache(self, krbccache):
+ self.krbccache = krbccache
+
+ def get_dn_from_principal(self, princ, debug):
+ """Given a kerberos principal get the LDAP uid"""
+ global _LDAPPool
+
+ princ = self.__safe_filter(princ)
+ searchfilter = "(krbPrincipalName=" + princ + ")"
+ # The only anonymous search we should have
+ conn = _LDAPPool.getConn(self.host,self.sslport,self.bindca,self.bindcert,self.bindkey,None,None,debug)
+ try:
+ ent = conn.getEntry(self.accountsdn, self.scope, searchfilter, ['dn'])
+ finally:
+ _LDAPPool.releaseConn(conn)
+
+ return "dn:" + ent.dn
+
+ def __setup_connection(self, opts):
+ """Set up common things done in the connection.
+ If there is a Kerberos credentials cache then return None as the
+ proxy dn and the ccache otherwise return the proxy dn and None as
+ the ccache.
+
+ We only want one or the other used at one time and we prefer
+ the Kerberos credentials cache. So if there is a ccache, return
+ that and None for proxy dn to make calling getConn() easier.
+ """
+
+ debug = "Off"
+
+ if opts is not None:
+ debug = opts.get('ipadebug')
+ if opts.get('krbccache'):
+ self.set_krbccache(opts['krbccache'])
+ self.set_principal(None)
+ else:
+ self.set_krbccache(None)
+ self.set_principal(opts['remoteuser'])
+ else:
+ # The caller should have already set the principal or the
+ # krbccache. If not they'll get an authentication error later.
+ pass
+
+ if self.princ is not None:
+ return self.get_dn_from_principal(self.princ, debug), None, debug
+ else:
+ return None, self.krbccache, debug
+
+ def getConnection(self, opts):
+ """Wrapper around IPAConnPool.getConn() so we don't have to pass
+ around self.* every time a connection is needed.
+
+ For SASL connections (where we have a krbccache) we can't set
+ the SSL variables for certificates. It confuses the ldap
+ module.
+ """
+ global _LDAPPool
+
+ (proxy_dn, krbccache, debug) = self.__setup_connection(opts)
+
+ if krbccache is not None:
+ bindca = None
+ bindcert = None
+ bindkey = None
+ port = self.port
+ else:
+ raise ipaerror.gen_exception(ipaerror.CONNECTION_NO_CCACHE)
+
+ try:
+ conn = _LDAPPool.getConn(self.host,port,krbccache,debug)
+ except ldap.INVALID_CREDENTIALS, e:
+ raise ipaerror.gen_exception(ipaerror.CONNECTION_GSSAPI_CREDENTIALS, nested_exception=e)
+
+ if conn is None:
+ raise ipaerror.gen_exception(ipaerror.CONNECTION_NO_CONN)
+
+ return conn
+
+ def releaseConnection(self, conn):
+ global _LDAPPool
+
+ _LDAPPool.releaseConn(conn)
+
+ def convert_entry(self, ent):
+ entry = dict(ent.data)
+ entry['dn'] = ent.dn
+ # For now convert single entry lists to a string for the ui.
+ # TODO: we need to deal with multi-values better
+ for key,value in entry.iteritems():
+ if isinstance(value,list) or isinstance(value,tuple):
+ if len(value) == 0:
+ entry[key] = ''
+ elif len(value) == 1:
+ entry[key] = value[0]
+ return entry
+
+ # TODO: rethink the get_entry vs get_list API calls.
+ # they currently restrict the data coming back without
+ # restricting scope. For now adding a __get_base/sub_entry()
+ # calls, but the API isn't great.
+ def __get_entry (self, base, scope, searchfilter, sattrs=None, opts=None):
+ """Get a specific entry (with a parametized scope).
+ Return as a dict of values.
+ Multi-valued fields are represented as lists.
+ """
+ ent=""
+
+ conn = self.getConnection(opts)
+ try:
+ ent = conn.getEntry(base, scope, searchfilter, sattrs)
+
+ finally:
+ self.releaseConnection(conn)
+
+ return self.convert_entry(ent)
+
+ def __get_base_entry (self, base, searchfilter, sattrs=None, opts=None):
+ """Get a specific entry (with a scope of BASE).
+ Return as a dict of values.
+ Multi-valued fields are represented as lists.
+ """
+ return self.__get_entry(base, ldap.SCOPE_BASE, searchfilter, sattrs, opts)
+
+ def __get_sub_entry (self, base, searchfilter, sattrs=None, opts=None):
+ """Get a specific entry (with a scope of SUB).
+ Return as a dict of values.
+ Multi-valued fields are represented as lists.
+ """
+ return self.__get_entry(base, ldap.SCOPE_SUBTREE, searchfilter, sattrs, opts)
+
+ def __get_list (self, base, searchfilter, sattrs=None, opts=None):
+ """Gets a list of entries. Each is converted to a dict of values.
+ Multi-valued fields are represented as lists.
+ """
+ entries = []
+
+ conn = self.getConnection(opts)
+ try:
+ entries = conn.getList(base, self.scope, searchfilter, sattrs)
+ finally:
+ self.releaseConnection(conn)
+
+ return map(self.convert_entry, entries)
+
+ def __update_entry (self, oldentry, newentry, opts=None):
+ """Update an LDAP entry
+
+ oldentry is a dict
+ newentry is a dict
+ """
+ oldentry = self.convert_scalar_values(oldentry)
+ newentry = self.convert_scalar_values(newentry)
+
+ # Should be able to get this from either the old or new entry
+ # but just in case someone has decided to try changing it, use the
+ # original
+ try:
+ moddn = oldentry['dn']
+ except KeyError:
+ raise ipaerror.gen_exception(ipaerror.LDAP_MISSING_DN)
+
+ conn = self.getConnection(opts)
+ try:
+ res = conn.updateEntry(moddn, oldentry, newentry)
+ finally:
+ self.releaseConnection(conn)
+ return res
+
+ def __safe_filter(self, criteria):
+ """Make sure any arguments used when creating a filter are safe."""
+
+ # TODO: this escaper assumes the python-ldap library will error out
+ # on invalid codepoints. we need to check malformed utf-8 input
+ # where the second byte in a multi-byte character
+ # is (illegally) ')' and make sure python-ldap
+ # bombs out.
+ criteria = re.sub(r'[\(\)\\\*]', ldap_search_escape, criteria)
+
+ return criteria
+
+ def __generate_match_filters(self, search_fields, criteria_words):
+ """Generates a search filter based on a list of words and a list
+ of fields to search against.
+
+ Returns a tuple of two filters: (exact_match, partial_match)"""
+
+ # construct search pattern for a single word
+ # (|(f1=word)(f2=word)...)
+ search_pattern = "(|"
+ for field in search_fields:
+ search_pattern += "(" + field + "=%(match)s)"
+ search_pattern += ")"
+ gen_search_pattern = lambda word: search_pattern % {'match':word}
+
+ # construct the giant match for all words
+ exact_match_filter = "(&"
+ partial_match_filter = "(|"
+ for word in criteria_words:
+ exact_match_filter += gen_search_pattern(word)
+ partial_match_filter += gen_search_pattern("*%s*" % word)
+ exact_match_filter += ")"
+ partial_match_filter += ")"
+
+ return (exact_match_filter, partial_match_filter)
+
+ def __get_schema(self, opts=None):
+ """Retrieves the current LDAP schema from the LDAP server."""
+
+ schema_entry = self.__get_base_entry("", "objectclass=*", ['dn','subschemasubentry'], opts)
+ schema_cn = schema_entry.get('subschemasubentry')
+ schema = self.__get_base_entry(schema_cn, "objectclass=*", ['*'], opts)
+
+ return schema
+
+ def __get_objectclasses(self, opts=None):
+ """Returns a list of available objectclasses that the LDAP
+ server supports. This parses out the syntax, attributes, etc
+ and JUST returns a lower-case list of the names."""
+
+ schema = self.__get_schema(opts)
+
+ objectclasses = schema.get('objectclasses')
+
+ # Convert this list into something more readable
+ result = []
+ for i in range(len(objectclasses)):
+ oc = objectclasses[i].lower().split(" ")
+ result.append(oc[3].replace("'",""))
+
+ return result
+
+ def __has_nsaccountlock(self, dn, opts):
+ """Check to see if an entry has the nsaccountlock attribute.
+ This attribute is provided by the Class of Service plugin so
+ doing a search isn't enough. It is provided by the two
+ entries cn=inactivated and cn=activated. So if the entry has
+ the attribute and isn't in either cn=activated or cn=inactivated
+ then the attribute must be in the entry itself.
+
+ Returns True or False
+ """
+ # First get the entry. If it doesn't have nsaccountlock at all we
+ # can exit early.
+ entry = self.get_entry_by_dn(dn, ['dn', 'nsaccountlock', 'memberof'], opts)
+ if not entry.get('nsaccountlock'):
+ return False
+
+ # Now look to see if they are in activated or inactivated
+ # entry is a member
+ memberof = entry.get('memberof')
+ if isinstance(memberof, basestring):
+ memberof = [memberof]
+ for m in memberof:
+ inactivated = m.find("cn=inactivated")
+ activated = m.find("cn=activated")
+ # if they are in either group that means that the nsaccountlock
+ # value comes from there, otherwise it must be in this entry.
+ if inactivated >= 0 or activated >= 0:
+ return False
+
+ return True
+
+# Higher-level API
+ def version(self, opts=None):
+ """The version of IPA"""
+ logging.debug("IPA: version %d" % version.NUM_VERSION)
+ return version.NUM_VERSION
+
+ def get_aci_entry(self, sattrs, opts=None):
+ """Returns the entry containing access control ACIs."""
+
+ if sattrs is not None and not isinstance(sattrs,list):
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ logging.info("IPA: get_aci_entry")
+
+ dn="%s,%s" % (ACIContainer, self.basedn)
+ return self.get_entry_by_dn(dn, sattrs, opts)
+
+# General searches
+
+ def get_entry_by_dn (self, dn, sattrs, opts=None):
+ """Get a specific entry. Return as a dict of values.
+ Multi-valued fields are represented as lists.
+ """
+ if not isinstance(dn,basestring) or len(dn) == 0:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if sattrs is not None and not isinstance(sattrs,list):
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+
+ searchfilter = "(objectClass=*)"
+ logging.info("IPA: get_entry_by_dn '%s'" % dn)
+ return self.__get_base_entry(dn, searchfilter, sattrs, opts)
+
+ def get_entry_by_cn (self, cn, sattrs, opts=None):
+ """Get a specific entry by cn. Return as a dict of values.
+ Multi-valued fields are represented as lists.
+ """
+
+ if not isinstance(cn,basestring) or len(cn) == 0:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if sattrs is not None and not isinstance(sattrs,list):
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+
+ logging.info("IPA: get_entry_by_cn '%s'" % cn)
+ cn = self.__safe_filter(cn)
+ searchfilter = "(cn=" + cn + ")"
+ return self.__get_sub_entry(self.accountsdn, searchfilter, sattrs, opts)
+
+ def update_entry (self, oldentry, newentry, opts=None):
+ """Update an entry in LDAP
+
+ oldentry and newentry are XML-RPC structs.
+
+ If oldentry is not empty then it is used when determine what
+ has changed.
+
+ If oldentry is empty then the value of newentry is compared
+ to the current value of oldentry.
+ """
+ if not newentry:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+
+ if not oldentry:
+ oldentry = self.get_entry_by_dn(newentry.get('dn'), None, opts)
+ if oldentry is None:
+ raise ipaerror.gen_exception(ipaerror.LDAP_NOT_FOUND)
+
+ logging.info("IPA: update_entry '%s'" % newentry.get('dn'))
+ return self.__update_entry(oldentry, newentry, opts)
+
+# User support
+
+ def __is_user_unique(self, uid, opts):
+ """Return True if the uid is unique in the tree, False otherwise."""
+ uid = self.__safe_filter(uid)
+ searchfilter = "(&(uid=%s)(objectclass=posixAccount))" % uid
+
+ try:
+ entry = self.__get_sub_entry(self.accountsdn, searchfilter, ['dn','uid'], opts)
+ return False
+ except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND):
+ return True
+
+ def __uid_too_long(self, uid, opts):
+ """Verify that the new uid is within the limits we set. This is a
+ very narrow test.
+
+ Returns True if it is longer than allowed
+ False otherwise
+ """
+ if not isinstance(uid,basestring) or len(uid) == 0:
+ # It is bad, but not too long
+ return False
+ logging.debug("IPA: __uid_too_long(%s)" % uid)
+ try:
+ config = self.get_ipa_config(opts)
+ maxlen = int(config.get('ipamaxusernamelength', 0))
+ if maxlen > 0 and len(uid) > maxlen:
+ return True
+ except Exception, e:
+ logging.debug("There was a problem " + str(e))
+
+ return False
+
+ def get_user_by_uid (self, uid, sattrs, opts=None):
+ """Get a specific user's entry. Return as a dict of values.
+ Multi-valued fields are represented as lists.
+ """
+
+ if not isinstance(uid,basestring) or len(uid) == 0:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if sattrs is not None and not isinstance(sattrs,list):
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ logging.info("IPA: get_user_by_uid '%s'" % uid)
+ uid = self.__safe_filter(uid)
+ searchfilter = "(uid=" + uid + ")"
+ return self.__get_sub_entry(self.accountsdn, searchfilter, sattrs, opts)
+
+ def get_user_by_principal(self, principal, sattrs, opts=None):
+ """Get a user entry searching by Kerberos Principal Name.
+ Return as a dict of values. Multi-valued fields are
+ represented as lists.
+ """
+
+ if not isinstance(principal,basestring) or len(principal) == 0:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if sattrs is not None and not isinstance(sattrs,list):
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ searchfilter = "(krbPrincipalName="+self.__safe_filter(principal)+")"
+ logging.info("IPA: get_user_by_principal '%s'" % principal)
+ return self.__get_sub_entry(self.accountsdn, searchfilter, sattrs, opts)
+
+ def get_user_by_email (self, email, sattrs, opts=None):
+ """Get a specific user's entry. Return as a dict of values.
+ Multi-valued fields are represented as lists.
+ """
+
+ if not isinstance(email,basestring) or len(email) == 0:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if sattrs is not None and not isinstance(sattrs,list):
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ logging.info("IPA: get_user_by_email '%s'" % email)
+ email = self.__safe_filter(email)
+ searchfilter = "(mail=" + email + ")"
+ return self.__get_sub_entry(self.basedn, searchfilter, sattrs, opts)
+
+ def get_users_by_manager (self, manager_dn, sattrs, opts=None):
+ """Gets the users that report to a particular manager.
+ """
+
+ if not isinstance(manager_dn,basestring) or len(manager_dn) == 0:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if sattrs is not None and not isinstance(sattrs,list):
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ logging.info("IPA: get_user_by_manager '%s'" % manager_dn)
+ manager_dn = self.__safe_filter(manager_dn)
+ searchfilter = "(&(objectClass=person)(manager=%s))" % manager_dn
+
+ try:
+ return self.__get_list(self.accountsdn, searchfilter, sattrs, opts)
+ except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND):
+ return []
+
+ def add_user (self, user, user_container, opts=None):
+ """Add a user in LDAP. Takes as input a dict where the key is the
+ attribute name and the value is either a string or in the case
+ of a multi-valued field a list of values. user_container sets
+ where in the tree the user is placed.
+ """
+ logging.info("IPA: add_user")
+ if not user_container:
+ user_container = DefaultUserContainer
+
+ if not isinstance(user,dict):
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if not isinstance(user_container,basestring) or len(user_container) == 0:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+
+ if not self.__is_user_unique(user['uid'], opts):
+ raise ipaerror.gen_exception(ipaerror.LDAP_DUPLICATE)
+ if self.__uid_too_long(user['uid'], opts):
+ raise ipaerror.gen_exception(ipaerror.INPUT_UID_TOO_LONG)
+
+ # dn is set here, not by the user
+ try:
+ del user['dn']
+ except KeyError:
+ pass
+
+ # No need to set empty fields, and they can cause issues when they
+ # get to LDAP, like:
+ # TypeError: ('expected a string in the list', None)
+ for k in user.keys():
+ if not user[k] or len(user[k]) == 0 or (isinstance(user[k],list) and len(user[k]) == 1 and '' in user[k]):
+ del user[k]
+
+ dn="uid=%s,%s,%s" % (ldap.dn.escape_dn_chars(user['uid']),
+ user_container,self.basedn)
+ entry = ipaserver.ipaldap.Entry(dn)
+
+ # FIXME: This should be dynamic and can include just about anything
+
+ # Get our configuration
+ config = self.get_ipa_config(opts)
+
+ # Let us add in some missing attributes
+ if user.get('homedirectory') is None:
+ user['homedirectory'] = '%s/%s' % (config.get('ipahomesrootdir'), user.get('uid'))
+ user['homedirectory'] = user['homedirectory'].replace('//', '/')
+ user['homedirectory'] = user['homedirectory'].rstrip('/')
+ if user.get('loginshell') is None:
+ user['loginshell'] = config.get('ipadefaultloginshell')
+ if user.get('gecos') is None:
+ user['gecos'] = user['uid']
+
+ # If uidnumber is blank the the FDS dna_plugin will automatically
+ # assign the next value. So we don't have to do anything with it.
+
+ group_dn="cn=%s,%s,%s" % (config.get('ipadefaultprimarygroup'), DefaultGroupContainer, self.basedn)
+ try:
+ default_group = self.get_entry_by_dn(group_dn, ['dn','gidNumber'], opts)
+ if default_group:
+ user['gidnumber'] = default_group.get('gidnumber')
+ except ipaerror.exception_for(ipaerror.LDAP_DATABASE_ERROR), e:
+ raise ipaerror.gen_exception(ipaerror.LDAP_DATABASE_ERROR, message=None, nested_exception=e.detail)
+ except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND):
+ # Fake an LDAP error so we can return something useful to the user
+ raise ipaerror.gen_exception(ipaerror.LDAP_NOT_FOUND, "The default group for new users, '%s', cannot be found." % config.get('ipadefaultprimarygroup'))
+
+ if user.get('krbprincipalname') is None:
+ user['krbprincipalname'] = "%s@%s" % (user.get('uid'), self.realm)
+
+ # FIXME. This is a hack so we can request separate First and Last
+ # name in the GUI.
+ if user.get('cn') is None:
+ user['cn'] = "%s %s" % (user.get('givenname'),
+ user.get('sn'))
+
+ if user.get('gn'):
+ del user['gn']
+
+ # some required objectclasses
+ entry.setValues('objectClass', (config.get('ipauserobjectclasses')))
+
+ # fill in our new entry with everything sent by the user
+ for u in user:
+ entry.setValues(u, user[u])
+
+ conn = self.getConnection(opts)
+ try:
+ try:
+ res = conn.addEntry(entry)
+ except TypeError, e:
+ raise ipaerror.gen_exception(ipaerror.LDAP_DATABASE_ERROR, "There is a problem with one of the data types.")
+ except ipaerror.exception_for(ipaerror.LDAP_DATABASE_ERROR), e:
+ raise ipaerror.gen_exception(ipaerror.LDAP_DATABASE_ERROR, message=None, nested_exception=e.detail)
+ except Exception, e:
+ raise ipaerror.gen_exception(ipaerror.LDAP_DATABASE_ERROR, nested_exception=e)
+ try:
+ self.add_user_to_group(user.get('uid'), group_dn, opts)
+ except ipaerror.exception_for(ipaerror.LDAP_DATABASE_ERROR), e:
+ raise ipaerror.gen_exception(ipaerror.LDAP_DATABASE_ERROR, message=None, nested_exception=e.detail)
+ except Exception, e:
+ raise ipaerror.gen_exception(ipaerror.LDAP_DATABASE_ERROR, "The user was created but adding to group %s failed" % group_dn)
+ finally:
+ self.releaseConnection(conn)
+ return res
+
+ def get_custom_fields (self, opts=None):
+ """Get the list of custom user fields.
+
+ A schema is a list of dict's of the form:
+ label: The label dispayed to the user
+ field: the attribute name
+ required: true/false
+
+ It is displayed to the user in the order of the list.
+ """
+
+ config = self.get_ipa_config(opts)
+
+ fields = config.get('ipacustomfields')
+
+ if fields is None or fields == '':
+ return []
+
+ fl = fields.split('$')
+ schema = []
+ for x in range(len(fl)):
+ vals = fl[x].split(',')
+ if len(vals) != 3:
+ # Raise?
+ logging.debug("IPA: Invalid field, skipping: %s", vals)
+ d = dict(label=unquote(vals[0]), field=unquote(vals[1]), required=unquote(vals[2]))
+ schema.append(d)
+
+ return schema
+# radius support
+
+ # clients
+ def get_radius_client_by_ip_addr(self, ip_addr, container=None, sattrs=None, opts=None):
+ filter = radius_util.radius_client_filter(ip_addr)
+ basedn = radius_util.radius_clients_basedn(container, self.basedn)
+ return self.__get_sub_entry(basedn, filter, sattrs, opts)
+
+ def __radius_client_exists(self, ip_addr, container, opts):
+ filter = radius_util.radius_client_filter(ip_addr)
+ basedn = radius_util.radius_clients_basedn(container, self.basedn)
+
+ try:
+ entry = self.__get_sub_entry(basedn, filter, ['dn','uid'], opts)
+ return True
+ except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND):
+ return False
+
+ def add_radius_client (self, client, container=None, opts=None):
+ if container is None:
+ container = radius_util.clients_container
+
+ ip_addr = client['radiusClientIPAddress']
+
+ if self.__radius_client_exists(ip_addr, container, opts):
+ raise ipaerror.gen_exception(ipaerror.LDAP_DUPLICATE)
+
+ dn = radius_util.radius_client_dn(ip_addr, container, self.basedn)
+ entry = ipaserver.ipaldap.Entry(dn)
+
+ # some required objectclasses
+ entry.setValues('objectClass', 'top', 'radiusClientProfile')
+
+ # fill in our new entry with everything sent by the client
+ for attr in client:
+ entry.setValues(attr, client[attr])
+
+ conn = self.getConnection(opts)
+ try:
+ res = conn.addEntry(entry)
+ finally:
+ self.releaseConnection(conn)
+ return res
+
+ def update_radius_client(self, oldentry, newentry, opts=None):
+ return self.update_entry(oldentry, newentry, opts)
+
+ def delete_radius_client(self, ip_addr, container=None, opts=None):
+ client = self.get_radius_client_by_ip_addr(ip_addr, container, ['dn', 'cn'], opts)
+ if client is None:
+ raise ipaerror.gen_exception(ipaerror.LDAP_NOT_FOUND)
+
+ conn = self.getConnection(opts)
+ try:
+ res = conn.deleteEntry(client['dn'])
+ finally:
+ self.releaseConnection(conn)
+ return res
+
+ def find_radius_clients(self, ip_attrs, container=None, sattrs=None, sizelimit=-1, timelimit=-1, opts=None):
+ def gen_filter(objectclass, attr, values):
+ '''Given ('myclass', 'myattr', [v1, v2]) returns
+ (&(objectclass=myclass)(|(myattr=v1)(myattr=v2)))
+ '''
+ # Don't use __safe_filter, prevents wildcarding
+ #attrs = ''.join(['(%s=%s)' % (attr, self.__safe_filter(val)) for val in values])
+ attrs = ''.join(['(%s=%s)' % (attr, val) for val in values])
+ filter = "(&(objectclass=%s)(|%s))" % (objectclass, attrs)
+ return filter
+
+ basedn = radius_util.radius_clients_basedn(container, self.basedn)
+ filter = gen_filter('radiusClientProfile', 'radiusClientIPAddress', ip_attrs)
+ conn = self.getConnection(opts)
+ try:
+ try:
+ results = conn.getListAsync(basedn, self.scope, filter, sattrs, 0, None, None, timelimit, sizelimit)
+ except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND):
+ results = [0]
+ finally:
+ self.releaseConnection(conn)
+
+ counter = results[0]
+ results = results[1:]
+ radius_clients = [counter]
+ for radius_client in results:
+ radius_clients.append(self.convert_entry(radius_client))
+
+ return radius_clients
+
+ # profiles
+ def get_radius_profile_by_uid(self, uid, user_profile=True, sattrs=None, opts=None):
+ if user_profile:
+ container = DefaultUserContainer
+ else:
+ container = radius_util.profiles_container
+
+ uid = self.__safe_filter(uid)
+ filter = radius_util.radius_profile_filter(uid)
+ basedn = radius_util.radius_profiles_basedn(container, self.basedn)
+ return self.__get_sub_entry(basedn, filter, sattrs, opts)
+
+ def __radius_profile_exists(self, uid, user_profile, opts):
+ if user_profile:
+ container = DefaultUserContainer
+ else:
+ container = radius_util.profiles_container
+
+ uid = self.__safe_filter(uid)
+ filter = radius_util.radius_profile_filter(uid)
+ basedn = radius_util.radius_profiles_basedn(container, self.basedn)
+
+ try:
+ entry = self.__get_sub_entry(basedn, filter, ['dn','uid'], opts)
+ return True
+ except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND):
+ return False
+
+ def add_radius_profile (self, profile, user_profile=True, opts=None):
+ uid = profile['uid']
+
+ if self.__radius_profile_exists(uid, user_profile, opts):
+ raise ipaerror.gen_exception(ipaerror.LDAP_DUPLICATE)
+
+ if user_profile:
+ container = DefaultUserContainer
+ else:
+ container = radius_util.profiles_container
+
+ dn = radius_util.radius_profile_dn(uid, container, self.basedn)
+ entry = ipaserver.ipaldap.Entry(dn)
+
+ # some required objectclasses
+ entry.setValues('objectClass', 'top', 'radiusprofile')
+
+ # fill in our new entry with everything sent by the profile
+ for attr in profile:
+ entry.setValues(attr, profile[attr])
+
+ conn = self.getConnection(opts)
+ try:
+ res = conn.addEntry(entry)
+ finally:
+ self.releaseConnection(conn)
+ return res
+
+ def update_radius_profile(self, oldentry, newentry, opts=None):
+ return self.update_entry(oldentry, newentry, opts)
+
+ def delete_radius_profile(self, uid, user_profile, opts=None):
+ profile = self.get_radius_profile_by_uid(uid, user_profile, ['dn', 'cn'], opts)
+ if profile is None:
+ raise ipaerror.gen_exception(ipaerror.LDAP_NOT_FOUND)
+
+ conn = self.getConnection(opts)
+ try:
+ res = conn.deleteEntry(profile['dn'])
+ finally:
+ self.releaseConnection(conn)
+ return res
+
+ def find_radius_profiles(self, uids, user_profile=True, sattrs=None, sizelimit=-1, timelimit=-1, opts=None):
+ def gen_filter(objectclass, attr, values):
+ '''Given ('myclass', 'myattr', [v1, v2]) returns
+ (&(objectclass=myclass)(|(myattr=v1)(myattr=v2)))
+ '''
+ # Don't use __safe_filter, prevents wildcarding
+ #attrs = ''.join(['(%s=%s)' % (attr, self.__safe_filter(val)) for val in values])
+ attrs = ''.join(['(%s=%s)' % (attr, val) for val in values])
+ filter = "(&(objectclass=%s)(|%s))" % (objectclass, attrs)
+ return filter
+
+ if user_profile:
+ container = DefaultUserContainer
+ else:
+ container = radius_util.profiles_container
+
+ filter = gen_filter('radiusprofile', 'uid', uids)
+ basedn="%s,%s" % (container, self.basedn)
+ conn = self.getConnection(opts)
+ try:
+ try:
+ results = conn.getListAsync(basedn, self.scope, filter, sattrs, 0, None, None, timelimit, sizelimit)
+ except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND):
+ results = [0]
+ finally:
+ self.releaseConnection(conn)
+
+ counter = results[0]
+ results = results[1:]
+ radius_profiles = [counter]
+ for radius_profile in results:
+ radius_profiles.append(self.convert_entry(radius_profile))
+
+ return radius_profiles
+
+ def set_custom_fields (self, schema, opts=None):
+ """Set the list of custom user fields.
+
+ A schema is a list of dict's of the form:
+ label: The label dispayed to the user
+ field: the attribute name
+ required: true/false
+
+ It is displayed to the user in the order of the list.
+ """
+ if not isinstance(schema,basestring) or len(schema) == 0:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+
+ config = self.get_ipa_config(opts)
+
+ # The schema is stored as:
+ # label,field,required$label,field,required$...
+ # quote() from urilib is used to ensure that it is easy to unparse
+
+ stored_schema = ""
+ for i in range(len(schema)):
+ entry = schema[i]
+ entry = quote(entry.get('label')) + "," + quote(entry.get('field')) + "," + quote(entry.get('required'))
+
+ if stored_schema != "":
+ stored_schema = stored_schema + "$" + entry
+ else:
+ stored_schema = entry
+
+ new_config = copy.deepcopy(config)
+ new_config['ipacustomfields'] = stored_schema
+
+ return self.update_entry(config, new_config, opts)
+
+ def get_all_users (self, opts=None):
+ """Return a list containing a User object for each
+ existing user.
+ """
+ logging.info("IPA: get_all_users")
+ searchfilter = "(objectclass=posixAccount)"
+
+ conn = self.getConnection(opts)
+ try:
+ all_users = conn.getList(self.accountsdn, self.scope, searchfilter, None)
+ finally:
+ self.releaseConnection(conn)
+
+ users = []
+ for u in all_users:
+ users.append(self.convert_entry(u))
+
+ return users
+
+ def find_users (self, criteria, sattrs, sizelimit=-1, timelimit=-1,
+ opts=None):
+ """Returns a list: counter followed by the results.
+ If the results are truncated, counter will be set to -1."""
+
+ if not isinstance(criteria,basestring) or len(criteria) == 0:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if sattrs is not None and not isinstance(sattrs, list):
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if not isinstance(sizelimit,int):
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if not isinstance(timelimit,int):
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+
+ logging.info("IPA: find_users '%s'" % criteria)
+ config = self.get_ipa_config(opts)
+ if timelimit < 0:
+ timelimit = float(config.get('ipasearchtimelimit'))
+ if sizelimit < 0:
+ sizelimit = int(config.get('ipasearchrecordslimit'))
+
+ # Assume the list of fields to search will come from a central
+ # configuration repository. A good format for that would be
+ # a comma-separated list of fields
+ search_fields_conf_str = config.get('ipausersearchfields')
+ search_fields = string.split(search_fields_conf_str, ",")
+
+ criteria = self.__safe_filter(criteria)
+ criteria_words = re.split(r'\s+', criteria)
+ criteria_words = filter(lambda value:value!="", criteria_words)
+ if len(criteria_words) == 0:
+ return [0]
+
+ (exact_match_filter, partial_match_filter) = self.__generate_match_filters(
+ search_fields, criteria_words)
+
+ #
+ # further constrain search to just the objectClass
+ # TODO - need to parameterize this into generate_match_filters,
+ # and work it into the field-specification search feature
+ #
+ exact_match_filter = "(&(objectClass=person)%s)" % exact_match_filter
+ partial_match_filter = "(&(objectClass=person)%s)" % partial_match_filter
+
+ conn = self.getConnection(opts)
+ try:
+ try:
+ exact_results = conn.getListAsync(self.accountsdn, self.scope,
+ exact_match_filter, sattrs, 0, None, None, timelimit,
+ sizelimit)
+ except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND):
+ exact_results = [0]
+
+ try:
+ partial_results = conn.getListAsync(self.accountsdn, self.scope,
+ partial_match_filter, sattrs, 0, None, None, timelimit,
+ sizelimit)
+ except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND):
+ partial_results = [0]
+ finally:
+ self.releaseConnection(conn)
+
+ exact_counter = exact_results[0]
+ partial_counter = partial_results[0]
+
+ exact_results = exact_results[1:]
+ partial_results = partial_results[1:]
+
+ # Remove exact matches from the partial_match list
+ exact_dns = set(map(lambda e: e.dn, exact_results))
+ partial_results = filter(lambda e: e.dn not in exact_dns,
+ partial_results)
+
+ if (exact_counter == -1) or (partial_counter == -1):
+ counter = -1
+ else:
+ counter = len(exact_results) + len(partial_results)
+
+ users = [counter]
+ for u in exact_results + partial_results:
+ users.append(self.convert_entry(u))
+
+ return users
+
+ def convert_scalar_values(self, orig_dict):
+ """LDAP update dicts expect all values to be a list (except for dn).
+ This method converts single entries to a list."""
+ if not orig_dict or not isinstance(orig_dict, dict):
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ new_dict={}
+ for (k,v) in orig_dict.iteritems():
+ if not isinstance(v, list) and k != 'dn':
+ v = [v]
+ new_dict[k] = v
+
+ return new_dict
+
+ def update_user (self, oldentry, newentry, opts=None):
+ """Wrapper around update_entry with user-specific handling.
+
+ oldentry and newentry are XML-RPC structs.
+
+ If oldentry is not empty then it is used when determine what
+ has changed.
+
+ If oldentry is empty then the value of newentry is compared
+ to the current value of oldentry.
+
+ If you want to change the RDN of a user you must use
+ this function. update_entry will fail.
+ """
+ logging.info("IPA: update_user")
+ if not isinstance(newentry,dict):
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if oldentry and not isinstance(oldentry,dict):
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if not oldentry:
+ oldentry = self.get_entry_by_dn(newentry.get('dn'), None, opts)
+ if oldentry is None:
+ raise ipaerror.gen_exception(ipaerror.LDAP_NOT_FOUND)
+
+ newrdn = 0
+
+ if oldentry.get('uid') != newentry.get('uid'):
+ if self.__uid_too_long(newentry.get('uid'), opts):
+ raise ipaerror.gen_exception(ipaerror.INPUT_UID_TOO_LONG)
+ # RDN change
+ conn = self.getConnection(opts)
+ try:
+ res = conn.updateRDN(oldentry.get('dn'), "uid=" + newentry.get('uid'))
+ newdn = oldentry.get('dn')
+ newdn = newdn.replace("uid=%s" % oldentry.get('uid'), "uid=%s" % newentry.get('uid'))
+
+ # Now fix up the dns and uids so they aren't seen as having
+ # changed.
+ oldentry['dn'] = newdn
+ newentry['dn'] = newdn
+ oldentry['uid'] = newentry['uid']
+ newrdn = 1
+ finally:
+ self.releaseConnection(conn)
+
+ # Get our configuration
+ config = self.get_ipa_config(opts)
+
+ # Make sure we have the latest object classes
+ # newentry['objectclass'] = uniq_list(newentry.get('objectclass') + config.get('ipauserobjectclasses'))
+
+ try:
+ rv = self.update_entry(oldentry, newentry, opts)
+ return rv
+ except ipaerror.exception_for(ipaerror.LDAP_EMPTY_MODLIST):
+ # This means that there was just an rdn change, nothing else.
+ if newrdn == 1:
+ return "Success"
+ else:
+ raise
+
+ def mark_entry_active (self, dn, opts=None):
+ """Mark an entry as active in LDAP."""
+
+ # This can be tricky. The entry itself can be marked inactive
+ # by being in the inactivated group. It can also be inactivated by
+ # being the member of an inactive group.
+ #
+ # First we try to remove the entry from the inactivated group. Then
+ # if it is still inactive we have to add it to the activated group
+ # which will override the group membership.
+
+ if not dn:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+
+ res = ""
+ # First, check the entry status
+ entry = self.get_entry_by_dn(dn, ['dn', 'nsAccountlock'], opts)
+
+ if entry.get('nsaccountlock', 'false').lower() == "false":
+ logging.debug("IPA: already active")
+ raise ipaerror.gen_exception(ipaerror.STATUS_ALREADY_ACTIVE)
+
+ if self.__has_nsaccountlock(dn, opts):
+ logging.debug("IPA: appears to have the nsaccountlock attribute")
+ raise ipaerror.gen_exception(ipaerror.STATUS_HAS_NSACCOUNTLOCK)
+
+ group = self.get_entry_by_cn("inactivated", None, opts)
+ try:
+ self.remove_member_from_group(entry.get('dn'), group.get('dn'), opts)
+ except ipaerror.exception_for(ipaerror.STATUS_NOT_GROUP_MEMBER):
+ # Perhaps the user is there as a result of group membership
+ pass
+
+ # Now they aren't a member of inactivated directly, what is the status
+ # now?
+ entry = self.get_entry_by_dn(dn, ['dn', 'nsAccountlock'], opts)
+
+ if entry.get('nsaccountlock', 'false').lower() == "false":
+ # great, we're done
+ logging.debug("IPA: removing from inactivated did it.")
+ return res
+
+ # So still inactive, add them to activated
+ group = self.get_entry_by_cn("activated", None, opts)
+ res = self.add_member_to_group(dn, group.get('dn'), opts)
+ logging.debug("IPA: added to activated.")
+
+ return res
+
+ def mark_entry_inactive (self, dn, opts=None):
+ """Mark an entry as inactive in LDAP."""
+
+ if not dn:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+
+ entry = self.get_entry_by_dn(dn, ['dn', 'nsAccountlock', 'memberOf'], opts)
+
+ if entry.get('nsaccountlock', 'false').lower() == "true":
+ logging.debug("IPA: already marked as inactive")
+ raise ipaerror.gen_exception(ipaerror.STATUS_ALREADY_INACTIVE)
+
+ if self.__has_nsaccountlock(dn, opts):
+ logging.debug("IPA: appears to have the nsaccountlock attribute")
+ raise ipaerror.gen_exception(ipaerror.STATUS_HAS_NSACCOUNTLOCK)
+
+ # First see if they are in the activated group as this will override
+ # the our inactivation.
+ group = self.get_entry_by_cn("activated", None, opts)
+ try:
+ self.remove_member_from_group(dn, group.get('dn'), opts)
+ except ipaerror.exception_for(ipaerror.STATUS_NOT_GROUP_MEMBER):
+ # this is fine, they may not be explicitly in this group
+ pass
+
+ # Now add them to inactivated
+ group = self.get_entry_by_cn("inactivated", None, opts)
+ res = self.add_member_to_group(dn, group.get('dn'), opts)
+
+ return res
+
+ def mark_user_active(self, uid, opts=None):
+ """Mark a user as active"""
+
+ if not isinstance(uid,basestring) or len(uid) == 0:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ user = self.get_user_by_uid(uid, ['dn', 'uid'], opts)
+ logging.info("IPA: mark_user_active '%s'" % user.get('dn'))
+ return self.mark_entry_active(user.get('dn'))
+
+ def mark_user_inactive(self, uid, opts=None):
+ """Mark a user as inactive"""
+
+ if not isinstance(uid,basestring) or len(uid) == 0:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if uid == "admin":
+ raise ipaerror.gen_exception(ipaerror.INPUT_CANT_INACTIVATE)
+ user = self.get_user_by_uid(uid, ['dn', 'uid'], opts)
+ logging.info("IPA: mark_user_inactive '%s'" % user.get('dn'))
+ return self.mark_entry_inactive(user.get('dn'))
+
+ def delete_user (self, uid, opts=None):
+ """Delete a user. Not to be confused with inactivate_user. This
+ makes the entry go away completely.
+
+ uid is the uid of the user to delete
+
+ The memberOf plugin handles removing the user from any other
+ groups.
+ """
+ if not isinstance(uid,basestring) or len(uid) == 0:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if uid == "admin":
+ raise ipaerror.gen_exception(ipaerror.INPUT_ADMIN_REQUIRED)
+ logging.info("IPA: delete_user '%s'" % uid)
+ user = self.get_user_by_uid(uid, ['dn', 'uid', 'objectclass'], opts)
+ if user is None:
+ raise ipaerror.gen_exception(ipaerror.LDAP_NOT_FOUND)
+
+ conn = self.getConnection(opts)
+ try:
+ res = conn.deleteEntry(user['dn'])
+ finally:
+ self.releaseConnection(conn)
+ return res
+
+ def modifyPassword (self, principal, oldpass, newpass, opts=None):
+ """Set/Reset a user's password
+
+ uid tells us who's password to change
+ oldpass is the old password (if available)
+ newpass is the new password
+ """
+ if not isinstance(principal,basestring) or len(principal) == 0:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if oldpass and not isinstance(oldpass,basestring):
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if not isinstance(newpass,basestring) or len(newpass) == 0:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ logging.info("IPA: modifyPassword '%s'" % principal)
+
+ user = self.get_user_by_principal(principal, ['krbprincipalname'], opts)
+ if user is None or user['krbprincipalname'] != principal:
+ raise ipaerror.gen_exception(ipaerror.LDAP_NOT_FOUND)
+
+ conn = self.getConnection(opts)
+ try:
+ res = conn.modifyPassword(user['dn'], oldpass, newpass)
+ finally:
+ self.releaseConnection(conn)
+ return res
+
+# Group support
+
+ def __is_group_unique(self, cn, opts):
+ """Return True if the cn is unique in the tree, False otherwise."""
+ cn = self.__safe_filter(cn)
+ searchfilter = "(&(cn=%s)(objectclass=posixGroup))" % cn
+
+ try:
+ entry = self.__get_sub_entry(self.accountsdn, searchfilter, ['dn','cn'], opts)
+ return False
+ except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND):
+ return True
+
+ def get_groups_by_member (self, member_dn, sattrs, opts=None):
+ """Get all of the groups an object is explicitly a member of.
+
+ This does not include groups an entry may be a member of as a
+ result of recursion (being a group that is a member of another
+ group). In other words, this searches on 'member' and not
+ 'memberof'.
+
+ Return as a dict of values.
+ Multi-valued fields are represented as lists.
+ """
+ if not isinstance(member_dn,basestring) or len(member_dn) == 0:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if sattrs is not None and not isinstance(sattrs,list):
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ logging.info("IPA: get_groups_by_member '%s'" % member_dn)
+
+ member_dn = self.__safe_filter(member_dn)
+ searchfilter = "(&(objectClass=posixGroup)(member=%s))" % member_dn
+
+ try:
+ return self.__get_list(self.accountsdn, searchfilter, sattrs, opts)
+ except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND):
+ return []
+
+ def add_group (self, group, group_container, opts=None):
+ """Add a group in LDAP. Takes as input a dict where the key is the
+ attribute name and the value is either a string or in the case
+ of a multi-valued field a list of values. group_container sets
+ where in the tree the group is placed."""
+ if not group_container:
+ group_container = DefaultGroupContainer
+
+ if not isinstance(group,dict):
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if not isinstance(group_container,basestring) or len(group_container) == 0:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+
+ if not self.__is_group_unique(group['cn'], opts):
+ raise ipaerror.gen_exception(ipaerror.LDAP_DUPLICATE)
+
+ # Get our configuration
+ config = self.get_ipa_config(opts)
+
+ dn="cn=%s,%s,%s" % (ldap.dn.escape_dn_chars(group['cn']),
+ group_container,self.basedn)
+ logging.info("IPA: add_group '%s'" % dn)
+ entry = ipaserver.ipaldap.Entry(dn)
+
+ # some required objectclasses
+ entry.setValues('objectClass', (config.get('ipagroupobjectclasses')))
+
+ # No need to explicitly set gidNumber. The dna_plugin will do this
+ # for us if the value isn't provided by the user.
+
+ # fill in our new entry with everything sent by the user
+ for g in group:
+ entry.setValues(g, group[g])
+
+ conn = self.getConnection(opts)
+ try:
+ res = conn.addEntry(entry)
+ finally:
+ self.releaseConnection(conn)
+
+ def find_groups (self, criteria, sattrs, sizelimit=-1, timelimit=-1,
+ opts=None):
+ """Return a list containing a User object for each
+ existing group that matches the criteria.
+ """
+ if not isinstance(criteria,basestring) or len(criteria) == 0:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if sattrs is not None and not isinstance(sattrs, list):
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if not isinstance(sizelimit,int):
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if not isinstance(timelimit,int):
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+
+ logging.info("IPA: find groups '%s'" % criteria)
+
+ config = self.get_ipa_config(opts)
+ if timelimit < 0:
+ timelimit = float(config.get('ipasearchtimelimit'))
+ if sizelimit < 0:
+ sizelimit = int(config.get('ipasearchrecordslimit'))
+
+ # Assume the list of fields to search will come from a central
+ # configuration repository. A good format for that would be
+ # a comma-separated list of fields
+ search_fields_conf_str = config.get('ipagroupsearchfields')
+ search_fields = string.split(search_fields_conf_str, ",")
+
+ criteria = self.__safe_filter(criteria)
+ criteria_words = re.split(r'\s+', criteria)
+ criteria_words = filter(lambda value:value!="", criteria_words)
+ if len(criteria_words) == 0:
+ return [0]
+
+ (exact_match_filter, partial_match_filter) = self.__generate_match_filters(
+ search_fields, criteria_words)
+
+ #
+ # further constrain search to just the objectClass
+ # TODO - need to parameterize this into generate_match_filters,
+ # and work it into the field-specification search feature
+ #
+ exact_match_filter = "(&(objectClass=posixGroup)%s)" % exact_match_filter
+ partial_match_filter = "(&(objectClass=posixGroup)%s)" % partial_match_filter
+
+ #
+ # TODO - copy/paste from find_users. needs to be refactored
+ #
+ conn = self.getConnection(opts)
+ try:
+ try:
+ exact_results = conn.getListAsync(self.accountsdn, self.scope,
+ exact_match_filter, sattrs, 0, None, None, timelimit,
+ sizelimit)
+ except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND):
+ exact_results = [0]
+
+ try:
+ partial_results = conn.getListAsync(self.accountsdn, self.scope,
+ partial_match_filter, sattrs, 0, None, None, timelimit,
+ sizelimit)
+ except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND):
+ partial_results = [0]
+ finally:
+ self.releaseConnection(conn)
+
+ exact_counter = exact_results[0]
+ partial_counter = partial_results[0]
+
+ exact_results = exact_results[1:]
+ partial_results = partial_results[1:]
+
+ # Remove exact matches from the partial_match list
+ exact_dns = set(map(lambda e: e.dn, exact_results))
+ partial_results = filter(lambda e: e.dn not in exact_dns,
+ partial_results)
+
+ if (exact_counter == -1) or (partial_counter == -1):
+ counter = -1
+ else:
+ counter = len(exact_results) + len(partial_results)
+
+ groups = [counter]
+ for u in exact_results + partial_results:
+ groups.append(self.convert_entry(u))
+
+ return groups
+
+ def add_member_to_group(self, member_dn, group_dn, opts=None):
+ """Add a member to an existing group.
+ """
+ if not isinstance(member_dn,basestring) or len(member_dn) == 0:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if not isinstance(group_dn,basestring) or len(group_dn) == 0:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+
+ logging.info("IPA: add_member_to_group '%s' to '%s'" % (member_dn, group_dn))
+ if member_dn.lower() == group_dn.lower():
+ raise ipaerror.gen_exception(ipaerror.INPUT_SAME_GROUP)
+
+ old_group = self.get_entry_by_dn(group_dn, None, opts)
+ if old_group is None:
+ raise ipaerror.gen_exception(ipaerror.LDAP_NOT_FOUND)
+ new_group = copy.deepcopy(old_group)
+
+ # check to make sure member_dn exists
+ member_entry = self.__get_base_entry(member_dn, "(objectClass=*)", ['dn','uid'], opts)
+ if not member_entry:
+ raise ipaerror.gen_exception(ipaerror.LDAP_NOT_FOUND)
+
+ if new_group.get('member') is not None:
+ if isinstance(new_group.get('member'),basestring):
+ new_group['member'] = [new_group['member']]
+ new_group['member'].append(member_dn)
+ else:
+ new_group['member'] = member_dn
+
+ try:
+ ret = self.__update_entry(old_group, new_group, opts)
+ except ipaerror.exception_for(ipaerror.LDAP_EMPTY_MODLIST):
+ raise
+ return ret
+
+ def add_members_to_group(self, member_dns, group_dn, opts=None):
+ """Given a list of dn's, add them to the group cn denoted by group
+ Returns a list of the member_dns that were not added to the group.
+ """
+ if not (isinstance(member_dns,list) or isinstance(member_dns,basestring)):
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if not isinstance(group_dn,basestring) or len(group_dn) == 0:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+
+ if not member_dns or not group_dn:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+
+ logging.info("IPA: add_members_to_group '%s'" % group_dn)
+
+ failed = []
+
+ if (isinstance(member_dns,basestring)):
+ member_dns = [member_dns]
+
+ for member_dn in member_dns:
+ try:
+ self.add_member_to_group(member_dn, group_dn, opts)
+ except ipaerror.exception_for(ipaerror.LDAP_EMPTY_MODLIST):
+ # User is already in the group
+ failed.append(member_dn)
+ except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND):
+ # User or the group does not exist
+ failed.append(member_dn)
+
+ return failed
+
+ def remove_member_from_group(self, member_dn, group_dn, opts=None):
+ """Remove a member_dn from an existing group.
+ """
+ if not isinstance(member_dn,basestring) or len(member_dn) == 0:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if not isinstance(group_dn,basestring) or len(group_dn) == 0:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+
+ old_group = self.get_entry_by_dn(group_dn, None, opts)
+ if old_group is None:
+ raise ipaerror.gen_exception(ipaerror.LDAP_NOT_FOUND)
+ if old_group.get('cn') == "admins":
+ member = self.get_entry_by_dn(member_dn, ['dn','uid'], opts)
+ if member.get('uid') == "admin":
+ raise ipaerror.gen_exception(ipaerror.INPUT_ADMIN_REQUIRED_IN_ADMINS)
+ logging.info("IPA: remove_member_from_group '%s' from '%s'" % (member_dn, group_dn))
+ new_group = copy.deepcopy(old_group)
+
+ if new_group.get('member') is not None:
+ if isinstance(new_group.get('member'),basestring):
+ new_group['member'] = [new_group['member']]
+ for i in range(len(new_group['member'])):
+ new_group['member'][i] = ipaserver.ipaldap.IPAdmin.normalizeDN(new_group['member'][i])
+ try:
+ new_group['member'].remove(member_dn)
+ except ValueError:
+ # member is not in the group
+ # FIXME: raise more specific error?
+ raise ipaerror.gen_exception(ipaerror.STATUS_NOT_GROUP_MEMBER)
+ else:
+ # Nothing to do if the group has no members
+ raise ipaerror.gen_exception(ipaerror.STATUS_NOT_GROUP_MEMBER)
+
+ try:
+ ret = self.__update_entry(old_group, new_group, opts)
+ except ipaerror.exception_for(ipaerror.LDAP_EMPTY_MODLIST):
+ raise
+ return ret
+
+ def remove_members_from_group(self, member_dns, group_dn, opts=None):
+ """Given a list of member dn's remove them from the group.
+ Returns a list of the members not removed from the group.
+ """
+ if not (isinstance(member_dns,list) or isinstance(member_dns,basestring)):
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if not isinstance(group_dn,basestring) or len(group_dn) == 0:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+
+ logging.info("IPA: remove_members_from_group '%s'" % group_dn)
+ failed = []
+
+ if (isinstance(member_dns,basestring)):
+ member_dns = [member_dns]
+
+ for member_dn in member_dns:
+ try:
+ self.remove_member_from_group(member_dn, group_dn, opts)
+ except ipaerror.exception_for(ipaerror.LDAP_EMPTY_MODLIST):
+ # member is not in the group
+ failed.append(member_dn)
+ except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND):
+ # member_dn or the group does not exist
+ failed.append(member_dn)
+ except ipaerror.exception_for(ipaerror.STATUS_NOT_GROUP_MEMBER):
+ # not a member of the group
+ failed.append(member_dn)
+ except ipaerror.exception_for(ipaerror.INPUT_ADMIN_REQUIRED_IN_ADMINS):
+ # Can't remove admin from admins group
+ failed.append(member_dn)
+
+ return failed
+
+ def add_user_to_group(self, user_uid, group_dn, opts=None):
+ """Add a user to an existing group.
+ """
+ if not isinstance(user_uid,basestring) or len(user_uid) == 0:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if not isinstance(group_dn,basestring) or len(group_dn) == 0:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ logging.info("IPA: add_user_to_group '%s' to '%s'" % (user_uid, group_dn))
+
+ user = self.get_user_by_uid(user_uid, ['dn', 'uid', 'objectclass'], opts)
+ if user is None:
+ raise ipaerror.gen_exception(ipaerror.LDAP_NOT_FOUND)
+
+ return self.add_member_to_group(user['dn'], group_dn, opts)
+
+ def add_users_to_group(self, user_uids, group_dn, opts=None):
+ """Given a list of user uid's add them to the group cn denoted by group
+ Returns a list of the users were not added to the group.
+ """
+ if not (isinstance(user_uids,list) or isinstance(user_uids,basestring)):
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if not isinstance(group_dn,basestring) or len(group_dn) == 0:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+
+ logging.info("IPA: add_users_to_group '%s'" % group_dn)
+ failed = []
+
+ if (isinstance(user_uids,basestring)):
+ user_uids = [user_uids]
+
+ for user_uid in user_uids:
+ try:
+ self.add_user_to_group(user_uid, group_dn, opts)
+ except ipaerror.exception_for(ipaerror.LDAP_EMPTY_MODLIST):
+ # User is already in the group
+ failed.append(user_uid)
+ except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND):
+ # User or the group does not exist
+ failed.append(user_uid)
+
+ return failed
+
+ def remove_user_from_group(self, user_uid, group_dn, opts=None):
+ """Remove a user from an existing group.
+ """
+ if not isinstance(user_uid,basestring) or len(user_uid) == 0:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if not isinstance(group_dn,basestring) or len(group_dn) == 0:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+
+ logging.info("IPA: remove_user_from_group '%s' from '%s'" % (user_uid, group_dn))
+ user = self.get_user_by_uid(user_uid, ['dn', 'uid', 'objectclass'], opts)
+ if user is None:
+ raise ipaerror.gen_exception(ipaerror.LDAP_NOT_FOUND)
+
+ return self.remove_member_from_group(user['dn'], group_dn, opts)
+
+ def remove_users_from_group(self, user_uids, group_dn, opts=None):
+ """Given a list of user uid's remove them from the group
+ Returns a list of the user uids not removed from the group.
+ """
+ if not (isinstance(user_uids,list) or isinstance(user_uids,basestring)):
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if not isinstance(group_dn,basestring) or len(group_dn) == 0:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+
+ logging.info("IPA: remove_users_from_group '%s'" % group_dn)
+ failed = []
+
+ if (isinstance(user_uids,basestring)):
+ user_uids = [user_uids]
+
+ for user_uid in user_uids:
+ try:
+ self.remove_user_from_group(user_uid, group_dn, opts)
+ except ipaerror.exception_for(ipaerror.LDAP_EMPTY_MODLIST):
+ # User is not in the group
+ failed.append(user_uid)
+ except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND):
+ # User or the group does not exist
+ failed.append(user_uid)
+
+ return failed
+
+ def add_groups_to_user(self, group_dns, user_dn, opts=None):
+ """Given a list of group dn's add them to the user.
+
+ Returns a list of the group dns that were not added.
+ """
+ if not (isinstance(group_dns,list) or isinstance(group_dns,basestring)):
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if not isinstance(user_dn,basestring) or len(user_dn) == 0:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+
+ logging.info("IPA: add_groups_to_user '%s'" % user_dn)
+ failed = []
+
+ if (isinstance(group_dns, basestring)):
+ group_dns = [group_dns]
+
+ for group_dn in group_dns:
+ try:
+ self.add_member_to_group(user_dn, group_dn, opts)
+ except ipaerror.exception_for(ipaerror.LDAP_EMPTY_MODLIST):
+ # User is already in the group
+ failed.append(group_dn)
+ except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND):
+ # User or the group does not exist
+ failed.append(group_dn)
+
+ return failed
+
+ def remove_groups_from_user(self, group_dns, user_dn, opts=None):
+ """Given a list of group dn's remove them from the user.
+
+ Returns a list of the group dns that were not removed.
+ """
+ if not (isinstance(group_dns,list) or isinstance(group_dns,basestring)):
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if not isinstance(user_dn,basestring) or len(user_dn) == 0:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+
+ logging.info("IPA: remove_groups_from_user '%s'" % user_dn)
+ failed = []
+
+ if (isinstance(group_dns,basestring)):
+ group_dns = [group_dns]
+
+ for group_dn in group_dns:
+ try:
+ self.remove_member_from_group(user_dn, group_dn, opts)
+ except ipaerror.exception_for(ipaerror.LDAP_EMPTY_MODLIST):
+ # User is not in the group
+ failed.append(group_dn)
+ except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND):
+ # User or the group does not exist
+ failed.append(group_dn)
+ except ipaerror.exception_for(ipaerror.STATUS_NOT_GROUP_MEMBER):
+ # User is not in the group
+ failed.append(group_dn)
+ except ipaerror.exception_for(ipaerror.INPUT_ADMIN_REQUIRED_IN_ADMINS):
+ # Can't remove admin from admins group
+ failed.append(member_dn)
+
+ return failed
+
+ def update_group (self, oldentry, newentry, opts=None):
+ """Wrapper around update_entry with group-specific handling.
+
+ oldentry and newentry are XML-RPC structs.
+
+ If oldentry is not empty then it is used when determine what
+ has changed.
+
+ If oldentry is empty then the value of newentry is compared
+ to the current value of oldentry.
+
+ If you want to change the RDN of a group you must use
+ this function. update_entry will fail.
+ """
+ if not isinstance(newentry,dict):
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if oldentry and not isinstance(oldentry,dict):
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if not oldentry:
+ oldentry = self.get_entry_by_dn(newentry.get('dn'), None, opts)
+ if oldentry is None:
+ raise ipaerror.gen_exception(ipaerror.LDAP_NOT_FOUND)
+
+ logging.info("IPA: update_group '%s'" % oldentry.get('cn'))
+ newrdn = 0
+
+ oldcn=oldentry.get('cn')
+ newcn=newentry.get('cn')
+ if isinstance(oldcn,basestring):
+ oldcn = [oldcn]
+ if isinstance(newcn,basestring):
+ newcn = [newcn]
+
+ if "admins" in oldcn:
+ raise ipaerror.gen_exception(ipaerror.INPUT_ADMINS_IMMUTABLE)
+
+ oldcn.sort()
+ newcn.sort()
+ if oldcn != newcn:
+ # RDN change
+ conn = self.getConnection(opts)
+ try:
+ res = conn.updateRDN(oldentry.get('dn'), "cn=" + newcn[0])
+ newdn = oldentry.get('dn')
+ newcn = newentry.get('cn')
+ if isinstance(newcn,basestring):
+ newcn = [newcn]
+
+ # Ick. Need to find the exact cn used in the old DN so we'll
+ # walk the list of cns and skip the obviously bad ones:
+ for c in oldentry.get('dn').split("cn="):
+ if c and c != "groups" and not c.startswith("accounts"):
+ newdn = newdn.replace("cn=%s" % c, "cn=%s," % newcn[0])
+ break
+
+ # Now fix up the dns and cns so they aren't seen as having
+ # changed.
+ oldentry['dn'] = newdn
+ newentry['dn'] = newdn
+ oldentry['cn'] = newentry.get('cn')
+ newrdn = 1
+ finally:
+ self.releaseConnection(conn)
+
+ # Get our configuration
+ config = self.get_ipa_config(opts)
+
+ # Make sure we have the latest object classes
+ # newentry['objectclass'] = uniq_list(newentry.get('objectclass') + config.get('ipagroupobjectclasses'))
+
+ try:
+ rv = self.update_entry(oldentry, newentry, opts)
+ return rv
+ except ipaerror.exception_for(ipaerror.LDAP_EMPTY_MODLIST):
+ if newrdn == 1:
+ # This means that there was just the rdn change, no other
+ # attributes
+ return "Success"
+ else:
+ raise
+
+ def delete_group (self, group_dn, opts=None):
+ """Delete a group
+ group_dn is the DN of the group to delete
+
+ The memberOf plugin handles removing the group from any other
+ groups.
+ """
+ if not isinstance(group_dn,basestring) or len(group_dn) == 0:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+
+ group = self.get_entry_by_dn(group_dn, ['dn', 'cn'], opts)
+ if group is None:
+ raise ipaerror.gen_exception(ipaerror.LDAP_NOT_FOUND)
+ logging.info("IPA: delete_group '%s'" % group_dn)
+
+ # We have 2 special groups, don't allow them to be removed
+ if "admins" in group.get('cn') or "editors" in group.get('cn'):
+ raise ipaerror.gen_exception(ipaerror.CONFIG_REQUIRED_GROUPS)
+
+ # Don't allow the default user group to be removed
+ config=self.get_ipa_config(opts)
+ default_group = self.get_entry_by_cn(config.get('ipadefaultprimarygroup'), None, opts)
+ if group_dn == default_group.get('dn'):
+ raise ipaerror.gen_exception(ipaerror.CONFIG_DEFAULT_GROUP)
+
+ conn = self.getConnection(opts)
+ try:
+ res = conn.deleteEntry(group_dn)
+ finally:
+ self.releaseConnection(conn)
+ return res
+
+ def add_group_to_group(self, group, tgroup, opts=None):
+ """Add a group to an existing group.
+ group is a DN of the group to add
+ tgroup is the DN of the target group to be added to
+ """
+ if not isinstance(group,basestring) or len(group) == 0:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if not isinstance(tgroup,basestring) or len(tgroup) == 0:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if group.lower() == tgroup.lower():
+ raise ipaerror.gen_exception(ipaerror.INPUT_SAME_GROUP)
+ old_group = self.get_entry_by_dn(tgroup, None, opts)
+ if old_group is None:
+ raise ipaerror.gen_exception(ipaerror.LDAP_NOT_FOUND)
+ logging.info("IPA: add_group_to_group '%s' to '%s'" % (group, tgroup))
+ new_group = copy.deepcopy(old_group)
+
+ group_dn = self.get_entry_by_dn(group, ['dn', 'cn', 'objectclass'], opts)
+ if group_dn is None:
+ raise ipaerror.gen_exception(ipaerror.LDAP_NOT_FOUND)
+
+ if new_group.get('member') is not None:
+ if isinstance(new_group.get('member'),basestring):
+ new_group['member'] = [new_group['member']]
+ new_group['member'].append(group_dn['dn'])
+ else:
+ new_group['member'] = group_dn['dn']
+
+ try:
+ ret = self.__update_entry(old_group, new_group, opts)
+ except ipaerror.exception_for(ipaerror.LDAP_EMPTY_MODLIST):
+ raise
+ return ret
+
+ def attrs_to_labels(self, attr_list, opts=None):
+ """Take a list of LDAP attributes and convert them to more friendly
+ labels."""
+ if not (isinstance(attr_list,list)):
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ logging.info("IPA: attrs_to_labels")
+
+ label_list = {}
+
+ for a in attr_list:
+ label_list[a] = attrs.attr_label_list.get(a,a)
+
+ return label_list
+
+ def get_all_attrs(self, opts=None):
+ """We have a list of hardcoded attributes -> readable labels. Return
+ that complete list if someone wants it.
+ """
+ logging.info("IPA: get_all_attrs")
+
+ return attrs.attr_label_list
+
+ def group_members(self, groupdn, attr_list, membertype, opts=None):
+ """Do a memberOf search of groupdn and return the attributes in
+ attr_list (an empty list returns all attributes).
+
+ membertype = 0 all members returned
+ membertype = 1 only direct members are returned
+ membertype = 2 only inherited members are returned
+
+ Members may be included in a group as a result of being a member
+ of a group that is a member of the group being queried.
+ """
+
+ if not isinstance(groupdn,basestring) or len(groupdn) == 0:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if attr_list is not None and not isinstance(attr_list,list):
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if membertype is not None and not isinstance(membertype,int):
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if membertype is None:
+ membertype = 0
+ if membertype < 0 or membertype > 3:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ logging.info("IPA: group_members '%s' %d" % (groupdn, membertype))
+ config = self.get_ipa_config(opts)
+ timelimit = float(config.get('ipasearchtimelimit'))
+
+ sizelimit = int(config.get('ipasearchrecordslimit'))
+
+ groupdn = self.__safe_filter(groupdn)
+ searchfilter = "(memberOf=%s)" % groupdn
+
+ if attr_list is None:
+ attr_list = []
+ attr_list.append("member")
+
+ conn = self.getConnection(opts)
+ try:
+ try:
+ results = conn.getListAsync(self.accountsdn, self.scope,
+ searchfilter, attr_list, 0, None, None, timelimit,
+ sizelimit)
+ except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND):
+ results = [0]
+ finally:
+ self.releaseConnection(conn)
+
+ counter = results[0]
+ results = results[1:]
+
+ if membertype == 0:
+ entries = [counter]
+ for e in results:
+ entries.append(self.convert_entry(e))
+
+ return entries
+
+ group = self.get_entry_by_dn(groupdn, ['dn', 'member'], opts)
+ real_members = group.get('member')
+ if isinstance(real_members, basestring):
+ real_members = [real_members]
+ if real_members is None:
+ real_members = []
+
+ # Normalize all the dns
+ for i in range(len(real_members)):
+ real_members[i] = ipaserver.ipaldap.IPAdmin.normalizeDN(real_members[i])
+
+ entries = [0]
+ for e in results:
+ if ipaserver.ipaldap.IPAdmin.normalizeDN(e.dn) not in real_members:
+ if membertype == 2:
+ entries.append(self.convert_entry(e))
+ else:
+ if membertype == 1:
+ entries.append(self.convert_entry(e))
+
+ if len(entries) > 1:
+ entries[0] = len(entries) - 1
+
+ return entries
+
+ def mark_group_active(self, cn, opts=None):
+ """Mark a group as active"""
+
+ if not isinstance(cn,basestring) or len(cn) == 0:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ logging.info("IPA: mark_group_active '%s'" % cn)
+ group = self.get_entry_by_cn(cn, ['dn', 'cn'], opts)
+ return self.mark_entry_active(group.get('dn'))
+
+ def mark_group_inactive(self, cn, opts=None):
+ """Mark a group as inactive"""
+
+ if not isinstance(cn,basestring) or len(cn) == 0:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if cn == "admins" or cn == "editors":
+ raise ipaerror.gen_exception(ipaerror.INPUT_CANT_INACTIVATE)
+ logging.info("IPA: mark_group_inactive '%s'" % cn)
+ group = self.get_entry_by_cn(cn, ['dn', 'uid'], opts)
+ return self.mark_entry_inactive(group.get('dn'))
+
+ def __is_service_unique(self, name, opts):
+ """Return True if the uid is unique in the tree, False otherwise."""
+ name = self.__safe_filter(name)
+ searchfilter = "(&(krbprincipalname=%s)(objectclass=krbPrincipal))" % name
+
+ try:
+ entry = self.__get_sub_entry(self.accountsdn, searchfilter, ['dn','krbprincipalname'], opts)
+ return False
+ except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND):
+ return True
+
+ def add_service_principal(self, name, force, opts=None):
+ """Given a name of the form: service/FQDN create a service
+ principal for it in the default realm.
+
+ Ensure that the principal points at a DNS A record so it will
+ work with Kerberos unless force is set to 1"""
+ if not name:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+
+ try:
+ f = int(force)
+ except ValueError:
+ f = 1
+ logging.info("IPA: add_service_principal '%s' (%d)" % (name, f))
+
+ # Break down the principal into its component parts, which may or
+ # may not include the realm.
+ sp = name.split('/')
+ if len(sp) != 2:
+ raise ipaerror.gen_exception(ipaerror.INPUT_MALFORMED_SERVICE_PRINCIPAL)
+ service = sp[0]
+
+ sr = sp[1].split('@')
+ if len(sr) == 1:
+ hostname = sr[0].lower()
+ realm = self.realm
+ elif len(sr) == 2:
+ hostname = sr[0].lower()
+ realm = sr[1]
+ else:
+ raise ipaerror.gen_exception(ipaerror.INPUT_MALFORMED_SERVICE_PRINCIPAL)
+
+ if not f:
+ fqdn = hostname + "."
+ rs = dnsclient.query(fqdn, dnsclient.DNS_C_IN, dnsclient.DNS_T_A)
+ if len(rs) == 0:
+ logging.debug("IPA: DNS A record lookup failed for '%s'" % hostname)
+ raise ipaerror.gen_exception(ipaerror.INPUT_NOT_DNS_A_RECORD)
+ else:
+ logging.debug("IPA: found %d records for '%s'" % (len(rs), hostname))
+
+ service_container = DefaultServiceContainer
+
+ # At some point we'll support multiple realms
+ if (realm != self.realm):
+ raise ipaerror.gen_exception(ipaerror.INPUT_REALM_MISMATCH)
+
+ # Put the principal back together again
+ princ_name = service + "/" + hostname + "@" + realm
+
+ conn = self.getConnection(opts)
+ if not self.__is_service_unique(princ_name, opts):
+ raise ipaerror.gen_exception(ipaerror.LDAP_DUPLICATE)
+
+ dn = "krbprincipalname=%s,%s,%s" % (ldap.dn.escape_dn_chars(princ_name),
+ service_container,self.basedn)
+ entry = ipaserver.ipaldap.Entry(dn)
+
+ entry.setValues('objectclass', 'krbPrincipal', 'krbPrincipalAux', 'krbTicketPolicyAux')
+ entry.setValues('krbprincipalname', princ_name)
+
+ try:
+ res = conn.addEntry(entry)
+ finally:
+ self.releaseConnection(conn)
+ return res
+
+ def delete_service_principal (self, principal, opts=None):
+ """Delete a service principal.
+
+ principal is the full DN of the entry to delete.
+
+ This should be called with much care.
+ """
+ if not isinstance(principal,basestring) or len(principal) == 0:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ entry = self.get_entry_by_dn(principal, ['dn', 'objectclass'], opts)
+ if entry is None:
+ raise ipaerror.gen_exception(ipaerror.LDAP_NOT_FOUND)
+ dn_list = ldap.explode_dn(entry['dn'].lower())
+ if "cn=kerberos" in dn_list:
+ raise ipaerror.gen_exception(ipaerror.INPUT_SERVICE_PRINCIPAL_REQUIRED)
+ logging.info("IPA: delete_service_principal '%s'" % principal)
+
+ conn = self.getConnection(opts)
+ try:
+ res = conn.deleteEntry(entry['dn'])
+ finally:
+ self.releaseConnection(conn)
+ return res
+
+ def find_service_principal(self, criteria, sattrs, sizelimit=-1,
+ timelimit=-1, opts=None):
+ """Returns a list: counter followed by the results.
+ If the results are truncated, counter will be set to -1."""
+ if not isinstance(criteria,basestring) or len(criteria) == 0:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if sattrs is not None and not isinstance(sattrs, list):
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if not isinstance(sizelimit,int):
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if not isinstance(timelimit,int):
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+
+ config = self.get_ipa_config(opts)
+ if timelimit < 0:
+ timelimit = float(config.get('ipasearchtimelimit'))
+ if sizelimit < 0:
+ sizelimit = int(config.get('ipasearchrecordslimit'))
+
+ search_fields = ["krbprincipalname"]
+ logging.info("IPA: find_service_principal '%s'" % criteria)
+
+ criteria = self.__safe_filter(criteria)
+ criteria_words = re.split(r'\s+', criteria)
+ criteria_words = filter(lambda value:value!="", criteria_words)
+ if len(criteria_words) == 0:
+ return [0]
+
+ (exact_match_filter, partial_match_filter) = self.__generate_match_filters(
+ search_fields, criteria_words)
+
+ #
+ # further constrain search to just the objectClass
+ # TODO - need to parameterize this into generate_match_filters,
+ # and work it into the field-specification search feature
+ #
+ exact_match_filter = "(&(objectclass=krbPrincipalAux)(!(objectClass=person))(!(|(krbprincipalname=kadmin/*)(krbprincipalname=K/M@*)(krbprincipalname=krbtgt/*)))%s)" % exact_match_filter
+ partial_match_filter = "(&(objectclass=krbPrincipalAux)(!(objectClass=person))(!(|(krbprincipalname=kadmin/*)(krbprincipalname=K/M@*)(krbprincipalname=krbtgt/*)))%s)" % partial_match_filter
+
+
+ conn = self.getConnection(opts)
+ try:
+ try:
+ exact_results = conn.getListAsync(self.basedn, self.scope,
+ exact_match_filter, sattrs, 0, None, None, timelimit,
+ sizelimit)
+ except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND):
+ exact_results = [0]
+
+ try:
+ partial_results = conn.getListAsync(self.basedn, self.scope,
+ partial_match_filter, sattrs, 0, None, None, timelimit,
+ sizelimit)
+ except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND):
+ partial_results = [0]
+ finally:
+ self.releaseConnection(conn)
+
+ exact_counter = exact_results[0]
+ partial_counter = partial_results[0]
+
+ exact_results = exact_results[1:]
+ partial_results = partial_results[1:]
+
+ # Remove exact matches from the partial_match list
+ exact_dns = set(map(lambda e: e.dn, exact_results))
+ partial_results = filter(lambda e: e.dn not in exact_dns,
+ partial_results)
+
+ if (exact_counter == -1) or (partial_counter == -1):
+ counter = -1
+ else:
+ counter = len(exact_results) + len(partial_results)
+
+ entries = [counter]
+ for e in exact_results + partial_results:
+ entries.append(self.convert_entry(e))
+
+ return entries
+
+
+# Configuration support
+ def get_ipa_config(self, opts=None):
+ """Retrieve the IPA configuration"""
+ searchfilter = "cn=ipaconfig"
+ try:
+ config = self.__get_sub_entry("cn=etc," + self.basedn, searchfilter, None, opts)
+ except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND):
+ raise ipaerror.gen_exception(ipaerror.LDAP_NO_CONFIG)
+
+ return config
+
+ def update_ipa_config(self, oldconfig, newconfig, opts=None):
+ """Update the IPA configuration.
+
+ oldconfig and newconfig are XML-RPC structs.
+
+ If oldconfig is not empty then it is used when determine what
+ has changed.
+
+ If oldconfig is empty then the value of newconfig is compared
+ to the current value of oldconfig.
+
+ """
+ if not isinstance(newconfig,dict):
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if oldconfig and not isinstance(oldconfig,dict):
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if not oldconfig:
+ oldconfig = self.get_entry_by_dn(newconfig.get('dn'), None, opts)
+ if oldconfig is None:
+ raise ipaerror.gen_exception(ipaerror.LDAP_NOT_FOUND)
+
+ # The LDAP routines want strings, not ints, so convert a few
+ # things. Otherwise it sees a string -> int conversion as a change.
+ try:
+ newconfig['ipapwdexpadvnotify'] = str(newconfig.get('ipapwdexpadvnotify'))
+ newconfig['ipasearchtimelimit'] = str(newconfig.get('ipasearchtimelimit'))
+ newconfig['ipasearchrecordslimit'] = str(newconfig.get('ipasearchrecordslimit'))
+ newconfig['ipamaxusernamelength'] = str(newconfig.get('ipamaxusernamelength'))
+ except KeyError:
+ # These should all be there but if not, let things proceed
+ pass
+
+ # Ensure that the default group for users exists
+ try:
+ group = self.get_entry_by_cn(newconfig.get('ipadefaultprimarygroup'), None, opts)
+ except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND):
+ raise
+ except:
+ raise
+
+ # Run through the list of User and Group object classes to make
+ # sure they are all valid. This doesn't handle dependencies but it
+ # will at least catch typos.
+ classes = self.__get_objectclasses(opts)
+ oc = newconfig['ipauserobjectclasses']
+ for i in range(len(oc)):
+ if not oc[i].lower() in classes:
+ raise ipaerror.gen_exception(ipaerror.CONFIG_INVALID_OC)
+ oc = newconfig['ipagroupobjectclasses']
+ for i in range(len(oc)):
+ if not oc[i].lower() in classes:
+ raise ipaerror.gen_exception(ipaerror.CONFIG_INVALID_OC)
+
+ return self.update_entry(oldconfig, newconfig, opts)
+
+ def get_password_policy(self, opts=None):
+ """Retrieve the IPA password policy"""
+ try:
+ policy = self.get_entry_by_cn("accounts", None, opts)
+ except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND):
+ raise ipaerror.gen_exception(ipaerror.LDAP_NO_CONFIG)
+
+ # convert some values for display purposes
+ policy['krbmaxpwdlife'] = str(int(policy.get('krbmaxpwdlife')) / 86400)
+ policy['krbminpwdlife'] = str(int(policy.get('krbminpwdlife')) / 3600)
+
+ return policy
+
+ def update_password_policy(self, oldpolicy, newpolicy, opts=None):
+ """Update the IPA configuration
+
+ oldpolicy and newpolicy are XML-RPC structs.
+
+ If oldpolicy is not empty then it is used when determine what
+ has changed.
+
+ If oldpolicy is empty then the value of newpolicy is compared
+ to the current value of oldpolicy.
+
+ """
+ if not isinstance(newpolicy,dict):
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if oldpolicy and not isinstance(oldpolicy,dict):
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if not oldpolicy:
+ oldpolicy = self.get_entry_by_dn(newpolicy.get('dn'), None, opts)
+ if oldpolicy is None:
+ raise ipaerror.gen_exception(ipaerror.LDAP_NOT_FOUND)
+
+
+ # The LDAP routines want strings, not ints, so convert a few
+ # things. Otherwise it sees a string -> int conversion as a change.
+ try:
+ for k in oldpolicy.iterkeys():
+ if k.startswith("krb", 0, 3):
+ oldpolicy[k] = str(oldpolicy[k])
+ for k in newpolicy.iterkeys():
+ if k.startswith("krb", 0, 3):
+ newpolicy[k] = str(newpolicy[k])
+
+ # Convert hours and days to seconds
+ oldpolicy['krbmaxpwdlife'] = str(int(oldpolicy.get('krbmaxpwdlife')) * 86400)
+ oldpolicy['krbminpwdlife'] = str(int(oldpolicy.get('krbminpwdlife')) * 3600)
+ newpolicy['krbmaxpwdlife'] = str(int(newpolicy.get('krbmaxpwdlife')) * 86400)
+ newpolicy['krbminpwdlife'] = str(int(newpolicy.get('krbminpwdlife')) * 3600)
+ except KeyError:
+ # These should all be there but if not, let things proceed
+ pass
+ except:
+ # Anything else raise an error
+ raise
+
+ return self.update_entry(oldpolicy, newpolicy, opts)
+
+def ldap_search_escape(match):
+ """Escapes out nasty characters from the ldap search.
+ See RFC 2254."""
+ value = match.group()
+ if (len(value) != 1):
+ return ""
+
+ if value == "(":
+ return "\\28"
+ elif value == ")":
+ return "\\29"
+ elif value == "\\":
+ return "\\5c"
+ elif value == "*":
+ # drop '*' from input. search performs its own wildcarding
+ return ""
+ elif value =='\x00':
+ return r'\00'
+ else:
+ return value
+
+def uniq_list(x):
+ """Return a unique list, preserving order and ignoring case"""
+ set = {}
+ return [set.setdefault(e.lower(),e) for e in x if e.lower() not in set]
diff --git a/ipa-server/xmlrpc-server/ipa-rewrite.conf b/ipa-server/xmlrpc-server/ipa-rewrite.conf
new file mode 100644
index 00000000..ef494300
--- /dev/null
+++ b/ipa-server/xmlrpc-server/ipa-rewrite.conf
@@ -0,0 +1,19 @@
+# VERSION 2 - DO NOT REMOVE THIS LINE
+
+RewriteEngine on
+
+# By default forward all requests to /ipa. If you don't want IPA
+# to be the default on your web server comment this line out. You will
+# need to modify ipa_webgui.cfg as well.
+RewriteRule ^/$$ https://$FQDN/ipa/ui [L,NC,R=301]
+
+# Redirect to the fully-qualified hostname. Not redirecting to secure
+# port so configuration files can be retrieved without requiring SSL.
+RewriteCond %{HTTP_HOST} !^$FQDN$$ [NC]
+RewriteRule ^/ipa/(.*) http://$FQDN/ipa/$$1 [L,R=301]
+
+# Redirect to the secure port if not displaying an error or retrieving
+# configuration.
+RewriteCond %{SERVER_PORT} !^443$$
+RewriteCond %{REQUEST_URI} !^/ipa/(errors|config)
+RewriteRule ^/ipa/(.*) https://$FQDN/ipa/$$1 [L,R=301,NC]
diff --git a/ipa-server/xmlrpc-server/ipa.conf b/ipa-server/xmlrpc-server/ipa.conf
new file mode 100644
index 00000000..85b4543a
--- /dev/null
+++ b/ipa-server/xmlrpc-server/ipa.conf
@@ -0,0 +1,109 @@
+#
+# VERSION 2 - DO NOT REMOVE THIS LINE
+#
+# LoadModule auth_kerb_module modules/mod_auth_kerb.so
+
+ProxyRequests Off
+
+# ipa-rewrite.conf is loaded separately
+
+# This is required so the auto-configuration works with Firefox 2+
+AddType application/java-archive jar
+
+<ProxyMatch ^.*/ipa/ui.*$$>
+ AuthType Kerberos
+ AuthName "Kerberos Login"
+ KrbMethodNegotiate on
+ KrbMethodK5Passwd off
+ KrbServiceName HTTP
+ KrbAuthRealms $REALM
+ Krb5KeyTab /etc/httpd/conf/ipa.keytab
+ KrbSaveCredentials on
+ Require valid-user
+ ErrorDocument 401 /ipa/errors/unauthorized.html
+ RewriteEngine on
+ Order deny,allow
+ Allow from all
+
+ RequestHeader set X-Forwarded-Keytab %{KRB5CCNAME}e
+
+ # RequestHeader unset Authorization
+</ProxyMatch>
+
+# The URI's with a trailing ! are those that aren't handled by the proxy
+ProxyPass /ipa/ui http://localhost:8080/ipa/ui
+ProxyPassReverse /ipa/ui http://localhost:8080/ipa/ui
+
+# Configure the XML-RPC service
+Alias /ipa/xml "/usr/share/ipa/ipaserver/XMLRPC"
+
+# This is where we redirect on failed auth
+Alias /ipa/errors "/usr/share/ipa/html"
+
+# For the MIT Windows config files
+Alias /ipa/config "/usr/share/ipa/html"
+
+<Directory "/usr/share/ipa/ipaserver">
+ AuthType Kerberos
+ AuthName "Kerberos Login"
+ KrbMethodNegotiate on
+ KrbMethodK5Passwd off
+ KrbServiceName HTTP
+ KrbAuthRealms $REALM
+ Krb5KeyTab /etc/httpd/conf/ipa.keytab
+ KrbSaveCredentials on
+ Require valid-user
+ ErrorDocument 401 /ipa/errors/unauthorized.html
+
+ SetHandler mod_python
+ PythonHandler ipaxmlrpc
+
+ PythonDebug Off
+
+ PythonOption IPADebug Off
+
+ # this is pointless to use since it would just reload ipaxmlrpc.py
+ PythonAutoReload Off
+</Directory>
+
+# Do no authentication on the directory that contains error messages
+<Directory "/usr/share/ipa/html">
+ AllowOverride None
+ Satisfy Any
+ Allow from all
+</Directory>
+
+# Protect our CGIs
+<Directory /var/www/cgi-bin>
+ AuthType Kerberos
+ AuthName "Kerberos Login"
+ KrbMethodNegotiate on
+ KrbMethodK5Passwd off
+ KrbServiceName HTTP
+ KrbAuthRealms $REALM
+ Krb5KeyTab /etc/httpd/conf/ipa.keytab
+ KrbSaveCredentials on
+ Require valid-user
+ ErrorDocument 401 /ipa/errors/unauthorized.html
+</Directory>
+
+#Alias /ipatest "/usr/share/ipa/ipatest"
+
+#<Directory "/usr/share/ipa/ipatest">
+# AuthType Kerberos
+# AuthName "Kerberos Login"
+# KrbMethodNegotiate on
+# KrbMethodK5Passwd off
+# KrbServiceName HTTP
+# KrbAuthRealms $REALM
+# Krb5KeyTab /etc/httpd/conf/ipa.keytab
+# KrbSaveCredentials on
+# Require valid-user
+# ErrorDocument 401 /ipa/errors/unauthorized.html
+#
+# SetHandler mod_python
+# PythonHandler test_mod_python
+#
+# PythonDebug Off
+#
+#</Directory>
diff --git a/ipa-server/xmlrpc-server/ipaxmlrpc.py b/ipa-server/xmlrpc-server/ipaxmlrpc.py
new file mode 100644
index 00000000..5e13611a
--- /dev/null
+++ b/ipa-server/xmlrpc-server/ipaxmlrpc.py
@@ -0,0 +1,394 @@
+# mod_python script
+
+# ipaxmlrpc - an XMLRPC interface for ipa.
+# Copyright (c) 2007 Red Hat
+#
+# IPA is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation;
+# version 2.1 of the License.
+#
+# This software is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this software; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+#
+# Based on kojixmlrpc - an XMLRPC interface for koji by
+# Mike McLean <mikem@redhat.com>
+#
+# Authors:
+# Rob Crittenden <rcritten@redhat.com>
+
+import sys
+
+
+import time
+import traceback
+import pprint
+from xmlrpclib import Marshaller,loads,dumps,Fault
+from mod_python import apache
+import logging
+
+from ipaserver import funcs
+from ipa import ipaerror, ipautil
+import ldap
+
+import string
+import base64
+
+#
+# An override so we can base64 encode all outgoing values.
+# This is set by calling: Marshaller._Marshaller__dump = xmlrpclib_dump
+#
+# Not currently used.
+#
+def xmlrpclib_escape(s, replace = string.replace):
+ """
+ xmlrpclib only handles certain characters. Lets encode the whole
+ blob
+ """
+
+ return base64.encodestring(s)
+
+def xmlrpclib_dump(self, value, write):
+ """
+ xmlrpclib cannot marshal instances of subclasses of built-in
+ types. This function overrides xmlrpclib.Marshaller.__dump so that
+ any value that is an instance of one of its acceptable types is
+ marshalled as that type.
+
+ xmlrpclib also cannot handle invalid 7-bit control characters. See
+ above.
+ """
+
+ # Use our escape function
+ args = [self, value, write]
+ if isinstance(value, (str, unicode)):
+ args.append(xmlrpclib_escape)
+
+ try:
+ # Try for an exact match first
+ f = self.dispatch[type(value)]
+ except KeyError:
+ # Try for an isinstance() match
+ for Type, f in self.dispatch.iteritems():
+ if isinstance(value, Type):
+ f(*args)
+ return
+ raise TypeError, "cannot marshal %s objects" % type(value)
+ else:
+ f(*args)
+
+
+class ModXMLRPCRequestHandler(object):
+ """Simple XML-RPC handler for mod_python environment"""
+
+ def __init__(self):
+ self.funcs = {}
+ self.traceback = False
+ #introspection functions
+ self.register_function(self.ping, name="ping")
+ self.register_function(self.list_api, name="_listapi")
+ self.register_function(self.system_listMethods, name="system.listMethods")
+ self.register_function(self.system_methodSignature, name="system.methodSignature")
+ self.register_function(self.system_methodHelp, name="system.methodHelp")
+ self.register_function(self.multiCall)
+
+ def register_function(self, function, name = None):
+ if name is None:
+ name = function.__name__
+ self.funcs[name] = function
+
+ def register_module(self, instance, prefix=None):
+ """Register all the public functions in an instance with prefix prepended
+
+ For example
+ h.register_module(exports,"pub.sys")
+ will register the methods of exports with names like
+ pub.sys.method1
+ pub.sys.method2
+ ...etc
+ """
+ for name in dir(instance):
+ if name.startswith('_'):
+ continue
+ function = getattr(instance, name)
+ if not callable(function):
+ continue
+ if prefix is not None:
+ name = "%s.%s" %(prefix,name)
+ self.register_function(function, name=name)
+
+ def register_instance(self,instance):
+ self.register_module(instance)
+
+ def _marshaled_dispatch(self, data, req):
+ """Dispatches an XML-RPC method from marshalled (XML) data."""
+
+ params, method = loads(data)
+ pythonopts = req.get_options()
+
+ # Populate the Apache environment variables
+ req.add_common_vars()
+
+ opts={}
+ opts['remoteuser'] = req.user
+
+ if req.subprocess_env.get("KRB5CCNAME") is not None:
+ opts['krbccache'] = req.subprocess_env.get("KRB5CCNAME")
+ else:
+ response = dumps(Fault(5, "Did not receive Kerberos credentials."))
+ return response
+
+ debuglevel = logging.INFO
+ if pythonopts.get("IPADebug"):
+ opts['ipadebug'] = pythonopts.get("IPADebug").lower()
+
+ if opts['ipadebug'] == "on":
+ debuglevel = logging.DEBUG
+
+ if not opts.get('ipadebug'):
+ opts['ipadebug'] = "off"
+
+ logging.basicConfig(level=debuglevel,
+ format='[%(asctime)s] [%(levelname)s] %(message)s',
+ datefmt='%a %b %d %H:%M:%S %Y',
+ stream=sys.stderr)
+
+# if opts['ipadebug'] == "on":
+# for o in opts:
+# logging.debug("IPA: setting option %s: %s" % (o, opts[o]))
+# for e in req.subprocess_env:
+# logging.debug("IPA: environment %s: %s" % (e, req.subprocess_env[e]))
+
+ # Tack onto the end of the passed-in arguments any options we also
+ # need
+ params = params + (opts,)
+
+ # special case
+# if method == "get_user":
+# Marshaller._Marshaller__dump = xmlrpclib_dump
+
+ start = time.time()
+ # generate response
+ try:
+ response = self._dispatch(method, params)
+ # wrap response in a singleton tuple
+ response = (response,)
+ response = dumps(response, methodresponse=1, allow_none=1)
+ except ipaerror.IPAError, e:
+ self.traceback = True
+
+ if (isinstance(e.detail, ldap.LDAPError) and len(e.detail[0].get('desc')) > 1):
+ err = ": %s: %s" % (e.detail[0].get('desc'), e.detail[0].get('info',''))
+ response = dumps(Fault(e.code, str(e) + err))
+ else:
+ response = dumps(Fault(e.code, str(e)))
+ except:
+ self.traceback = True
+ # report exception back to server
+ e_class, e = sys.exc_info()[:2]
+ faultCode = getattr(e_class,'faultCode',1)
+ tb_str = ''.join(traceback.format_exception(*sys.exc_info()))
+ faultString = tb_str
+ response = dumps(Fault(faultCode, faultString))
+
+ return response
+
+ def _dispatch(self,method,params):
+ func = self.funcs.get(method,None)
+ if func is None:
+ raise Fault(1, "Invalid method: %s" % method)
+
+ args = list(ipautil.unwrap_binary_data(params))
+ for i in range(len(args)):
+ if args[i] == '__NONE__':
+ args[i] = None
+
+ ret = func(*args)
+
+ return ipautil.wrap_binary_data(ret)
+
+ def multiCall(self, calls):
+ """Execute a multicall. Execute each method call in the calls list, collecting
+ results and errors, and return those as a list."""
+ results = []
+ for call in calls:
+ try:
+ result = self._dispatch(call['methodName'], call['params'])
+ except Fault, fault:
+ results.append({'faultCode': fault.faultCode, 'faultString': fault.faultString})
+ except:
+ # transform unknown exceptions into XML-RPC Faults
+ # don't create a reference to full traceback since this creates
+ # a circular reference.
+ exc_type, exc_value = sys.exc_info()[:2]
+ faultCode = getattr(exc_type, 'faultCode', 1)
+ faultString = ', '.join(exc_value.args)
+ trace = traceback.format_exception(*sys.exc_info())
+ # traceback is not part of the multicall spec, but we include it for debugging purposes
+ results.append({'faultCode': faultCode, 'faultString': faultString, 'traceback': trace})
+ else:
+ results.append([result])
+
+ return results
+
+ def list_api(self,opts):
+ funcs = []
+ for name,func in self.funcs.items():
+ #the keys in self.funcs determine the name of the method as seen over xmlrpc
+ #func.__name__ might differ (e.g. for dotted method names)
+ args = self._getFuncArgs(func)
+ funcs.append({'name': name,
+ 'doc': func.__doc__,
+ 'args': args})
+ return funcs
+
+ def ping(self,opts):
+ """Simple test to see if the XML-RPC is up and active."""
+ return "pong"
+
+ def _getFuncArgs(self, func):
+ args = []
+ for x in range(0, func.func_code.co_argcount):
+ if x == 0 and func.func_code.co_varnames[x] == "self":
+ continue
+ # opts is a name we tack on internally. Don't publish it.
+ if func.func_code.co_varnames[x] == "opts":
+ continue
+ if func.func_defaults and func.func_code.co_argcount - x <= len(func.func_defaults):
+ args.append((func.func_code.co_varnames[x], func.func_defaults[x - func.func_code.co_argcount + len(func.func_defaults)]))
+ else:
+ args.append(func.func_code.co_varnames[x])
+ return args
+
+ def system_listMethods(self, opts):
+ return self.funcs.keys()
+
+ def system_methodSignature(self, method, opts):
+ #it is not possible to autogenerate this data
+ return 'signatures not supported'
+
+ def system_methodHelp(self, method, opts):
+ func = self.funcs.get(method)
+ if func is None:
+ return ""
+ arglist = []
+ for arg in self._getFuncArgs(func):
+ if isinstance(arg,str):
+ arglist.append(arg)
+ else:
+ arglist.append('%s=%s' % (arg[0], arg[1]))
+ ret = '%s(%s)' % (method, ", ".join(arglist))
+ if func.__doc__:
+ ret += "\ndescription: %s" % func.__doc__
+ return ret
+
+ def handle_request(self,req):
+ """Handle a single XML-RPC request"""
+
+ # XMLRPC uses POST only. Reject anything else
+ if req.method != 'POST':
+ req.allow_methods(['POST'],1)
+ raise apache.SERVER_RETURN, apache.HTTP_METHOD_NOT_ALLOWED
+
+ # The LDAP connection pool is not thread-safe. Avoid problems and
+ # force the forked model for now.
+ if apache.mpm_query(apache.AP_MPMQ_IS_THREADED):
+ response = dumps(Fault(3, "Apache must use the forked model"))
+ else:
+ response = self._marshaled_dispatch(req.read(), req)
+
+ req.content_type = "text/xml"
+ req.set_content_length(len(response))
+ req.write(response)
+
+
+#
+# mod_python handler
+#
+
+def handler(req, profiling=False):
+ if profiling:
+ import profile, pstats, StringIO, tempfile
+ global _profiling_req
+ _profiling_req = req
+ temp = tempfile.NamedTemporaryFile()
+ profile.run("import ipxmlrpc; ipaxmlrpc.handler(ipaxmlrpc._profiling_req, False)", temp.name)
+ stats = pstats.Stats(temp.name)
+ strstream = StringIO.StringIO()
+ sys.stdout = strstream
+ stats.sort_stats("time")
+ stats.print_stats()
+ req.write("<pre>" + strstream.getvalue() + "</pre>")
+ _profiling_req = None
+ else:
+ opts = req.get_options()
+ try:
+ f = funcs.IPAServer()
+ h = ModXMLRPCRequestHandler()
+ h.register_function(f.version)
+ h.register_function(f.get_aci_entry)
+ h.register_function(f.get_entry_by_dn)
+ h.register_function(f.get_entry_by_cn)
+ h.register_function(f.update_entry)
+ h.register_function(f.get_user_by_uid)
+ h.register_function(f.get_user_by_principal)
+ h.register_function(f.get_user_by_email)
+ h.register_function(f.get_users_by_manager)
+ h.register_function(f.add_user)
+ h.register_function(f.get_custom_fields)
+ h.register_function(f.set_custom_fields)
+ h.register_function(f.get_all_users)
+ h.register_function(f.find_users)
+ h.register_function(f.update_user)
+ h.register_function(f.delete_user)
+ h.register_function(f.mark_user_active)
+ h.register_function(f.mark_user_inactive)
+ h.register_function(f.mark_group_active)
+ h.register_function(f.mark_group_inactive)
+ h.register_function(f.modifyPassword)
+ h.register_function(f.get_groups_by_member)
+ h.register_function(f.add_group)
+ h.register_function(f.find_groups)
+ h.register_function(f.add_member_to_group)
+ h.register_function(f.add_members_to_group)
+ h.register_function(f.remove_member_from_group)
+ h.register_function(f.remove_members_from_group)
+ h.register_function(f.add_user_to_group)
+ h.register_function(f.add_users_to_group)
+ h.register_function(f.add_group_to_group)
+ h.register_function(f.remove_user_from_group)
+ h.register_function(f.remove_users_from_group)
+ h.register_function(f.add_groups_to_user)
+ h.register_function(f.remove_groups_from_user)
+ h.register_function(f.update_group)
+ h.register_function(f.delete_group)
+ h.register_function(f.attrs_to_labels)
+ h.register_function(f.get_all_attrs)
+ h.register_function(f.group_members)
+ h.register_function(f.get_ipa_config)
+ h.register_function(f.update_ipa_config)
+ h.register_function(f.get_password_policy)
+ h.register_function(f.update_password_policy)
+ h.register_function(f.add_service_principal)
+ h.register_function(f.delete_service_principal)
+ h.register_function(f.find_service_principal)
+ h.register_function(f.get_radius_client_by_ip_addr)
+ h.register_function(f.add_radius_client)
+ h.register_function(f.update_radius_client)
+ h.register_function(f.delete_radius_client)
+ h.register_function(f.find_radius_clients)
+ h.register_function(f.get_radius_profile_by_uid)
+ h.register_function(f.add_radius_profile)
+ h.register_function(f.update_radius_profile)
+ h.register_function(f.delete_radius_profile)
+ h.register_function(f.find_radius_profiles)
+ h.handle_request(req)
+ finally:
+ pass
+ return apache.OK
diff --git a/ipa-server/xmlrpc-server/ssbrowser.html b/ipa-server/xmlrpc-server/ssbrowser.html
new file mode 100644
index 00000000..37dbcb40
--- /dev/null
+++ b/ipa-server/xmlrpc-server/ssbrowser.html
@@ -0,0 +1,68 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html>
+<head>
+<title>Browser Kerberos Setup</title>
+</head>
+<body>
+ <h2>Browser Kerberos Setup</h2>
+ <h3> Internet Explorer Configuration </h3>
+<p>Once you are able to log into the workstation with your kerberos key you should be able to use that ticket in Internet Explorer. For illustration purposes his page will use EXAMPLE.COM as the sample realm and example.com for the domain.
+</p>
+<ul><li> Login to the Windows machine using an account of domain EXAMPLE.COM
+
+</li><li> In Internet Explorer, click Tools, and then click Internet Options.
+</li></ul>
+<ol><li> Click the Security tab.
+</li><li> Click Local intranet.
+</li><li> Click Sites
+</li><li> Click Advanced
+</li><li> Add *.example.com to the list
+
+</li></ol>
+<ul><li> In Internet Explorer, click Tools, and then click Internet Options.
+</li></ul>
+<ol><li> Click the Security tab.
+</li><li> Click Local intranet.
+</li><li> Click Custom Level
+</li><li> Select Automatic logon only in Intranet zone.
+</li></ol>
+<ul><li> Visit a kerberized web site using IE. You must use the fully-qualified DN in the URL.
+</li><li> If all went right, it should work.
+
+</li></ul>
+<h3 class="title">Firefox Configuration</h3>
+<p>
+You can configure Firefox to use Kerberos for Single Sign-on. In order for this functionality to work correctly, you need to configure your web browser to send your Kerberos credentials to the appropriate <span class="abbrev">KDC</span>.The following section describes the configuration changes and other requirements to achieve this.
+</p>
+<ol class="arabic">
+<li>
+<p>
+In the address bar of Firefox, type <b class="userinput"><tt>about:config</tt></b> to display the list of current configuration options.
+</p>
+</li>
+
+<li>
+<p>
+In the <span><b class="guilabel">Filter</b></span> field, type <b class="userinput"><tt>negotiate</tt></b> to restrict the list of options.
+</p>
+</li>
+<li>
+<p>
+Double-click the <span class="emphasis"><em>network.negotiate-auth.trusted-uris</em></span> entry to display the <span class="emphasis"><em>Enter string value</em></span> dialog box.
+
+</p>
+</li>
+<li>
+<p>
+Enter the name of the domain against which you want to authenticate, for example, <i class="replaceable"><tt>.example.com</tt></i>.
+</p>
+</li>
+<li>
+<p>
+Repeat the above procedure for the <span class="emphasis"><em>network.negotiate-auth.delegation-uris</em></span> entry, using the same domain.
+</p>
+</li>
+
+</ol>
+</body>
+</html>
diff --git a/ipa-server/xmlrpc-server/test/Makefile.am b/ipa-server/xmlrpc-server/test/Makefile.am
new file mode 100644
index 00000000..310d9d47
--- /dev/null
+++ b/ipa-server/xmlrpc-server/test/Makefile.am
@@ -0,0 +1,12 @@
+NULL =
+
+EXTRA_DIST = \
+ README \
+ test_methods.py \
+ test_mod_python.py \
+ test.py \
+ $(NULL)
+
+MAINTAINERCLEANFILES = \
+ *~ \
+ Makefile.in
diff --git a/ipa-server/xmlrpc-server/test/README b/ipa-server/xmlrpc-server/test/README
new file mode 100644
index 00000000..544efa52
--- /dev/null
+++ b/ipa-server/xmlrpc-server/test/README
@@ -0,0 +1,60 @@
+Diagnosing Kerberos credentials cache problems is difficult.
+
+The first thing to try is to set LogLevel to debug in
+/etc/httpd/conf/httpd.conf and restart Apache.
+
+Look in /var/log/httpd/error_log for any problems.
+
+Also check out /var/log/krb5kdc.log
+
+To simplify things and test just Kerberos ticket forwarding:
+
+The first test is with a CGI:
+
+- copy test.py /var/www/cgi-bin
+- chmod +x /var/www/cgi-bin/test.py
+- kinit admin (or some other existing user)
+- curl -u : --negotiate http://yourhost.fqdn/cgi-bin/test.py
+
+For yourhost.fqdn use the fully-qualified hostname of your webserver.
+
+The output should look something like:
+
+KRB5CCNAME is FILE:/tmp/krb5cc_apache_TiMAbq
+Sucessfully bound to LDAP using SASL mechanism GSSAPI
+
+This CGI uses the forwarded credentials to make an authenticated LDAP
+connection. If this fails it means that Apache is not properly storing
+the kerberos credentials.
+
+If that works, the second test more closely models the way that IPA works.
+
+- mkdir /usr/share/ipa/ipatest
+- cp test_mod_python.py /usr/share/ipa/ipatest
+- uncomment the entries for ipatest in /etc/httpd/conf.d/ipa.conf. There are
+ entries for ProxyPass and ProxyReversePass, an Alias and a Directory
+- restart Apache
+- curl -u : --negotiate http://yourhost.fqdn/ipatest/
+
+For yourhost.fqdn use the fully-qualified hostname of your webserver.
+
+The output should look something like:
+
+KRB5CCNAME: FILE:/tmp/krb5cc_apache_c0MU9o<br>
+GATEWAY_INTERFACE: CGI/1.1<br>
+...
+SCRIPT_FILENAME: /usr/share/ipa/ipaserver/<br>
+REMOTE_PORT: 45691<br>
+REMOTE_USER: rcrit@GREYOAK.COM<br>
+AUTH_TYPE: Negotiate<br>
+KRB5CCNAME is FILE:/tmp/krb5cc_apache_c0MU9o<br>
+Sucessfully bound to LDAP using SASL mechanism GSSAPI<br>
+
+It should print all of the environment variables available to mod_python
+and do a GSSAPI LDAP connection.
+
+A final test, which lists the capabilities of the XML-RPC server is
+test_methods.py. This is more a sanity check that new functions added
+to the server work as expected.
+
+Note that opts is added by the server itself and is not passed in by the user.
diff --git a/ipa-server/xmlrpc-server/test/test.py b/ipa-server/xmlrpc-server/test/test.py
new file mode 100644
index 00000000..7c05f8d2
--- /dev/null
+++ b/ipa-server/xmlrpc-server/test/test.py
@@ -0,0 +1,41 @@
+#!/usr/bin/python
+
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+# A test CGI that tests that the Kerberos credentials cache was created
+# properly in Apache.
+
+import ldap
+import ldap.sasl
+import os
+
+sasl_auth = ldap.sasl.sasl({}, "GSSAPI")
+conn = ldap.initialize("ldap://localhost:389/")
+conn.protocol_version = 3
+
+print "Content-type: text/plain"
+print ""
+
+try:
+ print "KRB5CCNAME is", os.environ["KRB5CCNAME"]
+
+ try:
+ conn.sasl_interactive_bind_s("", sasl_auth)
+ except ldap.LDAPError,e:
+ print "Error using SASL mechanism", sasl_auth.mech, str(e)
+ else:
+ print "Sucessfully bound to LDAP using SASL mechanism", sasl_auth.mech
+ conn.unbind()
+except KeyError,e:
+ print "not set."
diff --git a/ipa-server/xmlrpc-server/test/test_methods.py b/ipa-server/xmlrpc-server/test/test_methods.py
new file mode 100644
index 00000000..88fcd933
--- /dev/null
+++ b/ipa-server/xmlrpc-server/test/test_methods.py
@@ -0,0 +1,57 @@
+#!/usr/bin/python
+
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+# Simple program to interrogate the XML-RPC server for information on what
+# it can do.
+
+import sys
+import xmlrpclib
+from ipa.krbtransport import KerbTransport
+import ipa
+from ipa import config
+
+ipa.config.init_config()
+
+serverlist = config.config.get_server()
+url = "http://" + serverlist[0] + "/ipa"
+s = xmlrpclib.Server(url, KerbTransport())
+
+print "A list of all methods available on the server."
+print "system.listMethods: ", s.system.listMethods()
+print ""
+
+print "Signatures are not supported."
+print "system.methodSignature: ", s.system.methodSignature("get_user_by_uid")
+print ""
+
+print "Help on a specific method"
+print "system.methodHelp: ", s.system.methodHelp("get_user_by_uid")
+
+print "The entire API:"
+result = s._listapi()
+for item in result:
+ print item['name'],
+ print "(",
+ i = len(item['args'])
+ p = 0
+ for a in item['args']:
+ if isinstance(a, list):
+ print "%s=%s" % (a[0], a[1]),
+ else:
+ print a,
+ if p < i - 1:
+ print ",",
+ p = p + 1
+ print ")"
diff --git a/ipa-server/xmlrpc-server/test/test_mod_python.py b/ipa-server/xmlrpc-server/test/test_mod_python.py
new file mode 100644
index 00000000..6136b541
--- /dev/null
+++ b/ipa-server/xmlrpc-server/test/test_mod_python.py
@@ -0,0 +1,52 @@
+#!/usr/bin/python
+
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+# A test CGI that tests that the Kerberos credentials cache was created
+# properly in Apache.
+
+import ldap
+import ldap.sasl
+import os
+from mod_python import apache
+
+def handler(req):
+ req.content_type = "text/plain"
+ req.send_http_header()
+ do_request(req)
+ return apache.OK
+
+def do_request(req):
+ sasl_auth = ldap.sasl.sasl({}, "GSSAPI")
+ conn = ldap.initialize("ldap://localhost:389/")
+ conn.protocol_version = 3
+
+ req.add_common_vars()
+
+ for e in req.subprocess_env:
+ req.write("%s: %s<br>\n" % (e, req.subprocess_env[e]))
+
+ try:
+ req.write("KRB5CCNAME is %s<br>\n" % req.subprocess_env["KRB5CCNAME"])
+ os.environ["KRB5CCNAME"] = req.subprocess_env["KRB5CCNAME"]
+
+ try:
+ conn.sasl_interactive_bind_s("", sasl_auth)
+ except ldap.LDAPError,e:
+ req.write("Error using SASL mechanism %s %s<br>\n" % (sasl_auth.mech, str(e)))
+ else:
+ req.write("Sucessfully bound to LDAP using SASL mechanism %s<br>\n" % sasl_auth.mech)
+ conn.unbind()
+ except KeyError,e:
+ req.write("KRB5CCNAME is not set.")
diff --git a/ipa-server/xmlrpc-server/unauthorized.html b/ipa-server/xmlrpc-server/unauthorized.html
new file mode 100644
index 00000000..6ba8a99e
--- /dev/null
+++ b/ipa-server/xmlrpc-server/unauthorized.html
@@ -0,0 +1,28 @@
+<html>
+<title>Kerberos Authentication Failed</h2>
+<body>
+<h2>Kerberos Authentication Failed</h2>
+<p>
+Unable to verify your Kerberos credentials. Please make sure
+that you have valid Kerberos tickets (obtainable via kinit), and that you
+have <a href="/ipa/errors/ssbrowser.html">configured your
+browser correctly</a>. If you are still unable to access
+the IPA Web interface, please contact the helpdesk on for additional assistance.
+</p>
+<p>
+Import the <a href="/ipa/errors/ca.crt">IPA Certificate Authority</a>.
+</p>
+<p>
+<script type="text/javascript">
+ if (navigator.userAgent.indexOf("Firefox") != -1 ||
+ navigator.userAgent.indexOf("SeaMonkey") != -1)
+ {
+ document.write("<p>You can automatically configure your browser to work with Kerberos by importing the Certificate Authority above and clicking on the Configure Browser button.</p>");
+ document.write("<p>You <strong>must</strong> reload this page after importing the Certificate Authority for the automatic settings to work</p>");
+ document.write("<object data=\"jar:/ipa/errors/configure.jar!/preferences.html\" type=\"text/html\"><\/object");
+ }
+</script>
+</p>
+</ul>
+</body>
+</html>