From 63f7cdf7f7e1c39b791dad6951fa39d9a6d58c9d Mon Sep 17 00:00:00 2001 From: Kevin McCarthy Date: Fri, 12 Oct 2007 15:11:55 -0700 Subject: Adds delegation listing and creation to the GUI. --- ipa-python/aci.py | 14 +- ipa-python/ipaclient.py | 8 + ipa-python/rpcclient.py | 17 +++ ipa-python/test/test_aci.py | 34 ++++- ipa-server/ipa-gui/ipagui/controllers.py | 2 + ipa-server/ipa-gui/ipagui/forms/delegate.py | 86 +++++++++++ ipa-server/ipa-gui/ipagui/static/css/style.css | 17 ++- .../ipa-gui/ipagui/subcontrollers/delegation.py | 168 +++++++++++++++++++++ .../ipagui/templates/delegategroupsearch.kid | 31 ++++ .../ipa-gui/ipagui/templates/delegatelayout.kid | 16 ++ .../ipa-gui/ipagui/templates/delegatelist.kid | 60 ++++++++ .../ipa-gui/ipagui/templates/delegatenew.kid | 15 ++ .../ipa-gui/ipagui/templates/delegatenewform.kid | 154 +++++++++++++++++++ ipa-server/ipa-gui/ipagui/templates/master.kid | 3 + ipa-server/xmlrpc-server/funcs.py | 9 ++ ipa-server/xmlrpc-server/ipaxmlrpc.py | 1 + 16 files changed, 625 insertions(+), 10 deletions(-) create mode 100644 ipa-server/ipa-gui/ipagui/forms/delegate.py create mode 100644 ipa-server/ipa-gui/ipagui/subcontrollers/delegation.py create mode 100644 ipa-server/ipa-gui/ipagui/templates/delegategroupsearch.kid create mode 100644 ipa-server/ipa-gui/ipagui/templates/delegatelayout.kid create mode 100644 ipa-server/ipa-gui/ipagui/templates/delegatelist.kid create mode 100644 ipa-server/ipa-gui/ipagui/templates/delegatenew.kid create mode 100644 ipa-server/ipa-gui/ipagui/templates/delegatenewform.kid diff --git a/ipa-python/aci.py b/ipa-python/aci.py index d834f899..137d9ee1 100644 --- a/ipa-python/aci.py +++ b/ipa-python/aci.py @@ -16,6 +16,7 @@ # import re +import urllib class ACI: """ @@ -25,10 +26,10 @@ class ACI: """ def __init__(self,acistr=None): + self.name = '' self.source_group = '' self.dest_group = '' self.attrs = [] - self.name = '' if acistr is not None: self.parse_acistr(acistr) @@ -40,15 +41,15 @@ class ACI: # dn's aren't typed in, but searched for, and the search results # will return escaped dns - acistr = ('(targetattr = "%s")' + + acistr = ('(targetattr="%s")' + '(targetfilter="(memberOf=%s)")' + '(version 3.0;' + 'acl "%s";' + 'allow (write) ' + - 'groupdn="%s";)') % (attrs_str, + 'groupdn="ldap:///%s";)') % (attrs_str, self.dest_group, self.name, - self.source_group) + urllib.quote(self.source_group, "/=, ")) return acistr def _match(self, prefix, inputstr): @@ -89,7 +90,7 @@ class ACI: def parse_acistr(self, acistr): """Parses the acistr. If the string isn't recognized, a SyntaxError is raised.""" - acistr = self._match('(targetattr = ', acistr) + acistr = self._match('(targetattr=', acistr) (attrstr, acistr) = self._match_str(acistr) self.attrs = attrstr.split(' || ') @@ -107,7 +108,8 @@ class ACI: acistr = self._match(';allow (write) groupdn=', acistr) (src_dn_str, acistr) = self._match_str(acistr) - self.source_group = src_dn_str + 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: diff --git a/ipa-python/ipaclient.py b/ipa-python/ipaclient.py index 3a6e1305..cf2e355a 100644 --- a/ipa-python/ipaclient.py +++ b/ipa-python/ipaclient.py @@ -54,6 +54,14 @@ class IPAClient: 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): diff --git a/ipa-python/rpcclient.py b/ipa-python/rpcclient.py index 8bc288b4..ae26d707 100644 --- a/ipa-python/rpcclient.py +++ b/ipa-python/rpcclient.py @@ -67,6 +67,23 @@ class RPCClient: return obj +# 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): diff --git a/ipa-python/test/test_aci.py b/ipa-python/test/test_aci.py index ffe2d071..5556deb3 100644 --- a/ipa-python/test/test_aci.py +++ b/ipa-python/test/test_aci.py @@ -22,15 +22,16 @@ sys.path.insert(0, ".") import unittest import aci +import urllib class TestACI(unittest.TestCase): - acitemplate = ('(targetattr = "%s")' + + acitemplate = ('(targetattr="%s")' + '(targetfilter="(memberOf=%s)")' + '(version 3.0;' + 'acl "%s";' + 'allow (write) ' + - 'groupdn="%s";)') + 'groupdn="ldap:///%s";)') def setUp(self): self.aci = aci.ACI() @@ -52,6 +53,20 @@ class TestACI(unittest.TestCase): 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' @@ -66,6 +81,21 @@ class TestACI(unittest.TestCase): 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') diff --git a/ipa-server/ipa-gui/ipagui/controllers.py b/ipa-server/ipa-gui/ipagui/controllers.py index 340d6f9f..f2b7bb90 100644 --- a/ipa-server/ipa-gui/ipagui/controllers.py +++ b/ipa-server/ipa-gui/ipagui/controllers.py @@ -14,12 +14,14 @@ import ipa.ipaclient from subcontrollers.user import UserController from subcontrollers.group import GroupController +from subcontrollers.delegation import DelegationController ipa.config.init_config() class Root(controllers.RootController): user = UserController() group = GroupController() + delegate = DelegationController() @expose(template="ipagui.templates.welcome") @identity.require(identity.not_anonymous()) 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..3b4967d6 --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/forms/delegate.py @@ -0,0 +1,86 @@ +import turbogears +from turbogears import validators, widgets + +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.userpassword, + 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] + +class DelegateFields(): + name = widgets.TextField(name="name", label="ACI 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") + + attrs = widgets.CheckBoxList(name="attrs", label="Can Modify", + options=aci_checkbox_attrs, validator=validators.NotEmpty) + +class DelegateNewValidator(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"), }) + attrs = validators.NotEmpty( + messages = { 'empty': _("Please select at least one value"), }) + +class DelegateNewForm(widgets.Form): + params = ['delegate', 'attr_list'] + + hidden_fields = [ + DelegateFields.source_group_dn, + DelegateFields.dest_group_dn, + DelegateFields.source_group_cn, + DelegateFields.dest_group_cn, + ] + + validator = DelegateNewValidator() + + def __init__(self, *args, **kw): + super(DelegateNewForm,self).__init__(*args, **kw) + (self.template_c, self.template) = widgets.meta.load_kid_template( + "ipagui.templates.delegatenewform") + self.delegate = DelegateFields + + def update_params(self, params): + super(DelegateNewForm,self).update_params(params) diff --git a/ipa-server/ipa-gui/ipagui/static/css/style.css b/ipa-server/ipa-gui/ipagui/static/css/style.css index ae845e86..fb97a67a 100644 --- a/ipa-server/ipa-gui/ipagui/static/css/style.css +++ b/ipa-server/ipa-gui/ipagui/static/css/style.css @@ -77,7 +77,7 @@ body { #main_content { background:#fff; float:right; - width:85%; + width:82%; min-height:500px; border-left: 1px solid #000; padding: 10px; @@ -92,7 +92,7 @@ body { #sidebar { background:#ccc; /* should be same as #page */ float:left; - width:10%; + width:13%; padding: 5px; font-size: medium; } @@ -206,6 +206,19 @@ body { background: #eee; } +/* + * Used for checkboxlist of aci attributes + */ +ul.requiredfield { + background: #ffffff; +} + +ul.checkboxlist { + padding: 0px; + margin: 0px; + list-style: none; +} + /* * TableKit css */ 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..8adbc7da --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/subcontrollers/delegation.py @@ -0,0 +1,168 @@ +import os +from pickle import dumps, loads +from base64 import b64encode, b64decode + +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 + +aci_fields = ['*', 'aci'] + +delegate_new_form = ipagui.forms.delegate.DelegateNewForm() + +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.not_anonymous()) + 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_new_form, delegate=delegate) + + @expose() + @identity.require(identity.not_anonymous()) + def create(self, **kw): + """Creates a new delegation""" + client = self.get_ipaclient() + + tg_errors, kw = self.delegatecreatevalidate(**kw) + if tg_errors: + return dict(form=delegate_new_form, delegate=kw, + tg_template='ipagui.templates.delegatenew') + + try: + 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') + + # not pulling down existing aci attributes + aci_entry = client.get_aci_entry(['dn']) + aci_entry.setValue('aci', new_aci.export_to_string()) + + # TODO - add a client.update_entry() call instead + client.update_group(aci_entry) + except ipaerror.IPAError, e: + turbogears.flash("Delgate add failed: " + str(e)) + return dict(form=delegate_new_form, delegate=kw, + tg_template='ipagui.templates.delegatenew') + + turbogears.flash("delegate created") + raise turbogears.redirect('/delegate/list') +# +# @expose("ipagui.templates.delegateedit") +# @identity.require(identity.not_anonymous()) +# def edit(self): +# """Display delegate page""" +# client = self.get_ipaclient() +# +# return dict(userfields=ipagui.forms.user.UserFields()) +# +# @expose() +# @identity.require(identity.not_anonymous()) +# def update(self, **kw): +# """Display delegate page""" +# client = self.get_ipaclient() +# +# turbogears.flash("delegate updated") +# raise turbogears.redirect('/delegate/list') + + @expose("ipagui.templates.delegatelist") + @identity.require(identity.not_anonymous()) + def list(self): + """Display delegate page""" + client = self.get_ipaclient() + + aci_entry = client.get_aci_entry(aci_fields) + aci_str_list = aci_entry.getValues('aci') + if aci_str_list is None: + 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 = self.extract_group_cns(aci_list, client) + + return dict(aci_list=aci_list, group_dn_to_cn=group_dn_to_cn) + + @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_new_form) + @identity.require(identity.not_anonymous()) + def delegatecreatevalidate(self, tg_errors=None, **kw): + return tg_errors, kw + + def extract_group_cns(self, 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.dn.str2dn(dn) + first_rdn = rdn_list[0] + for (type,value,junk) in first_rdn: + if type == "cn": + group_dn_to_cn[dn] = value + break; + 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-server/ipa-gui/ipagui/templates/delegategroupsearch.kid b/ipa-server/ipa-gui/ipagui/templates/delegategroupsearch.kid new file mode 100644 index 00000000..f97355f8 --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/templates/delegategroupsearch.kid @@ -0,0 +1,31 @@ +
+ + +
+
+ ${len(groups)} results returned: + + (truncated) + +
+ +
+ + + ${group.cn} + select +
+
+
+ No results found for "${criteria}" +
+
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..6cec389c --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/templates/delegatelayout.kid @@ -0,0 +1,16 @@ + + + + + + +
+
+ +
+
+ + + 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..c88b6e31 --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/templates/delegatelist.kid @@ -0,0 +1,60 @@ + + + + +Delegations + + + + +

