diff options
Diffstat (limited to 'ipalib/plugins')
-rw-r--r-- | ipalib/plugins/__init__.py | 25 | ||||
-rw-r--r-- | ipalib/plugins/b_kerberos.py | 34 | ||||
-rw-r--r-- | ipalib/plugins/b_xmlrpc.py | 102 | ||||
-rw-r--r-- | ipalib/plugins/f_automount.py | 563 | ||||
-rw-r--r-- | ipalib/plugins/f_delegation.py | 65 | ||||
-rw-r--r-- | ipalib/plugins/f_group.py | 384 | ||||
-rw-r--r-- | ipalib/plugins/f_host.py | 286 | ||||
-rw-r--r-- | ipalib/plugins/f_hostgroup.py | 354 | ||||
-rw-r--r-- | ipalib/plugins/f_misc.py | 89 | ||||
-rw-r--r-- | ipalib/plugins/f_netgroup.py | 461 | ||||
-rw-r--r-- | ipalib/plugins/f_passwd.py | 70 | ||||
-rw-r--r-- | ipalib/plugins/f_pwpolicy.py | 122 | ||||
-rw-r--r-- | ipalib/plugins/f_ra.py | 117 | ||||
-rw-r--r-- | ipalib/plugins/f_service.py | 204 | ||||
-rw-r--r-- | ipalib/plugins/f_user.py | 367 |
15 files changed, 3243 insertions, 0 deletions
diff --git a/ipalib/plugins/__init__.py b/ipalib/plugins/__init__.py new file mode 100644 index 00000000..544429ef --- /dev/null +++ b/ipalib/plugins/__init__.py @@ -0,0 +1,25 @@ +# Authors: +# Jason Gerard DeRose <jderose@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 + +""" +Sub-package containing all core plugins. + +By convention, modules with frontend plugins are named f_*.py and modules +with backend plugins are named b_*.py. +""" diff --git a/ipalib/plugins/b_kerberos.py b/ipalib/plugins/b_kerberos.py new file mode 100644 index 00000000..cc820497 --- /dev/null +++ b/ipalib/plugins/b_kerberos.py @@ -0,0 +1,34 @@ +# Authors: +# Jason Gerard DeRose <jderose@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 + +""" +Backend plugin for Kerberos. + +This wraps the python-kerberos and python-krbV bindings. +""" + +from ipalib import api +from ipalib.backend import Backend + +class krb(Backend): + """ + Kerberos backend plugin. + """ + +api.register(krb) diff --git a/ipalib/plugins/b_xmlrpc.py b/ipalib/plugins/b_xmlrpc.py new file mode 100644 index 00000000..14f2a9be --- /dev/null +++ b/ipalib/plugins/b_xmlrpc.py @@ -0,0 +1,102 @@ +# Authors: +# Jason Gerard DeRose <jderose@redhat.com> +# 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 + +""" +Backend plugin for XML-RPC client. + +This provides a lightwieght XML-RPC client using Python standard library +``xmlrpclib`` module. +""" + +import xmlrpclib +import socket +import httplib +import kerberos +from ipalib.backend import Backend +from ipalib.util import xmlrpc_marshal +from ipalib import api +from ipalib import errors + +class xmlrpc(Backend): + """ + XML-RPC client backend plugin. + """ + + def get_client(self): + """ + Return an xmlrpclib.ServerProxy instance (the client). + """ + # FIXME: Rob, is there any reason we can't use allow_none=True here? + # Are there any reasonably common XML-RPC client implementations + # that don't support the <nil/> extension? + # See: http://docs.python.org/library/xmlrpclib.html + uri = self.env.xmlrpc_uri + if uri.startswith('https://'): + return xmlrpclib.ServerProxy(uri, + transport=KerbTransport(), + ) + return xmlrpclib.ServerProxy(uri) + + def forward_call(self, name, *args, **kw): + """ + Forward a call over XML-RPC to an IPA server. + """ + self.info('Forwarding %r call to %r' % (name, self.env.xmlrpc_uri)) + client = self.get_client() + command = getattr(client, name) + params = xmlrpc_marshal(*args, **kw) + try: + return command(*params) + except socket.error, e: + raise + except xmlrpclib.Fault, e: + err = errors.convertFault(e) + raise err + return + +api.register(xmlrpc) + +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/ipalib/plugins/f_automount.py b/ipalib/plugins/f_automount.py new file mode 100644 index 00000000..2365ce22 --- /dev/null +++ b/ipalib/plugins/f_automount.py @@ -0,0 +1,563 @@ +# 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 + +""" +Frontend plugins for automount. + +RFC 2707bis http://www.padl.com/~lukeh/rfc2307bis.txt +""" + +from ldap import explode_dn +from ipalib import crud, errors +from ipalib import api, Str, Flag, Object, Command + +map_attributes = ['automountMapName', 'description', ] +key_attributes = ['description', 'automountKey', 'automountInformation'] + +def display_entry(textui, entry): + # FIXME: for now delete dn here. In the future pass in the kw to + # output_for_cli() + attr = sorted(entry.keys()) + + for a in attr: + if a != 'dn': + textui.print_plain("%s: %s" % (a, entry[a])) + +def make_automount_dn(mapname): + """ + Construct automount dn from map name. + """ + # FIXME, should this be in b_ldap? + # Experimenting to see what a plugin looks like for a 3rd party who can't + # modify the backend. + import ldap + return 'automountmapname=%s,%s,%s' % ( + ldap.dn.escape_dn_chars(mapname), + api.env.container_automount, + api.env.basedn, + ) + +class automount(Object): + """ + Automount object. + """ + takes_params = ( + Str('automountmapname', + cli_name='mapname', + primary_key=True, + doc='A group of related automount objects', + ), + ) +api.register(automount) + + +class automount_addmap(crud.Add): + 'Add a new automount map.' + + takes_options = ( + Str('description?', + doc='A description of the automount map'), + ) + + def execute(self, mapname, **kw): + """ + Execute the automount-addmap operation. + + Returns the entry as it will be created in LDAP. + + :param mapname: The map name being added. + :param kw: Keyword arguments for the other LDAP attributes. + """ + assert 'automountmapname' not in kw + assert 'dn' not in kw + ldap = self.api.Backend.ldap + kw['automountmapname'] = mapname + kw['dn'] = make_automount_dn(mapname) + + kw['objectClass'] = ['automountMap'] + + return ldap.create(**kw) + + def output_for_cli(self, textui, result, map, **options): + """ + Output result of this command to command line interface. + """ + textui.print_plain("Automount map %s added" % map) + +api.register(automount_addmap) + + +class automount_addkey(crud.Add): + 'Add a new automount key.' + takes_options = ( + Str('automountkey', + cli_name='key', + doc='An entry in an automount map'), + Str('automountinformation', + cli_name='info', + doc='Mount information for this key'), + Str('description?', + doc='A description of the mount'), + ) + + def execute(self, mapname, **kw): + """ + Execute the automount-addkey operation. + + Returns the entry as it will be created in LDAP. + + :param mapname: The map name being added to. + :param kw: Keyword arguments for the other LDAP attributes. + """ + assert 'automountmapname' not in kw + assert 'dn' not in kw + ldap = self.api.Backend.ldap + # use find_entry_dn instead of make_automap_dn so we can confirm that + # the map exists + map_dn = ldap.find_entry_dn("automountmapname", mapname, "automountmap", api.env.container_automount) + kw['dn'] = "automountkey=%s,%s" % (kw['automountkey'], map_dn) + + kw['objectClass'] = ['automount'] + + return ldap.create(**kw) + + def output_for_cli(self, textui, result, *args, **options): + """ + Output result of this command to command line interface. + """ + textui.print_plain("Automount key added") + +api.register(automount_addkey) + + +class automount_delmap(crud.Del): + 'Delete an automount map.' + def execute(self, mapname, **kw): + """Delete an automount map. This will also remove all of the keys + associated with this map. + + mapname is the automount map to remove + + :param mapname: The map to be removed + :param kw: Not used. + """ + ldap = self.api.Backend.ldap + dn = ldap.find_entry_dn("automountmapname", mapname, "automountmap", api.env.container_automount) + keys = api.Command['automount_getkeys'](mapname) + if keys: + for k in keys: + ldap.delete(k.get('dn')) + return ldap.delete(dn) + def output_for_cli(self, textui, result, *args, **options): + """ + Output result of this command to command line interface. + """ + print "Automount map and associated keys deleted" + +api.register(automount_delmap) + + +class automount_delkey(crud.Del): + 'Delete an automount key.' + takes_options = ( + Str('automountkey', + cli_name='key', + doc='The automount key to remove'), + ) + def execute(self, mapname, **kw): + """Delete an automount key. + + key is the automount key to remove + + :param mapname: The automount map containing the key to be removed + :param kw: "key" the key to be removed + """ + ldap = self.api.Backend.ldap + dn = ldap.find_entry_dn("automountmapname", mapname, "automountmap", api.env.container_automount) + keys = api.Command['automount_getkeys'](mapname) + keydn = None + keyname = kw.get('automountkey').lower() + if keys: + for k in keys: + if k.get('automountkey').lower() == keyname: + keydn = k.get('dn') + break + if not keydn: + raise errors.NotFound + return ldap.delete(keydn) + def output_for_cli(self, textui, result, *args, **options): + """ + Output result of this command to command line interface. + """ + print "Automount key deleted" + +api.register(automount_delkey) + +class automount_modmap(crud.Mod): + 'Edit an existing automount map.' + takes_options = ( + Str('description?', + doc='A description of the automount map'), + ) + def execute(self, mapname, **kw): + """ + Execute the automount-modmap operation. + + The dn should not be passed as a keyword argument as it is constructed + by this method. + + Returns the entry + + :param mapname: The map name to update. + :param kw: Keyword arguments for the other LDAP attributes. + """ + assert 'automountmapname' not in kw + assert 'dn' not in kw + ldap = self.api.Backend.ldap + dn = ldap.find_entry_dn("automountmapname", mapname, "automountmap", api.env.container_automount) + return ldap.update(dn, **kw) + + def output_for_cli(self, textui, result, *args, **options): + """ + Output result of this command to command line interface. + """ + print "Automount map updated" + +api.register(automount_modmap) + + +class automount_modkey(crud.Mod): + 'Edit an existing automount key.' + takes_options = ( + Str('automountkey', + cli_name='key', + doc='An entry in an automount map'), + Str('automountinformation?', + cli_name='info', + doc='Mount information for this key'), + Str('description?', + doc='A description of the automount map'), + ) + def execute(self, mapname, **kw): + """ + Execute the automount-modkey operation. + + Returns the entry + + :param mapname: The map name to update. + :param kw: Keyword arguments for the other LDAP attributes. + """ + assert 'automountmapname' not in kw + assert 'dn' not in kw + keyname = kw.get('automountkey').lower() + del kw['automountkey'] + ldap = self.api.Backend.ldap + dn = ldap.find_entry_dn("automountmapname", mapname, "automountmap", api.env.container_automount) + keys = api.Command['automount_getkeys'](mapname) + keydn = None + if keys: + for k in keys: + if k.get('automountkey').lower() == keyname: + keydn = k.get('dn') + break + if not keydn: + raise errors.NotFound + return ldap.update(keydn, **kw) + + def output_for_cli(self, textui, result, *args, **options): + """ + Output result of this command to command line interface. + """ + print "Automount key updated" + +api.register(automount_modkey) + + +class automount_findmap(crud.Find): + 'Search automount maps.' + takes_options = ( + Flag('all', doc='Retrieve all attributes'), + ) + def execute(self, term, **kw): + ldap = self.api.Backend.ldap + + search_fields = map_attributes + + for s in search_fields: + kw[s] = term + + kw['objectclass'] = 'automountMap' + kw['base'] = api.env.container_automount + if kw.get('all', False): + kw['attributes'] = ['*'] + else: + kw['attributes'] = map_attributes + return ldap.search(**kw) + + def output_for_cli(self, textui, result, *args, **options): + counter = result[0] + entries = result[1:] + if counter == 0: + textui.print_plain("No entries found") + return + elif counter == -1: + textui.print_plain("These results are truncated.") + textui.print_plain("Please refine your search and try again.") + + for e in entries: + display_entry(textui, e) + textui.print_plain("") + +api.register(automount_findmap) + + +class automount_findkey(crud.Find): + 'Search automount keys.' + takes_options = ( + Flag('all?', doc='Retrieve all attributes'), + ) + def get_args(self): + return (Str('automountkey', + cli_name='key', + doc='An entry in an automount map'),) + def execute(self, term, **kw): + ldap = self.api.Backend.ldap + + search_fields = key_attributes + + for s in search_fields: + kw[s] = term + + kw['objectclass'] = 'automount' + kw['base'] = api.env.container_automount + if kw.get('all', False): + kw['attributes'] = ['*'] + else: + kw['attributes'] = key_attributes + return ldap.search(**kw) + def output_for_cli(self, textui, result, *args, **options): + counter = result[0] + entries = result[1:] + if counter == 0: + textui.print_plain("No entries found") + return + elif counter == -1: + textui.print_plain("These results are truncated.") + textui.print_plain("Please refine your search and try again.") + + for e in entries: + display_entry(textui, e) + textui.print_plain("") + +api.register(automount_findkey) + + +class automount_showmap(crud.Get): + 'Examine an existing automount map.' + takes_options = ( + Flag('all?', doc='Retrieve all attributes'), + ) + def execute(self, mapname, **kw): + """ + Execute the automount-showmap operation. + + Returns the entry + + :param mapname: The automount map to retrieve + :param kw: "all" set to True = return all attributes + """ + ldap = self.api.Backend.ldap + dn = ldap.find_entry_dn("automountmapname", mapname, "automountmap", api.env.container_automount) + # FIXME: should kw contain the list of attributes to display? + if kw.get('all', False): + return ldap.retrieve(dn) + else: + return ldap.retrieve(dn, map_attributes) + def output_for_cli(self, textui, result, *args, **options): + if result: + display_entry(textui, result) + +api.register(automount_showmap) + + +class automount_showkey(crud.Get): + 'Examine an existing automount key.' + takes_options = ( + Str('automountkey', + cli_name='key', + doc='The automount key to display'), + Flag('all?', doc='Retrieve all attributes'), + ) + def execute(self, mapname, **kw): + """ + Execute the automount-showkey operation. + + Returns the entry + + :param mapname: The mapname to examine + :param kw: "automountkey" the key to retrieve + :param kw: "all" set to True = return all attributes + """ + ldap = self.api.Backend.ldap + dn = ldap.find_entry_dn("automountmapname", mapname, "automountmap", api.env.container_automount) + keys = api.Command['automount_getkeys'](mapname) + keyname = kw.get('automountkey').lower() + keydn = None + if keys: + for k in keys: + if k.get('automountkey').lower() == keyname: + keydn = k.get('dn') + break + if not keydn: + raise errors.NotFound + # FIXME: should kw contain the list of attributes to display? + if kw.get('all', False): + return ldap.retrieve(keydn) + else: + return ldap.retrieve(keydn, key_attributes) + def output_for_cli(self, textui, result, *args, **options): + # The automount map name associated with this key is available only + # in the dn. Add it as an attribute to display instead. + if result and not result.get('automountmapname'): + elements = explode_dn(result.get('dn').lower()) + for e in elements: + (attr, value) = e.split('=',1) + if attr == 'automountmapname': + result['automountmapname'] = value + display_entry(textui, result) + +api.register(automount_showkey) + + +class automount_getkeys(Command): + 'Retrieve all keys for an automount map.' + takes_args = ( + Str('automountmapname', + cli_name='mapname', + primary_key=True, + doc='A group of related automount objects', + ), + ) + def execute(self, mapname, **kw): + """ + Execute the automount-getkeys operation. + + Return a list of all automount keys for this mapname + + :param mapname: Retrieve all keys for this mapname + """ + ldap = self.api.Backend.ldap + dn = ldap.find_entry_dn("automountmapname", mapname, "automountmap", api.env.container_automount) + try: + keys = ldap.get_one_entry(dn, 'objectclass=*', ['automountkey']) + except errors.NotFound: + keys = [] + + return keys + def output_for_cli(self, textui, result, *args, **options): + for k in result: + textui.print_plain('%s' % k.get('automountkey')) + +api.register(automount_getkeys) + + +class automount_getmaps(Command): + 'Retrieve all automount maps' + takes_args = ( + Str('automountmapname?', + cli_name='mapname', + primary_key=True, + doc='A group of related automount objects', + ), + ) + def execute(self, mapname, **kw): + """ + Execute the automount-getmaps operation. + + Return a list of all automount maps. + """ + + ldap = self.api.Backend.ldap + base = api.env.container_automount + "," + api.env.basedn + + if not mapname: + mapname = "auto.master" + search_base = "automountmapname=%s,%s" % (mapname, base) + maps = ldap.get_one_entry(search_base, "objectClass=*", ["*"]) + + return maps + def output_for_cli(self, textui, result, *args, **options): + for k in result: + textui.print_plain('%s: %s' % (k.get('automountinformation'), k.get('automountkey'))) + +api.register(automount_getmaps) + +class automount_addindirectmap(crud.Add): + """ + Add a new automap indirect mount point. + """ + + takes_options = ( + Str('parentmap?', + cli_name='parentmap', + default=u'auto.master', + doc='The parent map to connect this to.', + ), + Str('automountkey', + cli_name='key', + doc='An entry in an automount map', + ), + Str('description?', + doc='A description of the automount map', + ), + ) + + def execute(self, mapname, **kw): + """ + Execute the automount-addindirectmap operation. + + Returns the key entry as it will be created in LDAP. + + This function creates 2 LDAP entries. It creates an + automountmapname entry and an automountkey entry. + + :param mapname: The map name being added. + :param kw['parentmap'] is the top-level map to add this to. + defaulting to auto.master + :param kw['automountkey'] is the mount point + :param kw['description'] is a textual description of this map + """ + mapkw = {} + if kw.get('description'): + mapkw['description'] = kw.get('description') + newmap = api.Command['automount_addmap'](mapname, **mapkw) + + keykw = {'automountkey': kw['automountkey'], 'automountinformation': mapname} + if kw.get('description'): + keykw['description'] = kw.get('description') + newkey = api.Command['automount_addkey'](kw['parentmap'], **keykw) + + return newkey + def output_for_cli(self, textui, result, map, **options): + """ + Output result of this command to command line interface. + """ + textui.print_plain("Indirect automount map %s added" % map) + +api.register(automount_addindirectmap) diff --git a/ipalib/plugins/f_delegation.py b/ipalib/plugins/f_delegation.py new file mode 100644 index 00000000..fbf8cfbf --- /dev/null +++ b/ipalib/plugins/f_delegation.py @@ -0,0 +1,65 @@ +# 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 + +""" +Frontend plugins for delegations. +""" + +from ipalib import frontend +from ipalib import crud +from ipalib.frontend import Param +from ipalib import api +from ipalib import errors + +class delegation(frontend.Object): + """ + Delegation object. + """ + takes_params = ( + 'attributes', + 'source', + 'target', + Param('name', primary_key=True) + ) +api.register(delegation) + + +class delegation_add(crud.Add): + 'Add a new delegation.' +api.register(delegation_add) + + +class delegation_del(crud.Del): + 'Delete an existing delegation.' +api.register(delegation_del) + + +class delegation_mod(crud.Mod): + 'Edit an existing delegation.' +api.register(delegation_mod) + + +class delegation_find(crud.Find): + 'Search for a delegation.' +api.register(delegation_find) + + +class delegation_show(crud.Get): + 'Examine an existing delegation.' +api.register(delegation_show) diff --git a/ipalib/plugins/f_group.py b/ipalib/plugins/f_group.py new file mode 100644 index 00000000..740b32f8 --- /dev/null +++ b/ipalib/plugins/f_group.py @@ -0,0 +1,384 @@ +# Authors: +# Jason Gerard DeRose <jderose@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 + +""" +Frontend plugins for group (Identity). +""" + +from ipalib import api, crud, errors +from ipalib import Object, Command # Plugin base classes +from ipalib import Str, Int # Parameter types + + +def get_members(members): + """ + Return a list of members. + + It is possible that the value passed in is None. + """ + if members: + members = members.split(',') + else: + members = [] + + return members + +class group(Object): + """ + Group object. + """ + takes_params = ( + Str('description', + doc='A description of this group', + ), + Int('gidnumber?', + cli_name='gid', + doc='The gid to use for this group. If not included one is automatically set.', + ), + Str('cn', + cli_name='name', + primary_key=True, + normalizer=lambda value: value.lower(), + ), + ) +api.register(group) + + +class group_add(crud.Add): + 'Add a new group.' + + def execute(self, cn, **kw): + """ + Execute the group-add operation. + + The dn should not be passed as a keyword argument as it is constructed + by this method. + + Returns the entry as it will be created in LDAP. + + No need to explicitly set gidNumber. The dna_plugin will do this + for us if the value isn't provided by the caller. + + :param cn: The name of the group being added. + :param kw: Keyword arguments for the other LDAP attributes. + """ + assert 'cn' not in kw + assert 'dn' not in kw + ldap = self.api.Backend.ldap + kw['cn'] = cn + kw['dn'] = ldap.make_group_dn(cn) + + # Get our configuration + config = ldap.get_ipa_config() + + # some required objectclasses + kw['objectClass'] = config.get('ipagroupobjectclasses') + + return ldap.create(**kw) + + def output_for_cli(self, textui, result, *args, **options): + """ + Output result of this command to command line interface. + """ + textui.print_name(self.name) + textui.print_entry(result) + textui.print_dashed('Added group "%s"' % result['cn']) + +api.register(group_add) + + +class group_del(crud.Del): + 'Delete an existing group.' + def execute(self, cn, **kw): + """ + Delete a group + + The memberOf plugin handles removing the group from any other + groups. + + :param cn: The name of the group being removed + :param kw: Unused + """ + # We have 2 special groups, don't allow them to be removed +# if "admins" == cn.lower() or "editors" == cn.lower(): +# raise ipaerror.gen_exception(ipaerror.CONFIG_REQUIRED_GROUPS) + + ldap = self.api.Backend.ldap + dn = ldap.find_entry_dn("cn", cn, "posixGroup") + self.log.info("IPA: group-del '%s'" % dn) + + # Don't allow the default user group to be removed + config=ldap.get_ipa_config() + default_group = ldap.find_entry_dn("cn", config.get('ipadefaultprimarygroup'), "posixGroup") + if dn == default_group: + raise errors.DefaultGroup + + return ldap.delete(dn) + + def output_for_cli(self, textui, result, cn): + """ + Output result of this command to command line interface. + """ + textui.print_plain("Deleted group %s" % cn) + +api.register(group_del) + + +class group_mod(crud.Mod): + 'Edit an existing group.' + def execute(self, cn, **kw): + """ + Execute the group-mod operation. + + The dn should not be passed as a keyword argument as it is constructed + by this method. + + Returns the entry + + :param cn: The name of the group to update. + :param kw: Keyword arguments for the other LDAP attributes. + """ + assert 'cn' not in kw + assert 'dn' not in kw + ldap = self.api.Backend.ldap + dn = ldap.find_entry_dn("cn", cn, "posixGroup") + return ldap.update(dn, **kw) + + def output_for_cli(self, textui, result, cn, **options): + """ + Output result of this command to command line interface. + """ + if result: + textui.print_plain("Group updated") + +api.register(group_mod) + + +class group_find(crud.Find): + 'Search the groups.' + def execute(self, term, **kw): + ldap = self.api.Backend.ldap + + # Pull the list of searchable attributes out of the configuration. + config = ldap.get_ipa_config() + search_fields_conf_str = config.get('ipagroupsearchfields') + search_fields = search_fields_conf_str.split(",") + + search_kw = {} + for s in search_fields: + search_kw[s] = term + + object_type = ldap.get_object_type("cn") + if object_type and not kw.get('objectclass'): + search_kw['objectclass'] = object_type + return ldap.search(**search_kw) + + def output_for_cli(self, textui, result, uid, **options): + counter = result[0] + groups = result[1:] + if counter == 0 or len(groups) == 0: + textui.print_plain("No entries found") + return + if len(groups) == 1: + textui.print_entry(groups[0]) + return + textui.print_name(self.name) + + for g in groups: + textui.print_entry(g) + textui.print_plain('') + if counter == -1: + textui.print_plain("These results are truncated.") + textui.print_plain("Please refine your search and try again.") + textui.print_count(groups, '%d groups matched') + +api.register(group_find) + + +class group_show(crud.Get): + 'Examine an existing group.' + def execute(self, cn, **kw): + """ + Execute the group-show operation. + + The dn should not be passed as a keyword argument as it is constructed + by this method. + + Returns the entry + + :param cn: The group name to retrieve. + :param kw: Not used. + """ + ldap = self.api.Backend.ldap + dn = ldap.find_entry_dn("cn", cn, "posixGroup") + # FIXME: should kw contain the list of attributes to display? + return ldap.retrieve(dn) + + def output_for_cli(self, textui, result, *args, **options): + counter = result[0] + groups = result[1:] + if counter == 0 or len(groups) == 0: + textui.print_plain("No entries found") + return + if len(groups) == 1: + textui.print_entry(groups[0]) + return + textui.print_name(self.name) + for u in groups: + textui.print_plain('%(givenname)s %(sn)s:' % u) + textui.print_entry(u) + textui.print_plain('') + if counter == -1: + textui.print_plain('These results are truncated.') + textui.print_plain('Please refine your search and try again.') + textui.print_count(groups, '%d groups matched') + +api.register(group_show) + + +class group_add_member(Command): + 'Add a member to a group.' + takes_args = ( + Str('group', primary_key=True), + ) + takes_options = ( + Str('users?', doc='comma-separated list of users to add'), + Str('groups?', doc='comma-separated list of groups to add'), + ) + def execute(self, cn, **kw): + """ + Execute the group-add-member operation. + + Returns the updated group entry + + :param cn: The group name to add new members to. + :param kw: groups is a comma-separated list of groups to add + :parem kw: users is a comma-separated list of users to add + """ + ldap = self.api.Backend.ldap + dn = ldap.find_entry_dn("cn", cn) + add_failed = [] + to_add = [] + completed = 0 + + members = get_members(kw.get('groups', '')) + for m in members: + if not m: continue + try: + member_dn = ldap.find_entry_dn("cn", m) + to_add.append(member_dn) + except errors.NotFound: + add_failed.append(m) + continue + + members = get_members(kw.get('users', '')) + for m in members: + if not m: continue + try: + member_dn = ldap.find_entry_dn("uid", m) + to_add.append(member_dn) + except errors.NotFound: + add_failed.append(m) + continue + + for member_dn in to_add: + try: + ldap.add_member_to_group(member_dn, dn) + completed+=1 + except: + add_failed.append(member_dn) + + return add_failed + + def output_for_cli(self, textui, result, *args, **options): + """ + Output result of this command to command line interface. + """ + if result: + print "These entries failed to add to the group:" + for a in add_failed: + print "\t'%s'" % a + + +api.register(group_add_member) + + +class group_remove_member(Command): + 'Remove a member from a group.' + takes_args = ( + Str('group', primary_key=True), + ) + takes_options = ( + Str('users?', doc='comma-separated list of users to remove'), + Str('groups?', doc='comma-separated list of groups to remove'), + ) + def execute(self, cn, **kw): + """ + Execute the group-remove-member operation. + + Returns the members that could not be added + + :param cn: The group name to add new members to. + :param kw: groups is a comma-separated list of groups to remove + :parem kw: users is a comma-separated list of users to remove + """ + ldap = self.api.Backend.ldap + dn = ldap.find_entry_dn("cn", cn) + to_remove = [] + remove_failed = [] + completed = 0 + + members = get_members(kw.get('groups', '')) + for m in members: + if not m: continue + try: + member_dn = ldap.find_entry_dn("cn", m) + to_remove.append(member_dn) + except errors.NotFound: + remove_failed.append(m) + continue + + members = get_members(kw.get('users', '')) + for m in members: + try: + member_dn = ldap.find_entry_dn("uid", m,) + to_remove.append(member_dn) + except errors.NotFound: + remove_failed.append(m) + continue + + for member_dn in to_remove: + try: + ldap.remove_member_from_group(member_dn, dn) + completed+=1 + except: + remove_failed.append(member_dn) + + return remove_failed + + def output_for_cli(self, textui, result, *args, **options): + """ + Output result of this command to command line interface. + """ + if result: + print "These entries failed to be removed from the group:" + for a in remove_failed: + print "\t'%s'" % a + +api.register(group_remove_member) diff --git a/ipalib/plugins/f_host.py b/ipalib/plugins/f_host.py new file mode 100644 index 00000000..ea819a77 --- /dev/null +++ b/ipalib/plugins/f_host.py @@ -0,0 +1,286 @@ +# 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 + +""" +Frontend plugins for host/machine Identity. +""" + +from ipalib import api, crud, errors, util +from ipalib import Object # Plugin base class +from ipalib import Str, Flag # Parameter types + + +def get_host(hostname): + """ + Try to get the hostname as fully-qualified first, then fall back to + just a host name search. + """ + ldap = api.Backend.ldap + + # Strip off trailing dot + if hostname.endswith('.'): + hostname = hostname[:-1] + try: + dn = ldap.find_entry_dn("cn", hostname, "ipaHost") + except errors.NotFound: + dn = ldap.find_entry_dn("serverhostname", hostname, "ipaHost") + return dn + +def validate_host(ugettext, cn): + """ + Require at least one dot in the hostname (to support localhost.localdomain) + """ + dots = len(cn.split('.')) + if dots < 2: + return 'Fully-qualified hostname required' + return None + +default_attributes = ['cn','description','localityname','nshostlocation','nshardwareplatform','nsosversion'] + +class host(Object): + """ + Host object. + """ + takes_params = ( + Str('cn', validate_host, + cli_name='hostname', + primary_key=True, + normalizer=lambda value: value.lower(), + ), + Str('description?', + doc='Description of the host', + ), + Str('localityname?', + cli_name='locality', + doc='Locality of this host (Baltimore, MD)', + ), + Str('nshostlocation?', + cli_name='location', + doc='Location of this host (e.g. Lab 2)', + ), + Str('nshardwareplatform?', + cli_name='platform', + doc='Hardware platform of this host (e.g. Lenovo T61)', + ), + Str('nsosversion?', + cli_name='os', + doc='Operating System and version on this host (e.g. Fedora 9)', + ), + Str('userpassword?', + cli_name='password', + doc='Set a password to be used in bulk enrollment', + ), + ) +api.register(host) + + +class host_add(crud.Add): + 'Add a new host.' + def execute(self, hostname, **kw): + """ + Execute the host-add operation. + + The dn should not be passed as a keyword argument as it is constructed + by this method. + + If password is set then this is considered a 'bulk' host so we + do not create a kerberos service principal. + + Returns the entry as it will be created in LDAP. + + :param hostname: The name of the host being added. + :param kw: Keyword arguments for the other LDAP attributes. + """ + assert 'cn' not in kw + assert 'dn' not in kw + assert 'krbprincipalname' not in kw + ldap = self.api.Backend.ldap + + kw['cn'] = hostname + kw['serverhostname'] = hostname.split('.',1)[0] + kw['dn'] = ldap.make_host_dn(hostname) + + # FIXME: do a DNS lookup to ensure host exists + + current = util.get_current_principal() + if not current: + raise errors.NotFound('Unable to determine current user') + kw['enrolledby'] = ldap.find_entry_dn("krbPrincipalName", current, "posixAccount") + + # Get our configuration + config = ldap.get_ipa_config() + + # some required objectclasses + # FIXME: add this attribute to cn=ipaconfig + #kw['objectclass'] = config.get('ipahostobjectclasses') + kw['objectclass'] = ['nsHost', 'ipaHost', 'pkiUser'] + + # Ensure the list of objectclasses is lower-case + kw['objectclass'] = map(lambda z: z.lower(), kw.get('objectclass')) + + if not kw.get('userpassword', False): + kw['krbprincipalname'] = "host/%s@%s" % (hostname, self.api.env.realm) + + if 'krbprincipalaux' not in kw.get('objectclass'): + kw['objectclass'].append('krbprincipalaux') + else: + if 'krbprincipalaux' in kw.get('objectclass'): + kw['objectclass'].remove('krbprincipalaux') + + return ldap.create(**kw) + def output_for_cli(self, textui, result, *args, **options): + """ + Output result of this command to command line interface. + """ + textui.print_plain("Host added") + +api.register(host_add) + + +class host_del(crud.Del): + 'Delete an existing host.' + def execute(self, hostname, **kw): + """Delete a host. + + hostname is the name of the host to delete + + :param hostname: The name of the host being removed. + :param kw: Not used. + """ + ldap = self.api.Backend.ldap + dn = get_host(hostname) + return ldap.delete(dn) + def output_for_cli(self, textui, result, *args, **options): + """ + Output result of this command to command line interface. + """ + textui.print_plain("Host deleted") + +api.register(host_del) + + +class host_mod(crud.Mod): + 'Edit an existing host.' + def execute(self, hostname, **kw): + """ + Execute the host-mod operation. + + The dn should not be passed as a keyword argument as it is constructed + by this method. + + Returns the entry + + :param hostname: The name of the host to retrieve. + :param kw: Keyword arguments for the other LDAP attributes. + """ + assert 'cn' not in kw + assert 'dn' not in kw + ldap = self.api.Backend.ldap + dn = get_host(hostname) + return ldap.update(dn, **kw) + + def output_for_cli(self, textui, result, *args, **options): + """ + Output result of this command to command line interface. + """ + textui.print_plain("Host updated") + +api.register(host_mod) + + +class host_find(crud.Find): + 'Search the hosts.' + + takes_options = ( + Flag('all', doc='Retrieve all attributes'), + ) + + # FIXME: This should no longer be needed with the Param.query kwarg. +# def get_args(self): +# """ +# Override Find.get_args() so we can exclude the validation rules +# """ +# yield self.obj.primary_key.__clone__(rules=tuple()) + + def execute(self, term, **kw): + ldap = self.api.Backend.ldap + + # Pull the list of searchable attributes out of the configuration. + #config = ldap.get_ipa_config() + # FIXME: add this attribute to cn=ipaconfig + #search_fields_conf_str = config.get('ipahostsearchfields') + #search_fields = search_fields_conf_str.split(",") + search_fields = ['cn','serverhostname','description','localityname','nshostlocation','nshardwareplatform','nsosversion'] + + search_kw = {} + for s in search_fields: + search_kw[s] = term + + # Can't use ldap.get_object_type() since cn is also used for group dns + search_kw['objectclass'] = "ipaHost" + if kw.get('all', False): + search_kw['attributes'] = ['*'] + else: + search_kw['attributes'] = default_attributes + return ldap.search(**search_kw) + def output_for_cli(self, textui, result, *args, **options): + counter = result[0] + hosts = result[1:] + if counter == 0: + textui.print_plain("No entries found") + return + + for h in hosts: + textui.print_entry(h) + if counter == -1: + textui.print_plain("These results are truncated.") + textui.print_plain("Please refine your search and try again.") +api.register(host_find) + + +class host_show(crud.Get): + 'Examine an existing host.' + takes_options = ( + Flag('all', doc='Display all host attributes'), + ) + def execute(self, hostname, **kw): + """ + Execute the host-show operation. + + The dn should not be passed as a keyword argument as it is constructed + by this method. + + Returns the entry + + :param hostname: The login name of the host to retrieve. + :param kw: "all" set to True = return all attributes + """ + ldap = self.api.Backend.ldap + dn = get_host(hostname) + # FIXME: should kw contain the list of attributes to display? + if kw.get('all', False): + return ldap.retrieve(dn) + else: + value = ldap.retrieve(dn, default_attributes) + del value['dn'] + return value + def output_for_cli(self, textui, result, *args, **options): + textui.print_entry(result) + +api.register(host_show) diff --git a/ipalib/plugins/f_hostgroup.py b/ipalib/plugins/f_hostgroup.py new file mode 100644 index 00000000..706712c9 --- /dev/null +++ b/ipalib/plugins/f_hostgroup.py @@ -0,0 +1,354 @@ +# 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 + +""" +Frontend plugins for groups of hosts +""" + +from ipalib import api, crud, errors +from ipalib import Object, Command # Plugin base classes +from ipalib import Str # Parameter types + + +hostgroup_filter = "groupofnames)(!(objectclass=posixGroup)" + +def get_members(members): + """ + Return a list of members. + + It is possible that the value passed in is None. + """ + if members: + members = members.split(',') + else: + members = [] + + return members + +class hostgroup(Object): + """ + Host Group object. + """ + takes_params = ( + Str('description', + doc='A description of this group', + ), + Str('cn', + cli_name='name', + primary_key=True, + normalizer=lambda value: value.lower(), + ) + ) +api.register(hostgroup) + + +class hostgroup_add(crud.Add): + 'Add a new group of hosts.' + + def execute(self, cn, **kw): + """ + Execute the hostgroup-add operation. + + The dn should not be passed as a keyword argument as it is constructed + by this method. + + Returns the entry as it will be created in LDAP. + + No need to explicitly set gidNumber. The dna_plugin will do this + for us if the value isn't provided by the caller. + + :param cn: The name of the host group being added. + :param kw: Keyword arguments for the other LDAP attributes. + """ + assert 'cn' not in kw + assert 'dn' not in kw + ldap = self.api.Backend.ldap + kw['cn'] = cn + kw['dn'] = ldap.make_hostgroup_dn(cn) + + # Get our configuration + #config = ldap.get_ipa_config() + + # some required objectclasses + # FIXME: get this out of config + kw['objectClass'] = ['groupofnames'] + + return ldap.create(**kw) + + def output_for_cli(self, textui, result, *args, **options): + """ + Output result of this command to command line interface. + """ + textui.print_plain("Group added") + +api.register(hostgroup_add) + + +class hostgroup_del(crud.Del): + 'Delete an existing group of hosts.' + def execute(self, cn, **kw): + """ + Delete a group of hosts + + The memberOf plugin handles removing the group from any other + groups. + + :param cn: The name of the group being removed + :param kw: Unused + """ + ldap = self.api.Backend.ldap + dn = ldap.find_entry_dn("cn", cn, hostgroup_filter) + + return ldap.delete(dn) + + def output_for_cli(self, textui, result, *args, **options): + """ + Output result of this command to command line interface. + """ + textui.print_plain("Group deleted") + +api.register(hostgroup_del) + + +class hostgroup_mod(crud.Mod): + 'Edit an existing group of hosts.' + def execute(self, cn, **kw): + """ + Execute the hostgroup-mod operation. + + The dn should not be passed as a keyword argument as it is constructed + by this method. + + Returns the entry + + :param cn: The name of the group to update. + :param kw: Keyword arguments for the other LDAP attributes. + """ + assert 'cn' not in kw + assert 'dn' not in kw + ldap = self.api.Backend.ldap + dn = ldap.find_entry_dn("cn", cn, hostgroup_filter) + return ldap.update(dn, **kw) + + def output_for_cli(self, textui, result, *args, **options): + """ + Output result of this command to command line interface. + """ + texui.print_plain("Group updated") + +api.register(hostgroup_mod) + + +class hostgroup_find(crud.Find): + 'Search the groups of hosts.' + def execute(self, term, **kw): + ldap = self.api.Backend.ldap + + # Pull the list of searchable attributes out of the configuration. + config = ldap.get_ipa_config() + + # FIXME: for now use same search fields as user groups + search_fields_conf_str = config.get('ipagroupsearchfields') + search_fields = search_fields_conf_str.split(",") + + search_kw = {} + for s in search_fields: + search_kw[s] = term + + search_kw['objectclass'] = hostgroup_filter + return ldap.search(**search_kw) + + def output_for_cli(self, textui, result, *args, **options): + counter = result[0] + groups = result[1:] + if counter == 0: + textui.print_plain("No entries found") + return + + for g in groups: + textui.print_entry(g) + + if counter == -1: + textui.print_plain("These results are truncated.") + textui.print_plain("Please refine your search and try again.") + +api.register(hostgroup_find) + + +class hostgroup_show(crud.Get): + 'Examine an existing group of hosts.' + def execute(self, cn, **kw): + """ + Execute the hostgroup-show operation. + + The dn should not be passed as a keyword argument as it is constructed + by this method. + + Returns the entry + + :param cn: The group name to retrieve. + :param kw: Not used. + """ + ldap = self.api.Backend.ldap + # FIXME: this works for now but the plan is to add a new objectclass + # type. + dn = ldap.find_entry_dn("cn", cn, hostgroup_filter) + # FIXME: should kw contain the list of attributes to display? + return ldap.retrieve(dn) + + def output_for_cli(self, textui, result, *args, **options): + textui.print_entry(result) + +api.register(hostgroup_show) + + +class hostgroup_add_member(Command): + 'Add a member to a group.' + takes_args = ( + Str('group', primary_key=True), + ) + takes_options = ( + Str('groups?', doc='comma-separated list of host groups to add'), + Str('hosts?', doc='comma-separated list of hosts to add'), + ) + def execute(self, cn, **kw): + """ + Execute the hostgroup-add-member operation. + + Returns the updated group entry + + :param cn: The group name to add new members to. + :param kw: groups is a comma-separated list of host groups to add + :param kw: hosts is a comma-separated list of hosts to add + """ + ldap = self.api.Backend.ldap + dn = ldap.find_entry_dn("cn", cn, hostgroup_filter) + add_failed = [] + to_add = [] + completed = 0 + + members = get_members(kw.get('groups', '')) + for m in members: + if not m: continue + try: + member_dn = ldap.find_entry_dn("cn", m, hostgroup_filter) + to_add.append(member_dn) + except errors.NotFound: + add_failed.append(m) + continue + + members = get_members(kw.get('hosts', '')) + for m in members: + if not m: continue + try: + member_dn = ldap.find_entry_dn("cn", m, "ipaHost") + to_add.append(member_dn) + except errors.NotFound: + add_failed.append(m) + continue + + for member_dn in to_add: + try: + ldap.add_member_to_group(member_dn, dn) + completed+=1 + except: + add_failed.append(member_dn) + + return add_failed + + def output_for_cli(self, textui, result, *args, **options): + """ + Output result of this command to command line interface. + """ + if result: + textui.print_plain("These entries failed to add to the group:") + for a in result: + print "\t'%s'" % a + else: + textui.print_plain("Group membership updated.") + +api.register(hostgroup_add_member) + + +class hostgroup_remove_member(Command): + 'Remove a member from a group.' + takes_args = ( + Str('group', primary_key=True), + ) + takes_options = ( + Str('hosts?', doc='comma-separated list of hosts to add'), + Str('groups?', doc='comma-separated list of groups to remove'), + ) + def execute(self, cn, **kw): + """ + Execute the group-remove-member operation. + + Returns the members that could not be added + + :param cn: The group name to add new members to. + :param kw: groups is a comma-separated list of groups to remove + :param kw: hosts is a comma-separated list of hosts to add + """ + ldap = self.api.Backend.ldap + dn = ldap.find_entry_dn("cn", cn, hostgroup_filter) + to_remove = [] + remove_failed = [] + completed = 0 + + members = get_members(kw.get('groups', '')) + for m in members: + if not m: continue + try: + member_dn = ldap.find_entry_dn("cn", m, hostgroup_filter) + to_remove.append(member_dn) + except errors.NotFound: + remove_failed.append(m) + continue + + members = get_members(kw.get('hosts', '')) + for m in members: + if not m: continue + try: + member_dn = ldap.find_entry_dn("cn", m, "ipaHost") + to_remove.append(member_dn) + except errors.NotFound: + remove_failed.append(m) + continue + + for member_dn in to_remove: + try: + ldap.remove_member_from_group(member_dn, dn) + completed+=1 + except: + remove_failed.append(member_dn) + + return remove_failed + + def output_for_cli(self, textui, result, *args, **options): + """ + Output result of this command to command line interface. + """ + if result: + textui.print_plain("These entries failed to be removed from the group:") + for a in result: + print "\t'%s'" % a + else: + textui.print_plain("Group membership updated.") + +api.register(hostgroup_remove_member) diff --git a/ipalib/plugins/f_misc.py b/ipalib/plugins/f_misc.py new file mode 100644 index 00000000..a2f0fa4e --- /dev/null +++ b/ipalib/plugins/f_misc.py @@ -0,0 +1,89 @@ +# Authors: +# Jason Gerard DeRose <jderose@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 + +""" +Misc frontend plugins. +""" + +import re +from ipalib import api, LocalOrRemote + + + +# FIXME: We should not let env return anything in_server +# when mode == 'production'. This would allow an attacker to see the +# configuration of the server, potentially revealing compromising +# information. However, it's damn handy for testing/debugging. +class env(LocalOrRemote): + """Show environment variables""" + + takes_args = ('variables*',) + + def __find_keys(self, variables): + keys = set() + for query in variables: + if '*' in query: + pat = re.compile(query.replace('*', '.*') + '$') + for key in self.env: + if pat.match(key): + keys.add(key) + elif query in self.env: + keys.add(query) + return sorted(keys) + + def execute(self, variables, **options): + if variables is None: + keys = self.env + else: + keys = self.__find_keys(variables) + return tuple( + (key, self.env[key]) for key in keys + ) + + def output_for_cli(self, textui, result, variables, **options): + if len(result) == 0: + return + if len(result) == 1: + textui.print_keyval(result) + return + textui.print_name(self.name) + textui.print_keyval(result) + textui.print_count(result, '%d variables') + +api.register(env) + + +class plugins(LocalOrRemote): + """Show all loaded plugins""" + + def execute(self, **options): + plugins = sorted(self.api.plugins, key=lambda o: o.plugin) + return tuple( + (p.plugin, p.bases) for p in plugins + ) + + def output_for_cli(self, textui, result, **options): + textui.print_name(self.name) + for (plugin, bases) in result: + textui.print_indented( + '%s: %s' % (plugin, ', '.join(bases)) + ) + textui.print_count(result, '%d plugin loaded', '%s plugins loaded') + +api.register(plugins) diff --git a/ipalib/plugins/f_netgroup.py b/ipalib/plugins/f_netgroup.py new file mode 100644 index 00000000..6ee55b0d --- /dev/null +++ b/ipalib/plugins/f_netgroup.py @@ -0,0 +1,461 @@ +# Authors: +# Rob Crittenden <rcritten@redhat.com> +# +# Copyright (C) 2009 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 + +""" +Frontend plugin for netgroups. +""" + +from ipalib import api, crud, errors +from ipalib import Object, Command # Plugin base classes +from ipalib import Str # Parameter types +from ipalib import uuid + +netgroup_base = "cn=ng, cn=alt" +netgroup_filter = "ipaNISNetgroup" +hostgroup_filter = "groupofnames)(!(objectclass=posixGroup)" + +def get_members(members): + """ + Return a list of members. + + It is possible that the value passed in is None. + """ + if members: + members = members.split(',') + else: + members = [] + + return members + +def find_members(ldap, failed, members, attribute, filter=None): + """ + Return 2 lists: one a list of DNs found, one a list of errors + """ + found = [] + for m in members: + if not m: continue + try: + member_dn = ldap.find_entry_dn(attribute, m, filter) + found.append(member_dn) + except errors.NotFound: + failed.append(m) + continue + + return found, failed + +def add_members(ldap, completed, members, dn, memberattr): + add_failed = [] + for member_dn in members: + try: + ldap.add_member_to_group(member_dn, dn, memberattr) + completed+=1 + except: + add_failed.append(member_dn) + + return completed, add_failed + +def add_external(ldap, completed, members, cn): + failed = [] + netgroup = api.Command['netgroup_show'](cn) + external = netgroup.get('externalhost', []) + if not isinstance(external, list): + external = [external] + external_len = len(external) + for m in members: + if not m in external: + external.append(m) + completed+=1 + else: + failed.append(m) + if len(external) > external_len: + kw = {'externalhost': external} + ldap.update(netgroup['dn'], **kw) + + return completed, failed + +def remove_members(ldap, completed, members, dn, memberattr): + remove_failed = [] + for member_dn in members: + try: + ldap.remove_member_from_group(member_dn, dn, memberattr) + completed+=1 + except: + remove_failed.append(member_dn) + + return completed, remove_failed + +def remove_external(ldap, completed, members, cn): + failed = [] + netgroup = api.Command['netgroup_show'](cn) + external = netgroup.get('externalhost', []) + if not isinstance(external, list): + external = [external] + external_len = len(external) + for m in members: + try: + external.remove(m) + completed+=1 + except ValueError: + failed.append(m) + if len(external) < external_len: + kw = {'externalhost': external} + ldap.update(netgroup['dn'], **kw) + + return completed, failed + +class netgroup(Object): + """ + netgroups object. + """ + takes_params = ( + Str('cn', + cli_name='name', + primary_key=True + ), + Str('description', + doc='Description', + ), + Str('nisdomainname?', + cli_name='domainname', + doc='Domain name', + ), + ) +api.register(netgroup) + + +class netgroup_add(crud.Add): + 'Add a new netgroup.' + + def execute(self, cn, **kw): + """ + Execute the netgroup-add operation. + + The dn should not be passed as a keyword argument as it is constructed + by this method. + + Returns the entry as it will be created in LDAP. + + :param cn: The name of the netgroup + :param kw: Keyword arguments for the other LDAP attributes. + """ + self.log.info("IPA: netgroup-add '%s'" % cn) + assert 'cn' not in kw + assert 'dn' not in kw + ldap = self.api.Backend.ldap + kw['cn'] = cn +# kw['dn'] = ldap.make_netgroup_dn() + kw['ipauniqueid'] = str(uuid.uuid1()) + kw['dn'] = "ipauniqueid=%s,%s,%s" % (kw['ipauniqueid'], netgroup_base, api.env.basedn) + + if not kw.get('nisdomainname', False): + kw['nisdomainname'] = api.env.domain + + # some required objectclasses + kw['objectClass'] = ['top', 'ipaAssociation', 'ipaNISNetgroup'] + + return ldap.create(**kw) + + def output_for_cli(self, textui, result, *args, **options): + """ + Output result of this command to command line interface. + """ + textui.print_name(self.name) + textui.print_entry(result) + textui.print_dashed('Added netgroup "%s"' % result.get('cn')) + +api.register(netgroup_add) + + +class netgroup_del(crud.Del): + 'Delete an existing netgroup.' + + def execute(self, cn, **kw): + """Delete a netgroup. + + cn is the cn of the netgroup to delete + + The memberOf plugin handles removing the netgroup from any other + groups. + + :param cn: The name of the netgroup being removed. + :param kw: Not used. + """ + self.log.info("IPA: netgroup-del '%s'" % cn) + + ldap = self.api.Backend.ldap + dn = ldap.find_entry_dn("cn", cn, netgroup_filter, netgroup_base) + return ldap.delete(dn) + + def output_for_cli(self, textui, result, cn): + """ + Output result of this command to command line interface. + """ + textui.print_plain('Deleted net group "%s"' % cn) + +api.register(netgroup_del) + + +class netgroup_mod(crud.Mod): + 'Edit an existing netgroup.' + def execute(self, cn, **kw): + """ + Execute the netgroup-mod operation. + + The dn should not be passed as a keyword argument as it is constructed + by this method. + + Returns the entry + + :param cn: The name of the netgroup to retrieve. + :param kw: Keyword arguments for the other LDAP attributes. + """ + self.log.info("IPA: netgroup-mod '%s'" % cn) + assert 'cn' not in kw + assert 'dn' not in kw + ldap = self.api.Backend.ldap + dn = ldap.find_entry_dn("cn", cn, netgroup_filter, netgroup_base) + return ldap.update(dn, **kw) + + def output_for_cli(self, textui, result, cn, **options): + """ + Output result of this command to command line interface. + """ + textui.print_name(self.name) + textui.print_entry(result) + textui.print_dashed('Updated netgroup "%s"' % result['cn']) + +api.register(netgroup_mod) + + +class netgroup_find(crud.Find): + 'Search the netgroups.' + def execute(self, term, **kw): + ldap = self.api.Backend.ldap + + search_fields = ['ipauniqueid','description','nisdomainname','cn'] + + search_kw = {} + for s in search_fields: + search_kw[s] = term + + search_kw['objectclass'] = netgroup_filter + search_kw['base'] = netgroup_base + return ldap.search(**search_kw) + + def output_for_cli(self, textui, result, *args, **options): + counter = result[0] + groups = result[1:] + if counter == 0 or len(groups) == 0: + textui.print_plain("No entries found") + return + if len(groups) == 1: + textui.print_entry(groups[0]) + return + textui.print_name(self.name) + for g in groups: + textui.print_entry(g) + textui.print_plain('') + if counter == -1: + textui.print_plain('These results are truncated.') + textui.print_plain('Please refine your search and try again.') + textui.print_count(groups, '%d netgroups matched') + +api.register(netgroup_find) + + +class netgroup_show(crud.Get): + 'Examine an existing netgroup.' + def execute(self, cn, **kw): + """ + Execute the netgroup-show operation. + + The dn should not be passed as a keyword argument as it is constructed + by this method. + + Returns the entry + + :param cn: The name of the netgroup to retrieve. + :param kw: Unused + """ + ldap = self.api.Backend.ldap + dn = ldap.find_entry_dn("cn", cn, netgroup_filter, netgroup_base) + return ldap.retrieve(dn) + + def output_for_cli(self, textui, result, *args, **options): + textui.print_entry(result) + +api.register(netgroup_show) + +class netgroup_add_member(Command): + 'Add a member to a group.' + takes_args = ( + Str('cn', + cli_name='name', + primary_key=True + ), + ) + takes_options = ( + Str('hosts?', doc='comma-separated list of hosts to add'), + Str('hostgroups?', doc='comma-separated list of host groups to add'), + Str('users?', doc='comma-separated list of users to add'), + Str('groups?', doc='comma-separated list of groups to add'), + ) + + def execute(self, cn, **kw): + """ + Execute the netgroup-add-member operation. + + Returns the updated group entry + + :param cn: The netgroup name to add new members to. + :param kw: hosts is a comma-separated list of hosts to add + :param kw: hostgroups is a comma-separated list of host groups to add + :param kw: users is a comma-separated list of users to add + :param kw: groups is a comma-separated list of host to add + """ + ldap = self.api.Backend.ldap + dn = ldap.find_entry_dn("cn", cn, netgroup_filter, netgroup_base) + add_failed = [] + to_add = [] + completed = 0 + + # Hosts + members = get_members(kw.get('hosts', '')) + (to_add, add_failed) = find_members(ldap, add_failed, members, "cn", "ipaHost") + + # If a host is not found we'll consider it an externalHost. It will + # be up to the user to handle typos + if add_failed: + (completed, failed) = add_external(ldap, completed, add_failed, cn) + add_failed = failed + + (completed, failed) = add_members(ldap, completed, to_add, dn, 'memberhost') + add_failed+=failed + + # Host groups + members = get_members(kw.get('hostgroups', '')) + (to_add, add_failed) = find_members(ldap, add_failed, members, "cn", hostgroup_filter) + (completed, failed) = add_members(ldap, completed, to_add, dn, 'memberhost') + add_failed+=failed + + # User + members = get_members(kw.get('users', '')) + (to_add, add_failed) = find_members(ldap, add_failed, members, "uid") + (completed, failed) = add_members(ldap, completed, to_add, dn, 'memberuser') + add_failed+=failed + + # Groups + members = get_members(kw.get('groups', '')) + (to_add, add_failed) = find_members(ldap, add_failed, members, "cn", "posixGroup") + (completed, failed) = add_members(ldap, completed, to_add, dn, 'memberuser') + add_failed+=failed + + return add_failed + + def output_for_cli(self, textui, result, *args, **options): + """ + Output result of this command to command line interface. + """ + if result: + textui.print_plain("These entries failed to add to the group:") + for a in result: + print "\t'%s'" % a + else: + textui.print_plain("netgroup membership updated.") + +api.register(netgroup_add_member) + + +class netgroup_remove_member(Command): + 'Remove a member from a group.' + takes_args = ( + Str('cn', + cli_name='name', + primary_key=True + ), + ) + takes_options = ( + Str('hosts?', doc='comma-separated list of hosts to remove'), + Str('hostgroups?', doc='comma-separated list of groups to remove'), + Str('users?', doc='comma-separated list of users to remove'), + Str('groups?', doc='comma-separated list of groups to remove'), + ) + def execute(self, cn, **kw): + """ + Execute the group-remove-member operation. + + Returns the members that could not be added + + :param cn: The group name to add new members to. + :param kw: hosts is a comma-separated list of hosts to remove + :param kw: hostgroups is a comma-separated list of host groups to remove + :param kw: users is a comma-separated list of users to remove + :param kw: groups is a comma-separated list of host to remove + """ + ldap = self.api.Backend.ldap + dn = ldap.find_entry_dn("cn", cn, netgroup_filter, netgroup_base) + remove_failed = [] + to_remove = [] + completed = 0 + + # Hosts + members = get_members(kw.get('hosts', '')) + (to_remove, remove_failed) = find_members(ldap, remove_failed, members, "cn", "ipaHost") + + # If a host is not found we'll consider it an externalHost. It will + # be up to the user to handle typos + if remove_failed: + (completed, failed) = remove_external(ldap, completed, remove_failed, cn) + remove_failed = failed + + (completed, failed) = remove_members(ldap, completed, to_remove, dn, 'memberhost') + remove_failed+=failed + + # Host groups + members = get_members(kw.get('hostgroups', '')) + (to_remove, remove_failed) = find_members(ldap, remove_failed, members, "cn", hostgroup_filter) + (completed, failed) = remove_members(ldap, completed, to_remove, dn, 'memberhost') + remove_failed+=failed + + # User + members = get_members(kw.get('users', '')) + (to_remove, remove_failed) = find_members(ldap, remove_failed, members, "uid") + (completed, failed) = remove_members(ldap, completed, to_remove, dn, 'memberuser') + remove_failed+=failed + + # Groups + members = get_members(kw.get('groups', '')) + (to_remove, remove_failed) = find_members(ldap, remove_failed, members, "cn", "posixGroup") + (completed, failed) = remove_members(ldap, completed, to_remove, dn, 'memberuser') + remove_failed+=failed + + return remove_failed + + def output_for_cli(self, textui, result, *args, **options): + """ + Output result of this command to command line interface. + """ + if result: + textui.print_plain("These entries failed to be removed from the group:") + for a in result: + print "\t'%s'" % a + else: + textui.print_plain("netgroup membership updated.") + +api.register(netgroup_remove_member) diff --git a/ipalib/plugins/f_passwd.py b/ipalib/plugins/f_passwd.py new file mode 100644 index 00000000..ea78c4c1 --- /dev/null +++ b/ipalib/plugins/f_passwd.py @@ -0,0 +1,70 @@ +# 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 + +""" +Frontend plugins for password changes. +""" + +from ipalib import api, errors, util +from ipalib import Command # Plugin base classes +from ipalib import Str, Password # Parameter types + + +class passwd(Command): + 'Edit existing password policy.' + + takes_args = ( + Str('principal', + cli_name='user', + primary_key=True, + default_from=util.get_current_principal, + ), + Password('password'), + ) + + def execute(self, principal, password): + """ + Execute the passwd operation. + + The dn should not be passed as a keyword argument as it is constructed + by this method. + + Returns the entry + + :param param uid: The login name of the user being updated. + :param kw: Not used. + """ + if principal.find('@') > 0: + u = principal.split('@') + if len(u) > 2: + raise errors.InvalidUserPrincipal, principal + else: + principal = principal+"@"+self.api.env.realm + dn = self.Backend.ldap.find_entry_dn( + "krbprincipalname", + principal, + "posixAccount" + ) + return self.Backend.ldap.modify_password(dn, newpass=password) + + def output_for_cli(self, textui, result, principal, password): + assert password is None + textui.print_plain('Changed password for "%s"' % principal) + +api.register(passwd) diff --git a/ipalib/plugins/f_pwpolicy.py b/ipalib/plugins/f_pwpolicy.py new file mode 100644 index 00000000..d914ce72 --- /dev/null +++ b/ipalib/plugins/f_pwpolicy.py @@ -0,0 +1,122 @@ +# 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 + +""" +Frontend plugins for password policy. +""" + +from ipalib import api +from ipalib import Command # Plugin base classes +from ipalib import Int # Parameter types + + +class pwpolicy_mod(Command): + 'Edit existing password policy.' + takes_options = ( + Int('krbmaxpwdlife?', + cli_name='maxlife', + doc='Max. Password Lifetime (days)' + ), + Int('krbminpwdlife?', + cli_name='minlife', + doc='Min. Password Lifetime (hours)' + ), + Int('krbpwdhistorylength?', + cli_name='history', + doc='Password History Size' + ), + Int('krbpwdmindiffchars?', + cli_name='minclasses', + doc='Min. Number of Character Classes' + ), + Int('krbpwdminlength?', + cli_name='minlength', + doc='Min. Length of Password' + ), + ) + def execute(self, *args, **kw): + """ + Execute the pwpolicy-mod operation. + + The dn should not be passed as a keyword argument as it is constructed + by this method. + + Returns the entry + + :param args: This function takes no positional arguments + :param kw: Keyword arguments for the other LDAP attributes. + """ + assert 'dn' not in kw + ldap = self.api.Backend.ldap + dn = ldap.find_entry_dn("cn", "accounts", "krbPwdPolicy") + + # The LDAP routines want strings, not ints, so convert a few + # things. Otherwise it sees a string -> int conversion as a change. + for k in kw.iterkeys(): + if k.startswith("krb", 0, 3): + kw[k] = str(kw[k]) + + # Convert hours and days to seconds + if kw.get('krbmaxpwdlife'): + kw['krbmaxpwdlife'] = str(int(kw.get('krbmaxpwdlife')) * 86400) + if kw.get('krbminpwdlife'): + kw['krbminpwdlife'] = str(int(kw.get('krbminpwdlife')) * 3600) + + return ldap.update(dn, **kw) + + def output_for_cli(self, textui, result, *args, **options): + textui.print_plain("Policy modified") + +api.register(pwpolicy_mod) + + +class pwpolicy_show(Command): + 'Retrieve current password policy' + def execute(self, *args, **kw): + """ + Execute the pwpolicy-show operation. + + The dn should not be passed as a keyword argument as it is constructed + by this method. + + Returns the entry + + :param args: Not used. + :param kw: Not used. + """ + ldap = self.api.Backend.ldap + dn = ldap.find_entry_dn("cn", "accounts", "krbPwdPolicy") + + policy = ldap.retrieve(dn) + + # 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 output_for_cli(self, textui, result, *args, **options): + textui.print_plain("Password Policy") + textui.print_plain("Min. Password Lifetime (hours): %s" % result.get('krbminpwdlife')) + textui.print_plain("Max. Password Lifetime (days): %s" % result.get('krbmaxpwdlife')) + textui.print_plain("Min. Number of Character Classes: %s" % result.get('krbpwdmindiffchars')) + textui.print_plain("Min. Length of Password: %s" % result.get('krbpwdminlength')) + textui.print_plain("Password History Size: %s" % result.get('krbpwdhistorylength')) + +api.register(pwpolicy_show) diff --git a/ipalib/plugins/f_ra.py b/ipalib/plugins/f_ra.py new file mode 100644 index 00000000..7ac84e65 --- /dev/null +++ b/ipalib/plugins/f_ra.py @@ -0,0 +1,117 @@ +# Authors: +# Andrew Wnuk <awnuk@redhat.com> +# Jason Gerard DeRose <jderose@redhat.com> +# +# Copyright (C) 2009 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 + +""" +Frontend plugins for IPA-RA PKI operations. +""" + +from ipalib import api, Command, Str, Int + + +class request_certificate(Command): + """ Submit a certificate request. """ + + takes_args = ['csr'] + + takes_options = [Str('request_type?', default=u'pkcs10')] + + def execute(self, csr, **options): + return self.Backend.ra.request_certificate(csr, **options) + + def output_for_cli(self, textui, result, *args, **options): + if isinstance(result, dict) and len(result) > 0: + textui.print_entry(result, 0) + else: + textui.print_plain('Failed to submit a certificate request.') + +api.register(request_certificate) + + +class get_certificate(Command): + """ Retrieve an existing certificate. """ + + takes_args = ['serial_number'] + + def execute(self, serial_number, **options): + return self.Backend.ra.get_certificate(serial_number) + + def output_for_cli(self, textui, result, *args, **options): + if isinstance(result, dict) and len(result) > 0: + textui.print_entry(result, 0) + else: + textui.print_plain('Failed to obtain a certificate.') + +api.register(get_certificate) + + +class check_request_status(Command): + """ Check a request status. """ + + takes_args = ['request_id'] + + + def execute(self, request_id, **options): + return self.Backend.ra.check_request_status(request_id) + + def output_for_cli(self, textui, result, *args, **options): + if isinstance(result, dict) and len(result) > 0: + textui.print_entry(result, 0) + else: + textui.print_plain('Failed to retrieve a request status.') + +api.register(check_request_status) + + +class revoke_certificate(Command): + """ Revoke a certificate. """ + + takes_args = ['serial_number'] + + # FIXME: The default is 0. Is this really an Int param? + takes_options = [Int('revocation_reason?', default=0)] + + + def execute(self, serial_number, **options): + return self.Backend.ra.revoke_certificate(serial_number, **options) + + def output_for_cli(self, textui, result, *args, **options): + if isinstance(result, dict) and len(result) > 0: + textui.print_entry(result, 0) + else: + textui.print_plain('Failed to revoke a certificate.') + +api.register(revoke_certificate) + + +class take_certificate_off_hold(Command): + """ Take a revoked certificate off hold. """ + + takes_args = ['serial_number'] + + def execute(self, serial_number, **options): + return self.Backend.ra.take_certificate_off_hold(serial_number) + + def output_for_cli(self, textui, result, *args, **options): + if isinstance(result, dict) and len(result) > 0: + textui.print_entry(result, 0) + else: + textui.print_plain('Failed to take a revoked certificate off hold.') + +api.register(take_certificate_off_hold) diff --git a/ipalib/plugins/f_service.py b/ipalib/plugins/f_service.py new file mode 100644 index 00000000..06d6a5d0 --- /dev/null +++ b/ipalib/plugins/f_service.py @@ -0,0 +1,204 @@ +# Authors: +# Jason Gerard DeRose <jderose@redhat.com> +# 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 + +""" +Frontend plugins for service (Identity). +""" + +from ipalib import api, crud, errors +from ipalib import Object # Plugin base classes +from ipalib import Str, Flag # Parameter types + + +class service(Object): + """ + Service object. + """ + takes_params = ( + Str('principal', primary_key=True), + ) +api.register(service) + + +class service_add(crud.Add): + """ + Add a new service. + """ + + takes_options = ( + Flag('force', + doc='Force a service principal name', + ), + ) + def execute(self, principal, **kw): + """ + Execute the service-add operation. + + The dn should not be passed as a keyword argument as it is constructed + by this method. + + Returns the entry as it will be created in LDAP. + + :param principal: The service to be added in the form: service/hostname + :param kw: Keyword arguments for the other LDAP attributes. + """ + assert 'krbprincipalname' not in kw + ldap = self.api.Backend.ldap + + force = kw.get('force', False) + try: + del kw['force'] + except: + pass + + # Break down the principal into its component parts, which may or + # may not include the realm. + sp = principal.split('/') + if len(sp) != 2: + raise errors.MalformedServicePrincipal + service = sp[0] + + if service.lower() == "host": + raise errors.HostService + + sr = sp[1].split('@') + if len(sr) == 1: + hostname = sr[0].lower() + realm = self.api.env.realm + elif len(sr) == 2: + hostname = sr[0].lower() + realm = sr[1] + else: + raise MalformedServicePrincipal + + """ + FIXME once DNS client is done + if not force: + fqdn = hostname + "." + rs = dnsclient.query(fqdn, dnsclient.DNS_C_IN, dnsclient.DNS_T_A) + if len(rs) == 0: + self.log.debug("IPA: DNS A record lookup failed for '%s'" % hostname) + raise ipaerror.gen_exception(ipaerror.INPUT_NOT_DNS_A_RECORD) + else: + self.log.debug("IPA: found %d records for '%s'" % (len(rs), hostname)) + """ + + # At some point we'll support multiple realms + if (realm != self.api.env.realm): + raise errors.RealmMismatch + + # Put the principal back together again + princ_name = service + "/" + hostname + "@" + realm + + dn = ldap.make_service_dn(princ_name) + + kw['dn'] = dn + kw['objectClass'] = ['krbPrincipal', 'krbPrincipalAux', 'krbTicketPolicyAux'] + + return ldap.create(**kw) + + def output_to_cli(self, ret): + if ret: + print "Service added" + +api.register(service_add) + + +class service_del(crud.Del): + 'Delete an existing service.' + def execute(self, principal, **kw): + """ + Delete a service principal. + + principal is the krbprincipalname of the entry to delete. + + This should be called with much care. + + :param principal: The service to be added in the form: service/hostname + :param kw: not used + """ + ldap = self.api.Backend.ldap + dn = ldap.find_entry_dn("krbprincipalname", principal) + return ldap.delete(dn) + + def output_to_cli(self, ret): + if ret: + print "Service removed" + +api.register(service_del) + +# There is no service-mod. The principal itself contains nothing that +# is user-changeable + +class service_find(crud.Find): + 'Search the existing services.' + def execute(self, principal, **kw): + ldap = self.api.Backend.ldap + + search_kw = {} + search_kw['filter'] = "&(objectclass=krbPrincipalAux)(!(objectClass=posixAccount))(!(|(krbprincipalname=kadmin/*)(krbprincipalname=K/M@*)(krbprincipalname=krbtgt/*)))" + search_kw['krbprincipalname'] = principal + + object_type = ldap.get_object_type("krbprincipalname") + if object_type and not kw.get('objectclass'): + search_kw['objectclass'] = object_type + + return ldap.search(**search_kw) + + def output_for_cli(self, textui, result, *args, **options): + counter = result[0] + services = result[1:] + if counter == 0: + textui.print_plain("No entries found") + return + + for s in services: + textui.print_entry(s) + + if counter == -1: + textui.print_plain("These results are truncated.") + textui.print_plain("Please refine your search and try again.") + textui.print_count(services, '%d services matched') + +api.register(service_find) + + +class service_show(crud.Get): + 'Examine an existing service.' + def execute(self, principal, **kw): + """ + Execute the service-show operation. + + The dn should not be passed as a keyword argument as it is constructed + by this method. + + Returns the entry + + :param principal: The service principal to retrieve + :param kw: Not used. + """ + ldap = self.api.Backend.ldap + dn = ldap.find_entry_dn("krbprincipalname", principal) + # FIXME: should kw contain the list of attributes to display? + return ldap.retrieve(dn) + def output_for_cli(self, textui, result, *args, **options): + textui.print_entry(result) + +api.register(service_show) diff --git a/ipalib/plugins/f_user.py b/ipalib/plugins/f_user.py new file mode 100644 index 00000000..506ad14d --- /dev/null +++ b/ipalib/plugins/f_user.py @@ -0,0 +1,367 @@ +# Authors: +# Jason Gerard DeRose <jderose@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 + +""" +Frontend plugins for user (Identity). +""" + +from ipalib import api, crud, errors +from ipalib import Object, Command # Plugin base classes +from ipalib import Str, Password, Flag, Int # Parameter types + + +def display_user(user): + # FIXME: for now delete dn here. In the future pass in the kw to + # output_for_cli() + attr = sorted(user.keys()) + # Always have sn following givenname + try: + l = attr.index('givenname') + attr.remove('sn') + attr.insert(l+1, 'sn') + except ValueError: + pass + + for a in attr: + if a != 'dn': + print "%s: %s" % (a, user[a]) + +default_attributes = ['uid','givenname','sn','homeDirectory','loginshell'] + + +class user(Object): + """ + User object. + """ + + takes_params = ( + Str('givenname', + cli_name='first', + doc="User's first name", + ), + Str('sn', + cli_name='last', + doc="User's last name", + ), + Str('uid', + cli_name='user', + primary_key=True, + default_from=lambda givenname, sn: givenname[0] + sn, + normalizer=lambda value: value.lower(), + ), + Str('gecos?', + doc='GECOS field', + default_from=lambda uid: uid, + ), + Str('homedirectory?', + cli_name='home', + doc="User's home directory", + default_from=lambda uid: '/home/%s' % uid, + ), + Str('loginshell?', + cli_name='shell', + default=u'/bin/sh', + doc="User's Login shell", + ), + Str('krbprincipalname?', + cli_name='principal', + doc="User's Kerberos Principal name", + default_from=lambda uid: '%s@%s' % (uid, api.env.realm), + ), + Str('mailaddress?', + cli_name='email', + doc="User's e-mail address", + ), + Password('userpassword?', + cli_name='password', + doc="Set user's password", + ), + Str('groups?', + doc='Add account to one or more groups (comma-separated)', + ), + Int('uidnumber?', + cli_name='uid', + doc='The uid to use for this user. If not included one is automatically set.', + ), + ) + +api.register(user) + + +class user_add(crud.Add): + 'Add a new user.' + + def execute(self, uid, **kw): + """ + Execute the user-add operation. + + The dn should not be passed as a keyword argument as it is constructed + by this method. + + Returns the entry as it will be created in LDAP. + + :param uid: The login name of the user being added. + :param kw: Keyword arguments for the other LDAP attributes. + """ + assert 'uid' not in kw + assert 'dn' not in kw + ldap = self.api.Backend.ldap + kw['uid'] = uid + kw['dn'] = ldap.make_user_dn(uid) + + # FIXME: enforce this elsewhere +# if servercore.uid_too_long(kw['uid']): +# raise errors.UsernameTooLong + + # Get our configuration + config = ldap.get_ipa_config() + + # Let us add in some missing attributes + if kw.get('homedirectory') is None: + kw['homedirectory'] = '%s/%s' % (config.get('ipahomesrootdir'), kw.get('uid')) + kw['homedirectory'] = kw['homedirectory'].replace('//', '/') + kw['homedirectory'] = kw['homedirectory'].rstrip('/') + if kw.get('loginshell') is None: + kw['loginshell'] = config.get('ipadefaultloginshell') + if kw.get('gecos') is None: + kw['gecos'] = kw['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. + + if not kw.get('gidnumber'): + try: + group_dn = ldap.find_entry_dn("cn", config.get('ipadefaultprimarygroup')) + default_group = ldap.retrieve(group_dn, ['dn','gidNumber']) + if default_group: + kw['gidnumber'] = default_group.get('gidnumber') + except errors.NotFound: + # Fake an LDAP error so we can return something useful to the kw + raise errors.NotFound, "The default group for new kws, '%s', cannot be found." % config.get('ipadefaultprimarygroup') + except Exception, e: + # catch everything else + raise e + + if kw.get('krbprincipalname') is None: + kw['krbprincipalname'] = "%s@%s" % (kw.get('uid'), self.api.env.realm) + + # FIXME. This is a hack so we can request separate First and Last + # name in the GUI. + if kw.get('cn') is None: + kw['cn'] = "%s %s" % (kw.get('givenname'), + kw.get('sn')) + + # some required objectclasses + kw['objectClass'] = config.get('ipauserobjectclasses') + + return ldap.create(**kw) + + def output_for_cli(self, textui, result, *args, **options): + """ + Output result of this command to command line interface. + """ + textui.print_name(self.name) + textui.print_entry(result) + textui.print_dashed('Added user "%s"' % result['uid']) + +api.register(user_add) + + +class user_del(crud.Del): + 'Delete an existing user.' + + def execute(self, uid, **kw): + """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. + + :param uid: The login name of the user being added. + :param kw: Not used. + """ + if uid == "admin": + # FIXME: do we still want a "special" user? + raise SyntaxError("admin required") +# raise ipaerror.gen_exception(ipaerror.INPUT_ADMIN_REQUIRED) + self.log.info("IPA: user-del '%s'" % uid) + + ldap = self.api.Backend.ldap + dn = ldap.find_entry_dn("uid", uid) + return ldap.delete(dn) + + def output_for_cli(self, textui, result, uid): + """ + Output result of this command to command line interface. + """ + textui.print_plain('Deleted user "%s"' % uid) + +api.register(user_del) + + +class user_mod(crud.Mod): + 'Edit an existing user.' + def execute(self, uid, **kw): + """ + Execute the user-mod operation. + + The dn should not be passed as a keyword argument as it is constructed + by this method. + + Returns the entry + + :param uid: The login name of the user to retrieve. + :param kw: Keyword arguments for the other LDAP attributes. + """ + assert 'uid' not in kw + assert 'dn' not in kw + ldap = self.api.Backend.ldap + dn = ldap.find_entry_dn("uid", uid) + return ldap.update(dn, **kw) + + def output_for_cli(self, textui, result, uid, **options): + """ + Output result of this command to command line interface. + """ + textui.print_name(self.name) + textui.print_entry(result) + textui.print_dashed('Updated user "%s"' % result['uid']) + +api.register(user_mod) + + +class user_find(crud.Find): + 'Search the users.' + takes_options = ( + Flag('all', doc='Retrieve all user attributes'), + ) + def execute(self, term, **kw): + ldap = self.api.Backend.ldap + + # Pull the list of searchable attributes out of the configuration. + config = ldap.get_ipa_config() + search_fields_conf_str = config.get('ipausersearchfields') + search_fields = search_fields_conf_str.split(",") + + search_kw = {} + for s in search_fields: + search_kw[s] = term + + object_type = ldap.get_object_type("uid") + if object_type and not kw.get('objectclass'): + search_kw['objectclass'] = object_type + if kw.get('all', False): + search_kw['attributes'] = ['*'] + else: + search_kw['attributes'] = default_attributes + return ldap.search(**search_kw) + + def output_for_cli(self, textui, result, uid, **options): + counter = result[0] + users = result[1:] + if counter == 0 or len(users) == 0: + textui.print_plain("No entries found") + return + if len(users) == 1: + textui.print_entry(users[0]) + return + textui.print_name(self.name) + for u in users: + gn = u.get('givenname', '') + sn= u.get('sn', '') + textui.print_plain('%s %s:' % (gn, sn)) + textui.print_entry(u) + textui.print_plain('') + if counter == -1: + textui.print_plain('These results are truncated.') + textui.print_plain('Please refine your search and try again.') + textui.print_count(users, '%d users matched') + +api.register(user_find) + + +class user_show(crud.Get): + 'Examine an existing user.' + takes_options = ( + Flag('all', doc='Retrieve all user attributes'), + ) + def execute(self, uid, **kw): + """ + Execute the user-show operation. + + The dn should not be passed as a keyword argument as it is constructed + by this method. + + Returns the entry + + :param uid: The login name of the user to retrieve. + :param kw: "all" set to True = return all attributes + """ + ldap = self.api.Backend.ldap + dn = ldap.find_entry_dn("uid", uid) + # FIXME: should kw contain the list of attributes to display? + if kw.get('all', False): + return ldap.retrieve(dn) + else: + return ldap.retrieve(dn, default_attributes) + + def output_for_cli(self, textui, result, uid, **options): + if result: + display_user(result) + +api.register(user_show) + +class user_lock(Command): + 'Lock a user account.' + + takes_args = ( + Str('uid', primary_key=True), + ) + + def execute(self, uid, **kw): + ldap = self.api.Backend.ldap + dn = ldap.find_entry_dn("uid", uid) + return ldap.mark_entry_inactive(dn) + + def output_for_cli(self, textui, result, uid): + if result: + textui.print_plain('Locked user "%s"' % uid) + +api.register(user_lock) + + +class user_unlock(Command): + 'Unlock a user account.' + + takes_args = ( + Str('uid', primary_key=True), + ) + + def execute(self, uid, **kw): + ldap = self.api.Backend.ldap + dn = ldap.find_entry_dn("uid", uid) + return ldap.mark_entry_active(dn) + + def output_for_cli(self, textui, result, uid): + if result: + textui.print_plain('Unlocked user "%s"' % uid) + +api.register(user_unlock) |