Delegations

+ + + + + + + + + + + + + + + + + + + + + +
NamePeople in GroupCan ModifyFor People in GroupAction
+ ${aci.name} + + ${source_cn} + + ${", ".join(aci.attrs)} + + ${dest_cn} + + edit (TODO)
+
+ + + + + + + +
+ add new delegation
+
+ + 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..71d9e7e2 --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/templates/delegatenew.kid @@ -0,0 +1,15 @@ + + + + +Add Delegation + + + +

Add Delegation

+ + ${form.display(action=tg.url("/delegate/create"), value=delegate)} + + + diff --git a/ipa-server/ipa-gui/ipagui/templates/delegatenewform.kid b/ipa-server/ipa-gui/ipagui/templates/delegatenewform.kid new file mode 100644 index 00000000..95f93b5b --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/templates/delegatenewform.kid @@ -0,0 +1,154 @@ +
+ + + + + +
+ + + + + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + +
+ + + +
+ +
+ ${value_for(delegate.source_group_cn)} + change + +
+ +
+ + + +
+ +
+ ${value_for(delegate.dest_group_cn)} + change + +
+ +
+ + + + + +
+ +
+ + +
diff --git a/ipa-server/ipa-gui/ipagui/templates/master.kid b/ipa-server/ipa-gui/ipagui/templates/master.kid index 52b88e37..2926c4f9 100644 --- a/ipa-server/ipa-gui/ipagui/templates/master.kid +++ b/ipa-server/ipa-gui/ipagui/templates/master.kid @@ -77,6 +77,9 @@ Manage Policy
Self Service

+

+ Delegation Mgmt
+

diff --git a/ipa-server/xmlrpc-server/funcs.py b/ipa-server/xmlrpc-server/funcs.py index 517d54a7..0dd0c2c5 100644 --- a/ipa-server/xmlrpc-server/funcs.py +++ b/ipa-server/xmlrpc-server/funcs.py @@ -43,6 +43,7 @@ except ImportError: # Need a global to store this between requests _LDAPPool = None +ACIContainer = "cn=accounts" DefaultUserContainer = "cn=users,cn=accounts" DefaultGroupContainer = "cn=groups,cn=accounts" @@ -315,6 +316,14 @@ class IPAServer: return (exact_match_filter, partial_match_filter) +# Higher-level API + + def get_aci_entry(self, sattrs=None, opts=None): + """Returns the entry containing access control ACIs.""" + + 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=None, opts=None): diff --git a/ipa-server/xmlrpc-server/ipaxmlrpc.py b/ipa-server/xmlrpc-server/ipaxmlrpc.py index 805dbf07..3872ee21 100644 --- a/ipa-server/xmlrpc-server/ipaxmlrpc.py +++ b/ipa-server/xmlrpc-server/ipaxmlrpc.py @@ -317,6 +317,7 @@ def handler(req, profiling=False): try: f = funcs.IPAServer() h = ModXMLRPCRequestHandler() + 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.get_user_by_uid) -- cgit