shelldap-1.3.1/0000775000373600017500000000000012475655447012661 5ustar mahlonmahlonshelldap-1.3.1/ChangeLog0000664000373600017500000003122712475655447014440 0ustar mahlonmahlon2015-03-04 Mahlon E. Smith * .hgsigs: Added signature for changeset ac3c6d1057d5 [377bd38ab38c] [tip] 2015-03-04 Mahlon E. Smith * .hgtags: Added tag v1.3.1 for changeset 7b7810fee305 [ac3c6d1057d5] * shelldap: Bump version and copyright. [7b7810fee305] [v1.3.1] 2014-12-16 Mahlon E. Smith * shelldap: Use the system tempdir instead of hardcoding /tmp. [589332cac30b] 2014-12-04 Mahlon E. Smith * .hgsigs: Added signature for changeset ceb8bd75e05b [39012216a450] * .hgtags: Added tag v1.3.0 for changeset b3b840a4b56c [ceb8bd75e05b] * shelldap: Minor style cleanups, version bump. [b3b840a4b56c] [v1.3.0] 2014-12-03 Dennis Kaarsemaker * shelldap: Add a 'less' command, that does the same as cat, but uses a pager. To avoid code duplication, refactor run_cat to be a thin wrapper around a common function called by both run_cat and run_less. [39e9f802eb40] 2014-08-11 Mahlon E. Smith * .hgtags, shelldap: Branch merge. [b5adcd83b152] * .hgsigs: Added signature for changeset b220dc774937 [feb78b7417ee] * .hgtags: Added tag 1.2.0 for changeset 1a480ba231b6 [b220dc774937] 2014-08-11 Dennis Kaarsemaker * CONTRIBUTORS, shelldap: Explicitly disable wrapping when writing to file, leaving it up to the user's editor. No reason to have shelldap and editor wrap battles. Dennis Kaarsemaker [1a480ba231b6] [1.2.0] 2014-08-11 Mahlon E. Smith * CONTRIBUTORS, shelldap: Add 'rm' for a fully qualified DN, instead of only working with RDN. Reported by Lars Tauber . [86e3374a40a3] 2014-06-23 Mahlon E. Smith * shelldap: Fix the pod so it can build without complaint under perl 5.20, bump to v1.1.1. Patch from Kurt Jaeger . [5a0c99ca0c0d] 2014-06-21 Mahlon E. Smith * .hgtags: Added tag 1.1.0 for changeset e1728adb2561 [d8387a513f2c] * shelldap: Bump version. [e1728adb2561] [1.1.0] 2013-12-03 Mahlon E. Smith * CONTRIBUTORS: Add CONTRIBUTORS file. [f1ca808f165e] * shelldap: Add quick documentation blurb for SASL mechanisms. Make SASL dependency optional. [ed8253b3105a] * shelldap: Add simple SASL support. Patch from Michael Raitza . [e3bd30b95695] * shelldap: Fix the DN regexp to include dashes. Patch from Mike Hix . [f90f7ff0b146] 2013-05-15 Mahlon E. Smith * .hgtags: Added tag 1.0.2 for changeset 94b64bbf93cf [5b122351067c] * shelldap: Automatically use ldif syntax highlighting for editors that understand LDIF. (rickh_shelldap@printstring.com) [94b64bbf93cf] [1.0.2] 2013-05-03 Mahlon E. Smith * shelldap: Catch a case where the LDAP object is defined, but in a state that schema/root_dse are not obtainable. Add the connected server to 'id/whoami' output. [85cc85d0c1b1] 2013-04-26 Mahlon E. Smith * shelldap: Fix another LCS edge case that rev #0cc20d93ff50 introduced. [32e313d5d2d2] 2013-04-18 Mahlon E. Smith * .hgtags: Added tag 1.0.1 for changeset 0cc20d93ff50 [d7392bebb86c] 2013-04-18 Mahlon E. Smith * shelldap: Fix for edge case Diff::LCS traversals. Also ensure re-edit state is cleared in between attempts. [0cc20d93ff50] [1.0.1] 2013-03-19 Mahlon E. Smith * .hgsigs: Added signature for changeset 5de7014b0e60 [ae62c24653ef] * .hgtags: Added tag 1.0.0 for changeset 27bbe75233a3 [5de7014b0e60] * shelldap: Add the "inspect" command, which provides some quick reference for server schema objectClasses and attributes. [27bbe75233a3] [1.0.0] 2013-03-15 Mahlon E. Smith * shelldap: Numerous changes: - Add a command line option (-f) to specify an alternate configuration file. - Whitespace and comment cleanup. - Allow setting the $editor from the config file. - Break out the fetching of valid must/may attributes for an object class into a separate function - Offer to re-enter the editor if there is an error during create or edit, so changes aren't lost. Thanks to Alexander Perlis for the suggestion. - Wrap the passwd command with connection retry. - Change the version number to reflect semantic versioning (http://semver.org), in preparation of the 1.0.0 release. [21ba5eb5c2fc] * shelldap: Alter the default wrap width for LDIF to expand to the terminal size, with an optional rc file override. [57df728cdb77] * shelldap: More robust path for connection retries. Show optional, unused attributes as comments in the editor. [fe27dfe5179e] 2013-01-13 Mahlon E. Smith * Makefile: Fix the gmake variable that snags the current version number. [bf9d6fa1b1d4] 2013-01-10 Mahlon E. Smith * shelldap: Fix the uninitialized $path value errors I erroneously introduced on 'cd' without an argument. [f0616455056d] 2013-01-08 Mahlon E. Smith * .hgtags: Added tag 0.7 for changeset 4e77e8e5d467 [173de72687b2] * shelldap: Bump to v0.7. [4e77e8e5d467] [0.7] * shelldap: Attempt to retry the operation on failure. Less-than-optimal behavior reported by Alexander Perlis . [b8836c9018fb] 2012-11-27 Mahlon E. Smith * shelldap: Add a flag to force a password prompt, so you can override credentials from your cached shelldap.rc. [b8c6d4e8f828] 2012-10-10 Salvatore Bonaccorso * shelldap: Take only second argument for run_{cd,edit,mkdir} Make the behaviour of cd, edit and mkdir similar to cat and delete/rm and fail if some RDN's in the argument contain spaces without beeing quoted. --- shelldap | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) [3e7c107f8b93] 2011-09-12 Mahlon E. Smith * shelldap: Fix bug introduced in rev:a3a710f720dd with passwd arguments. [c6a3abc56c74] 2011-09-06 Mahlon E. Smith * .hgtags: Added tag 0.5 for changeset 12f279ef4f9d [bc105b22eb0f] * shelldap: Backout the additional objectClasses patch for mkdir: same behavior can be acheived with 'touch', less complex to leave it as is. [12f279ef4f9d] [0.5] * shelldap: Add a --version flag. [7a8855e7cfb8] * shelldap: Small documentation fixes, add better verbosity when saving connection cache data. [2e78218b8045] * shelldap: Make sure the hasSubordinates attribute is defined before checking its value. [3e5572aeee55] * shelldap: fix 'ls -R' output, minor style cleanup [40c3719c87d4] * shelldap: Repair broken path behavior, remove unneeded #path_to_dn 'relative' flag. [057fefab56b0] 2011-03-06 Peter Marschall * shelldap: [PATCH 19/19] remove rdn_to_dn() after its last users are gone From 892013debac0aef9937ecfbf2c8aab72c88e07cc Mon Sep 17 00:00:00 2001 Signed-off-by: Peter Marschall --- shelldap | 21 --------------------- 1 files changed, 0 insertions(+), 21 deletions(-) [95dbffcc757b] * shelldap: run_cat: convert to using path_to_dn() run_edit: convert to using path_to_dn() run_copy & run_move: convert to using path_to_dn() run_grep: convert to using path_to_dn() run_passwd: convert to using path_to_dn() [a3a710f720dd] * shelldap: add method path_to_dn() to convert a given "path" to a DN path_to_dn() replaces the occurrences of '~', '.' and '..' in a path given and returns a DN. However, it does not check whether the DN is valid. Especially: - on return it is not guaranteed that the DN exists - on return the first part does not need to be a valid RDN [e4b4b0968107] * shelldap: mkdir: support more objectclasses Depending on the naming attribute given, support the objectclasses 'country' and 'organization' in addition to the default 'organizationalUnit'. [bd95c3aea253] 2011-03-05 Peter Marschall * shelldap: make_filter: cope with filters that are already parenthesized Treat filter elements correctly that may be more complex filters themselves; e.g. '(&(sn=Doe)(givenname=John))' [d42bd1b087a1] * shelldap: run_list: new argument syntax: [] [] [] From 232fbd24ff43c9c0d0691cf0e1b51a82ef099489 Mon Sep 17 00:00:00 2001 Make run_list work with a properly defined argument syntax: - start with (optional) options: -R -l - continue with filter ['(objectclass=*)' as fallback if none given] - end with attributes (also optional) Add method is_valid_filter() to check whether a strig is a legal LDAP filter. [7d170d1bc17b] * shelldap: fix attribute lists for LDAP queries LDAP does not know of an attribute named 'dn'. To get only the DN in a search, the attriibute list to use is '1.1'. On all other cases, the DN of the entries found is automaticlly part of the result set too. [68318d115f6c] * shelldap: remove now unused parent_dn() method [77fd303f1a28] * shelldap: cd: flexible treatment of repeated '..', even as prefix Treat '..' as any shell does: - while the path given starts with '..', strip away the first element of the current base DN - use ',' as separator - if anything remains in thep ath given, prepend it to the stripped down baseDN - use the result as the new base DN [3a8ae9117981] 2011-09-06 Mahlon E. Smith * shelldap: small style cleanup [2ab2df609cc7] 2011-03-05 Peter Marschall * shelldap: base(): make more secure, allow '' as DN Only accept DNs as arguments to base that are legal DNs. Convert DN given into canonical form. [8c212bdb221b] * shelldap: slight cleanup: make more clear, it's an array [cf8013cbfb58] * shelldap: use sane way to get a default basedn: RootDSe's namingContexts [d956658803b8] 2011-09-06 Mahlon E. Smith * shelldap: Add documentation for the additional short flags. [18e71da965ff] 2011-03-05 Peter Marschall * shelldap: accept short option names for some options Accept short name equivalents like in ldap... commands for 'server', 'basedn' and 'binddn'. [db47ba64ebda] * shelldap: simplify over-complex call of N:L:E->get_value() @{ Net::LDAP::Entry->get_value(..., asref => 1) } is equivalent to Net::LDAP::Entry->get_value(...) [669085d93aa3] * shelldap: use symbolic LDAP error codes instead of numbers [a2e3faa3d2fc] 2011-09-06 Mahlon E. Smith * shelldap: Exit with a nicer error message if IO::Socket::SSL isn't installed, but the user is requesting SSL/TLS. (this is normally required by Net::LDAP.) [f6157d378459] 2011-03-22 Giacomo Tenaglia * shelldap: Allow '-' on RDN name when copying [b8fae8fb7942] 2011-02-21 Mahlon E. Smith * Makefile: Add a quick Makefile to automate future release tarballs. [d7975e514b2a] 2011-02-17 Mahlon E. Smith * shelldap: Bump to version 0.4. [d703cba056e3] * .hgtags: Added tag 0.4 for changeset 664bbe3dcd44 [ba2121c095af] * shelldap: Follow regular man page conventions. Patch from Salvatore Bonaccorso . [664bbe3dcd44] [0.4] * shelldap: Minor cleanup. [cb5e528f7ff2] * shelldap: Improve performance for cd/ls for containers with a large number of entries. Patch from Yann Cezard . [38aaae38427a] * .hgtags: Tagging for release 0.3. [44ab209b2a3b] * shelldap: Update documentation, now that multiline edits work. Minor other cleanups. Bump version. [46dfe9d6f368] [0.3] * shelldap: Combine multiple lines into a single one before displaying LDIF. Patch by Gertjan Halkes . [78b2a48e07db] 2010-07-15 Mahlon E. Smith * shelldap: Append a trailing slash to entries that contain other entries. Thanks to Jonathan Rozes for the idea, and Michael Granger for telling me about the hasSubordinates attribute (that he was already using to do exactly this in the Treequel-based ruby shell, heh!) [5a65bc849363] 2010-05-17 Mahlon E. Smith * shelldap: Add options to support ssl key verification when connecting with TLS. Many thanks to Josef Wells ! Small whitespace cleanup. Display correct configuration file in error message, if a YAML parse error occurred. [0f815f3daaf7] 2009-07-24 convert-repo * .hgtags: update tags [35fec0d1acb8] 2008-12-04 mahlon * shelldap: Bumping to 0.2 release. [66ab8df0b6c8] [0.2] * shelldap: Restructure for tags/branches. [f7990a76e217] shelldap-1.3.1/README0000664000373600017500000002373412475655447013552 0ustar mahlonmahlonNAME Shelldap - A program for interacting with an LDAP server via a shell-like interface DESCRIPTION Shelldap /LDAP::Shell is a program for interacting with an LDAP server via a shell-like interface. This is not meant to be an exhaustive LDAP editing and browsing interface, but rather an intuitive shell for performing basic LDAP tasks quickly and with minimal effort. SYNPOSIS shelldap --server example.net [--help] FEATURES - Upon successful authenticated binding, credential information is auto-cached to ~/.shelldap.rc -- future loads require no command line flags. - Custom 'description maps' for entry listings. (See the 'list' command.) - History and autocomplete via readline, if installed. - Automatic reconnection attempts if the connection is lost with the LDAP server. - Basic schema introspection for quick reference. - It feels like a semi-crippled shell, making LDAP browsing and editing at least halfway pleasurable. OPTIONS All command line options follow getopts long conventions. shelldap --server example.net --basedn dc=your,o=company You may also optionally create a ~/.shelldap.rc file with command line defaults. This file should be valid YAML. (This file is generated automatically on a successful bind auth.) Example: server: ldap.example.net binddn: cn=Manager,dc=your,o=company bindpass: xxxxxxxxx basedn: dc=your,o=company tls: yes tls_cacert: /etc/ssl/certs/cacert.pem tls_cert: ~/.ssl/client.cert.pem tls_key: ~/.ssl/private/client.key.pem configfile Optional. Use an alternate configuration file, instead of the default ~/.shelldap.rc. --configfile /tmp/alternate-config.yml -f /tmp/alternate-config.yml This config file overrides values found in the default config, so you can easily have separate config files for connecting to your cn=monitor or cn=log overlays (for example.) server Required. The LDAP server to connect to. This can be a hostname, IP address, or a URI. --server ldaps://ldap.example.net -H ldaps://ldap.example.net binddn The full dn of a user to authenticate as. If not specified, defaults to an anonymous bind. You will be prompted for a password. --binddn cn=Manager,dc=your,o=company -D cn=Manager,dc=your,o=company basedn The directory 'root' of your LDAP server. If omitted, shelldap will try and ask the server for a sane default. --basedn dc=your,o=company -b dc=your,o=company promptpass Force password prompting. Useful to temporarily override cached credentials. sasl A space separated list of SASL mechanisms. Requires the Authen::SASL module. --sasl "PLAIN CRAM-MD5 GSSAPI" tls Enables TLS over what would normally be an insecure connection. Requires server side support. tls_cacert Specify CA Certificate to trust. --tls_cacert /etc/ssl/certs/cacert.pem tls_cert The TLS client certificate. --tls_cert ~/.ssl/client.cert.pem tls_key The TLS client key. Not specifying a key will connect via TLS without key verification. --tls_key ~/.ssl/private/client.key.pem cacheage Set the time to cache directory lookups in seconds. By default, directory lookups are cached for 300 seconds, to speed autocomplete up when changing between different basedns. Modifications to the directory automatically reset the cache. Directory listings are not cached. (This is just used for autocomplete.) Set it to 0 to disable caching completely. timeout Set the maximum time an LDAP operation can take before it is cancelled. debug Print extra operational info out, and backtrace on fatal error. version Display the version number. SHELL COMMANDS cat Display an LDIF dump of an entry. Globbing is supported. Specify either the full dn, or an rdn. For most commands, rdns are local to the current search base. ('cwd', as translated to shell speak.) You may additionally add a list of attributes to display. Use '+' for server side attributes. cat uid=mahlon cat ou=* cat uid=mahlon,ou=People,dc=example,o=company cat uid=mahlon + userPassword less Like cat, but uses the configured pager to display output. cd Change directory. Translated to LDAP, this changes the current basedn. All commands after a 'cd' operate within the new basedn. cd change to 'home' basedn cd ~ change to the binddn, or basedn if anonymously bound cd - change to previous node cd ou=People change to explicit path below current node cd .. change to parent node cd ../../ou=Groups change to node ou=Groups, which is a sibling to the current node's grandparent Since LDAP doesn't actually limit what can be a container object, you can actually cd into any entry. Many commands then work on '.', meaning "wherever I currently am." cd uid=mahlon cat . clear Clear the screen. copy Copy an entry to a different dn path. All copies are relative to the current basedn, unless a full dn is specified. All attributes are copied, then an LDAP moddn() is performed. copy uid=mahlon uid=bob copy uid=mahlon ou=Others,dc=example,o=company copy uid=mahlon,ou=People,dc=example,o=company uid=mahlon,ou=Others,dc=example,o=company aliased to: cp create Create an entry from scratch. Arguments are space separated objectClass names. Possible objectClasses are derived automatically from the server, and will tab-complete. After the classes are specified, an editor will launch. Required attributes are listed first, then optional attributes. Optionals are commented out. After the editor exits, the resulting LDIF is validated and added to the LDAP directory. create top person organizationalPerson inetOrgPerson posixAccount aliased to: touch delete Remove an entry from the directory. Globbing is supported. All deletes are sanity-prompted. The -v flag prints the entries out for review before delete. delete uid=mahlon delete uid=ma* rm -v uid=mahlon,ou=People,dc=example,o=company l=office aliased to: rm edit Edit an entry in an external editor. After the editor exits, the resulting LDIF is sanity checked, and changes are written to the LDAP directory. edit uid=mahlon aliased to: vi env Show values for various runtime variables. grep Search for arbitrary LDAP filters, and return matching dn results. The search string must be a valid LDAP filter. grep uid=mahlon grep uid=mahlon ou=People grep -r (&(uid=mahlon)(objectClass=*)) aliased to: search inspect View schema information about a given entry, or a list of arbitrary objectClasses, along with the most common flags for the objectClass attributes. inspect uid=mahlon inspect posixAccount organizationalUnit inspect _schema The output is a list of found objectClasses, their schema heirarchy (up to 'top'), whether or not they are a structural class, and then a merged list of all valid attributes for the given objectClasses. Attributes are marked as either required or optional, and whether they allow multiple values or not. If you ask for the special "_schema" object, the raw server schema is dumped to screen. list List entries for the current basedn. Globbing is supported. aliased to: ls ls -l ls -lR uid=mahlon list uid=m* In 'long' mode, descriptions are listed as well, if they exist. There are some default 'long listing' mappings for common objectClass types. You can additionally specify your own mappings in your .shelldap.rc, like so: ... descmaps: objectClass: attributename posixAccount: gecos posixGroup: gidNumber ipHost: ipHostNumber mkdir Creates a new 'organizationalUnit' entry. mkdir containername mkdir ou=whatever move Move an entry to a different dn path. Usage is identical to copy. aliased to: mv passwd If supported server side, change the password for a specified entry. The entry must have a 'userPassword' attribute. passwd uid=mahlon pwd Print the 'working directory' - aka, the current ldap basedn. setenv Modify various runtime variables normally set from the command line. setenv debug 1 export debug=1 whoami Show current auth credentials. Unless you specified a binddn, this will just show an anonymous bind. aliased to: id TODO Referral support. Currently, if you try to write to a replicant slave, you'll just get a referral. It would be nice if shelldap automatically tried to follow it. For now, it only makes sense to connect to a master if you plan on doing any writes. BUGS / LIMITATIONS There is no support for editing binary data. If you need to edit base64 stuff, just feed it to the regular ldapmodify/ldapadd/etc tools. AUTHOR Mahlon E. Smith shelldap-1.3.1/shelldap0000775000373600017500000015053312475655447014412 0ustar mahlonmahlon#!/usr/bin/env perl # vim: set nosta noet ts=4 sw=4: # # Copyright (c) 2006-2015, Mahlon E. Smith # All rights reserved. # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # * Neither the name of Mahlon E. Smith nor the names of his # contributors may be used to endorse or promote products derived # from this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND ANY # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. =head1 NAME Shelldap - A program for interacting with an LDAP server via a shell-like interface =head1 DESCRIPTION Shelldap /LDAP::Shell is a program for interacting with an LDAP server via a shell-like interface. This is not meant to be an exhaustive LDAP editing and browsing interface, but rather an intuitive shell for performing basic LDAP tasks quickly and with minimal effort. =head1 SYNPOSIS shelldap --server example.net [--help] =head1 FEATURES - Upon successful authenticated binding, credential information is auto-cached to ~/.shelldap.rc -- future loads require no command line flags. - Custom 'description maps' for entry listings. (See the 'list' command.) - History and autocomplete via readline, if installed. - Automatic reconnection attempts if the connection is lost with the LDAP server. - Basic schema introspection for quick reference. - It feels like a semi-crippled shell, making LDAP browsing and editing at least halfway pleasurable. =head1 OPTIONS All command line options follow getopts long conventions. shelldap --server example.net --basedn dc=your,o=company You may also optionally create a ~/.shelldap.rc file with command line defaults. This file should be valid YAML. (This file is generated automatically on a successful bind auth.) Example: server: ldap.example.net binddn: cn=Manager,dc=your,o=company bindpass: xxxxxxxxx basedn: dc=your,o=company tls: yes tls_cacert: /etc/ssl/certs/cacert.pem tls_cert: ~/.ssl/client.cert.pem tls_key: ~/.ssl/private/client.key.pem =over 4 =item B Optional. Use an alternate configuration file, instead of the default ~/.shelldap.rc. --configfile /tmp/alternate-config.yml -f /tmp/alternate-config.yml This config file overrides values found in the default config, so you can easily have separate config files for connecting to your cn=monitor or cn=log overlays (for example.) =back =over 4 =item B Required. The LDAP server to connect to. This can be a hostname, IP address, or a URI. --server ldaps://ldap.example.net -H ldaps://ldap.example.net =back =over 4 =item B The full dn of a user to authenticate as. If not specified, defaults to an anonymous bind. You will be prompted for a password. --binddn cn=Manager,dc=your,o=company -D cn=Manager,dc=your,o=company =back =over 4 =item B The directory 'root' of your LDAP server. If omitted, shelldap will try and ask the server for a sane default. --basedn dc=your,o=company -b dc=your,o=company =back =over 4 =item B Force password prompting. Useful to temporarily override cached credentials. =back =over 4 =item B A space separated list of SASL mechanisms. Requires the Authen::SASL module. --sasl "PLAIN CRAM-MD5 GSSAPI" =back =over 4 =item B Enables TLS over what would normally be an insecure connection. Requires server side support. =item B Specify CA Certificate to trust. --tls_cacert /etc/ssl/certs/cacert.pem =item B The TLS client certificate. --tls_cert ~/.ssl/client.cert.pem =item B The TLS client key. Not specifying a key will connect via TLS without key verification. --tls_key ~/.ssl/private/client.key.pem =back =over 4 =item B Set the time to cache directory lookups in seconds. By default, directory lookups are cached for 300 seconds, to speed autocomplete up when changing between different basedns. Modifications to the directory automatically reset the cache. Directory listings are not cached. (This is just used for autocomplete.) Set it to 0 to disable caching completely. =back =over 4 =item B Set the maximum time an LDAP operation can take before it is cancelled. =back =over 4 =item B Print extra operational info out, and backtrace on fatal error. =back =over 4 =item B Display the version number. =back =head1 SHELL COMMANDS =over 4 =item B< cat> Display an LDIF dump of an entry. Globbing is supported. Specify either the full dn, or an rdn. For most commands, rdns are local to the current search base. ('cwd', as translated to shell speak.) You may additionally add a list of attributes to display. Use '+' for server side attributes. cat uid=mahlon cat ou=* cat uid=mahlon,ou=People,dc=example,o=company cat uid=mahlon + userPassword =item B< less> Like cat, but uses the configured pager to display output. =item B< cd> Change directory. Translated to LDAP, this changes the current basedn. All commands after a 'cd' operate within the new basedn. cd change to 'home' basedn cd ~ change to the binddn, or basedn if anonymously bound cd - change to previous node cd ou=People change to explicit path below current node cd .. change to parent node cd ../../ou=Groups change to node ou=Groups, which is a sibling to the current node's grandparent Since LDAP doesn't actually limit what can be a container object, you can actually cd into any entry. Many commands then work on '.', meaning "wherever I currently am." cd uid=mahlon cat . =item B Clear the screen. =item B Copy an entry to a different dn path. All copies are relative to the current basedn, unless a full dn is specified. All attributes are copied, then an LDAP moddn() is performed. copy uid=mahlon uid=bob copy uid=mahlon ou=Others,dc=example,o=company copy uid=mahlon,ou=People,dc=example,o=company uid=mahlon,ou=Others,dc=example,o=company aliased to: cp =item B Create an entry from scratch. Arguments are space separated objectClass names. Possible objectClasses are derived automatically from the server, and will tab-complete. After the classes are specified, an editor will launch. Required attributes are listed first, then optional attributes. Optionals are commented out. After the editor exits, the resulting LDIF is validated and added to the LDAP directory. create top person organizationalPerson inetOrgPerson posixAccount aliased to: touch =item B Remove an entry from the directory. Globbing is supported. All deletes are sanity-prompted. The -v flag prints the entries out for review before delete. delete uid=mahlon delete uid=ma* rm -v uid=mahlon,ou=People,dc=example,o=company l=office aliased to: rm =item B Edit an entry in an external editor. After the editor exits, the resulting LDIF is sanity checked, and changes are written to the LDAP directory. edit uid=mahlon aliased to: vi =item B Show values for various runtime variables. =item B Search for arbitrary LDAP filters, and return matching dn results. The search string must be a valid LDAP filter. grep uid=mahlon grep uid=mahlon ou=People grep -r (&(uid=mahlon)(objectClass=*)) aliased to: search =item B View schema information about a given entry, or a list of arbitrary objectClasses, along with the most common flags for the objectClass attributes. inspect uid=mahlon inspect posixAccount organizationalUnit inspect _schema The output is a list of found objectClasses, their schema heirarchy (up to 'top'), whether or not they are a structural class, and then a merged list of all valid attributes for the given objectClasses. Attributes are marked as either required or optional, and whether they allow multiple values or not. If you ask for the special "_schema" object, the raw server schema is dumped to screen. =item B List entries for the current basedn. Globbing is supported. aliased to: ls ls -l ls -lR uid=mahlon list uid=m* In 'long' mode, descriptions are listed as well, if they exist. There are some default 'long listing' mappings for common objectClass types. You can additionally specify your own mappings in your .shelldap.rc, like so: ... descmaps: objectClass: attributename posixAccount: gecos posixGroup: gidNumber ipHost: ipHostNumber =item B Creates a new 'organizationalUnit' entry. mkdir containername mkdir ou=whatever =item B Move an entry to a different dn path. Usage is identical to B. aliased to: mv =item B If supported server side, change the password for a specified entry. The entry must have a 'userPassword' attribute. passwd uid=mahlon =item B< pwd> Print the 'working directory' - aka, the current ldap basedn. =item B Modify various runtime variables normally set from the command line. setenv debug 1 export debug=1 =item B Show current auth credentials. Unless you specified a binddn, this will just show an anonymous bind. aliased to: id =back =head1 TODO Referral support. Currently, if you try to write to a replicant slave, you'll just get a referral. It would be nice if shelldap automatically tried to follow it. For now, it only makes sense to connect to a master if you plan on doing any writes. =head1 BUGS / LIMITATIONS There is no support for editing binary data. If you need to edit base64 stuff, just feed it to the regular ldapmodify/ldapadd/etc tools. =head1 AUTHOR Mahlon E. Smith =cut package LDAP::Shell; use strict; use warnings; use Term::ReadKey; use Term::Shell; use Digest::MD5; use Net::LDAP qw/ LDAP_SUCCESS LDAP_SERVER_DOWN LDAP_OPERATIONS_ERROR LDAP_TIMELIMIT_EXCEEDED LDAP_BUSY LDAP_UNAVAILABLE LDAP_OTHER LDAP_TIMEOUT LDAP_NO_MEMORY LDAP_CONNECT_ERROR /; use Net::LDAP::Util qw/ canonical_dn ldap_explode_dn /; use Net::LDAP::LDIF; use Data::Dumper; use File::Temp; use Algorithm::Diff; use Carp 'confess'; use base 'Term::Shell'; require Net::LDAP::Extension::SetPassword; my $conf = $main::conf; # make 'die' backtrace in debug mode $SIG{'__DIE__'} = \&Carp::confess if $conf->{'debug'}; ######################################################################## ### U T I L I T Y F U N C T I O N S ######################################################################## ### Initial shell behaviors. ### sub init { my $self = shift; $self->{'API'}->{'match_uniq'} = 0; $self->{'editor'} = $conf->{'editor'} || $ENV{'EDITOR'} || 'vi'; $self->{'pager'} = $conf->{'pager'} || $ENV{'PAGER'} || 'less'; $self->{'env'} = [ qw/ debug cacheage timeout / ]; # let autocomplete work with the '=' character my $term = $self->term(); $term->Attribs->{'basic_word_break_characters'} =~ s/=//m; $term->Attribs->{'completer_word_break_characters'} =~ s/=//m; # read in history eval { $term->history_truncate_file("$ENV{'HOME'}/.shelldap_history", 50); $term->ReadHistory("$ENV{'HOME'}/.shelldap_history"); }; # gather metadata from the LDAP server $self->{'root_dse'} = $self->ldap->root_dse() or die "Unable to retrieve LDAP server information. (Doublecheck connection arguments.)\n"; $self->{'schema'} = $self->ldap->schema(); # get an initial list of all objectClasses $self->{'objectclasses'} = []; foreach my $o ( $self->{'schema'}->all_objectclasses() ) { push @{ $self->{'objectclasses'} }, $o->{'name'}; } if ( $conf->{'debug'} ) { my @versions = $self->{'root_dse'}->get_value('supportedLDAPVersion'); print "Connected to $conf->{'server'}\n"; print "Supported LDAP version: ", ( join ', ', @versions ), "\n"; print "Cipher in use: ", $self->ldap()->cipher(), "\n"; } # try an initial search and bail early if it doesn't work. (bad baseDN?) my $s = $self->search(); die "LDAP baseDN error: ", $s->{'message'}, "\n" if $s->{'code'}; # okay, now do an initial population of 'cwd' for autocomplete. $self->update_entries(); # whew, okay. Update prompt, wait for input! $self->update_prompt(); return; } ### Return an LDAP connection handle, creating it if necessary. ### sub ldap { my $self = shift; my $rv; # use cached connection object if it exists return $self->{'ldap'} if $self->{'ldap'}; # fill in potentially missing info die "No server specified.\n" unless $conf->{'server'}; # Emit a nicer error message if IO::Socket::SSL is # not installed and Net::LDAP decides it is required. # if ( $conf->{'tls'} || $conf->{'server'} =~ m|ldaps://| ) { eval 'use IO::Socket::SSL'; die qq{IO::Socket::SSL not installed, but is required for SSL or TLS connections. You may try connecting insecurely, or install the module and try again.\n} if $@; } # Prompt for a password after disabling local echo. # if ( ($conf->{'binddn'} && ! $conf->{'bindpass'}) || $conf->{'promptpass'} ) { print "Bind password: "; Term::ReadKey::ReadMode 2; chomp( $conf->{'bindpass'} = ); Term::ReadKey::ReadMode 0; print "\n"; } # make the connection my $ldap = Net::LDAP->new( $conf->{'server'} ) or die "Unable to connect to LDAP server '$conf->{'server'}': $!\n"; # secure connection options # if ( $conf->{'tls'} ) { if ( $conf->{'tls_key'} ) { $ldap->start_tls( verify => 'require', cafile => $conf->{'tls_cacert'}, clientcert => $conf->{'tls_cert'}, clientkey => $conf->{'tls_key'}, keydecrypt => sub { print "Key Passphrase: "; Term::ReadKey::ReadMode 2; chomp( my $secret = ); Term::ReadKey::ReadMode 0; print "\n"; return $secret; }); } else { $ldap->start_tls( verify => 'none' ); } } eval 'use Authen::SASL'; my ( $sasl, $sasl_conn ); my $has_sasl = ! defined( $@ ); if ( $has_sasl && $conf->{'sasl'} ) { my $serv = $conf->{'server'}; $serv =~ s!^ldap[si]?://!!; $sasl = Authen::SASL->new( mechanism => $conf->{'sasl'} ); $sasl_conn = $sasl->client_new('ldap', $serv); } # bind with sasl # if ( $has_sasl && $sasl_conn ) { $rv = $ldap->bind( $conf->{'binddn'}, password => $conf->{'bindpass'}, sasl => $sasl_conn ); } # simple bind as an authenticated dn # elsif ( $conf->{'binddn'} ) { $rv = $ldap->bind( $conf->{'binddn'}, password => $conf->{'bindpass'} ); } # bind anonymously # else { $rv = $ldap->bind(sasl => $sasl_conn); } my $err = $rv->error(); $self->debug( "Bind as " . ( $conf->{'binddn'} ? $conf->{'binddn'} : 'anonymous' ) . " to " . $conf->{'server'} . ": $err\n" ); if ( $rv->code() ) { $err .= " (try the --tls flag?)" if $err =~ /confidentiality required/i; $err .= "\n" . $sasl->error() if $sasl; die "LDAP bind error: $err\n"; } # Offer to cache authentication info. # If we enter this conditional, we have successfully authed with the server # (non anonymous), and we haven't cached anything in the past. # if ( $conf->{'binddn'} && ! -e $conf->{'configfile'} ) { print "Would you like to cache your connection information? [Yn]: "; chomp( my $response = ); unless ( $response =~ /^n/i ) { YAML::Syck::DumpFile( $conf->{'configfile'}, $conf ); chmod 0600, $conf->{'configfile'}; print "Connection info cached to $conf->{'configfile'}.\n"; } } $self->{'ldap'} = $ldap; return $ldap; } ### Return a new LDIF object, suitable for populating with ### a Net::LDAP::Entry. ### sub ldif { my $self = shift; my $use_temp = shift; # create tmpfile and link ldif object with it # if ( $use_temp ) { my ( undef, $fname ) = File::Temp::tempfile( 'shelldap_XXXXXXXX', SUFFIX => '.ldif', TMPDIR => 1, UNLINK => 1 ); $self->{'ldif'} = Net::LDAP::LDIF->new( $fname, 'w', sort => 1, wrap => 0 ); $self->{'ldif_fname'} = $fname; } # ldif -> stdout # else { $self->{'ldif'} = Net::LDAP::LDIF->new( \*STDOUT, 'w', sort => 1, wrap => $self->wrapsize ); } return $self->{'ldif'}; } ### Return an Entry object from an LDIF filename, or undef if there was an error. ### sub load_ldif { my $self = shift; my $ldif = Net::LDAP::LDIF->new( shift(), 'r' ); return unless $ldif; my $e; eval { $e = $ldif->read_entry(); }; return if $@; return $e; } ### Given a filename, return an md5 checksum. ### sub chksum { my $self = shift; my $file = shift or return; my $md5 = Digest::MD5->new(); open F, $file or die "Unable to read file: $!\n"; my $hash = $md5->addfile( *F )->hexdigest(); close F; return $hash; } ### Find and return the current terminal width. ### sub wrapsize { my $self = shift; my $wrap = $conf->{'wrap'}; eval { my $rows; my $term = Term::ReadLine->new( 1 ); ( $rows, $wrap ) = $term->get_screen_size() unless $wrap; }; $wrap ||= 78; return $wrap; } ### Used by Term::Shell to generate the prompt. ### sub prompt_str { my $self = shift; return $self->{'prompt'}; } ### Display the current working entry as the prompt, ### truncating if necessary. ### sub update_prompt { my $self = shift; my $base = $self->base(); if ( length $base > 50 ) { my $cwd_dn = $1 if $base =~ /^(.*?),/; $self->{'prompt'} = "... $cwd_dn > "; } else { my $prompt = $base; $prompt =~ s/$conf->{'basedn'}/~/; $self->{'prompt'} = "$prompt > "; } return; } ### Prompt the user to re-edit their LDIF on error. ### Returns true if the user wants to do so. ### sub prompt_edit_again { my $self = shift; print "Edit again? [Yn]: "; chomp( my $ans = ); return $ans !~ /^n/i; } ### Return the basedn of the LDAP connection, being either explicitly ### configured or determined automatically from server metadata. ### sub base { my $self = shift; $self->{'base'} ||= $conf->{'basedn'}; # try and determine base automatically from rootDSE # unless ( $self->{'base'} ) { my @namingContexts = $self->{'root_dse'}->get_value('namingContexts'); $conf->{'basedn'} = $namingContexts[0]; $self->{'base'} = $namingContexts[0]; } if ( $_[0] ) { my $base = canonical_dn( $_[0], casefold => 'none' ); $self->{'base'} = $base if $base; } return $self->{'base'}; } ### Returns true if the specified dn is valid on this LDAP server. ### sub is_valid_dn { my $self = shift; my $dn = shift or return 0; my $r = $self->search({ base => $dn }); return $r->{'code'} == LDAP_SUCCESS ? 1 : 0; } ### Emit LDIF to the terminal. ### sub display { my $self = shift; my $dn = shift; my @attrs = @{;shift}; my $use_pager = shift; unless ( $dn ) { print "No dn provided.\n"; return; } # support '.' $dn = $self->base() if $dn eq '.'; # support globbing # my $s; if ( $dn eq '*' ) { $s = $self->search({ scope => 'one', vals => 1, attrs => \@attrs }); } elsif ( $dn =~ /\*/ ) { $s = $self->search({ scope => 'one', vals => 1, filter => $dn, attrs => \@attrs }); } # absolute/relative dn # else { $dn = $self->path_to_dn( $dn ); $s = $self->search({ base => $dn, vals => 1, attrs => \@attrs }); } # emit error, if any # if ( $s->{'code'} ) { print $s->{'message'} . "\n"; return; } # display to stdout or pager # my $ldif = $self->ldif( $use_pager ); foreach my $e ( @{ $s->{'entries'} } ) { $ldif->write_entry( $e ); } if( $use_pager ) { system( $self->{'pager'}, $self->{'ldif_fname'} ); unlink $self->{'ldif_fname'}; } return; } ### Perform an LDAP search. ### ### Returns a hashref containing the return code and ### an arrayref of Net::LDAP::Entry objects. ### sub search { my $self = shift; my $opts = shift || {}; $opts->{'base'} ||= $self->base(), $opts->{'filter'} ||= '(objectClass=*)'; $opts->{'scope'} ||= 'base'; my $search = sub { return $self->ldap->search( base => $opts->{'base'}, filter => $opts->{'filter'}, scope => $opts->{'scope'}, timelimit => $conf->{'timeout'}, typesonly => ! $opts->{'vals'}, attrs => $opts->{'attrs'} || ['*'] ); }; my $s = $self->with_retry( $search ); my $rv = { code => $s->code(), message => $s->error(), entries => [] }; $rv->{'entries'} = $opts->{'scope'} eq 'base' ? [ $s->shift_entry() ] : [ $s->entries() ]; return $rv; } ### Maintain the cache of possible autocomplete values for ### the current DN. ### sub update_entries { my $self = shift; my %opts = @_; my $base = lc( $self->base() ); my $s = $opts{'search'} || $self->search({ scope => 'one', base => $base }); $self->{'cwd_entries'} = []; return if $s->{'code'}; # setup cache object $self->{'cache'} ||= {}; $self->{'cache'}->{ $base } ||= {}; $self->{'cache'}->{ $base } = {} if $opts{'clearcache'}; my $cache = $self->{'cache'}->{ $base }; my $now = time(); if ( ! exists $cache->{'entries'} or $now - $cache->{'timestamp'} > $conf->{'cacheage'} ) { $self->debug("Caching entries for $base\n"); foreach my $e ( @{ $s->{'entries'} } ) { my $dn = $e->dn(); my $rdn = $dn; $rdn =~ s/,$base//i; # remove base from display push @{ $self->{'cwd_entries'} }, $rdn; } $cache->{'timestamp'} = $now; $cache->{'entries'} = $self->{'cwd_entries'}; } else { $self->debug("Using cached lookups for $base\n"); } $self->{'cwd_entries'} = $cache->{'entries'}; return; } ### Roughly convert a given path to a DN. ### ### Additionally support: ### parent '..' ### current '.' ### last '-' ### home '~' ### ### Synopsis: $dn = $self->path_to_dn( $path ); ### sub path_to_dn { my $self = shift; my $path = shift; my %flags = @_; my $curbase = $self->base(); # support empty 'cd' or 'cd ~' going to root return $conf->{'basedn'} if ! $path || $path eq '~'; # return current base DN return $curbase if $path eq '.'; # support 'cd -' return $self->{'previous_base'} if $path eq '-'; # relative path, upwards # if ( $path =~ /^\.\./o ) { # support '..' (possibly iterated and as prefix to a DN) my @base = @{ ldap_explode_dn($curbase, casefold => 'none') }; # deal with leading .., # while ( $path =~ /^\.\./ ) { shift( @base ) if @base; $path =~ s/^\.\.//; last if $path !~ /[,\/]\s*/; $path =~ s/[,\/]\s*//; } # append the new dn to the node if one was specified: # cd ../../cn=somewhere vs # cd ../../ # my $newbase_root = canonical_dn( \@base, casefold => 'none' ); $path = $path ? $path . ',' . $newbase_root : $newbase_root; } # attach the base if it isn't already there (this takes care of # deeper relative nodes and absolutes) # else { $path = "$path," . $curbase unless $path =~ /$curbase/; } return $path; } ### Given an array ref of shell-like globs, ### create and return a Net::LDAP::Filter object. ### sub make_filter { my $self = shift; my $globs = shift or return; return unless ref $globs eq 'ARRAY'; return unless scalar @$globs; my $filter; $filter = join('', map { (/^\(.*\)$/o) ? $_ : "($_)" } @$globs); $filter = '(|' . $filter . ')' if (scalar(@$globs) > 1); $filter = Net::LDAP::Filter->new( $filter ); if ( $filter ) { $self->debug( 'Filter parsed as: ' . $filter->as_string() . "\n" ); } else { print "Error parsing filter.\n"; return; } return $filter; } ### Given an arrayref of objectClasses, pull a complete list of ### required and optional attrbutes. Returns two arrayrefs. ### sub fetch_attributes { my $self = shift; my $ocs = shift or return [], []; my ( %seen, @must_attr, @may_attr ); foreach my $oc ( sort @{$ocs} ) { # required my @must = $self->{'schema'}->must( $oc ); foreach my $attr ( sort { $a->{'name'} cmp $b->{'name'} } @must ) { next if $attr->{'name'} =~ /^objectclass$/i; next if $seen{ $attr->{'name'} }; push @must_attr, $attr->{'name'}; $seen{ $attr->{'name'} }++; } # optional my @may = $self->{'schema'}->may( $oc ); foreach my $attr ( sort { $a->{'name'} cmp $b->{'name'} } @may ) { next if $attr->{'name'} =~ /^objectclass$/i; next if $seen{ $attr->{'name'} }; push @may_attr, $attr->{'name'}; $seen{ $attr->{'name'} }++; } } return \@must_attr, \@may_attr; } ### Check whether a given string can be used directly as ### an LDAP search filter. ### ### Synopsis: $yesNo = $self->is_valid_filter($string); ### sub is_valid_filter { my $self = shift; my $filter = shift or return; return Net::LDAP::Filter->new( $filter ) ? 1 : 0; } ### Call code in subref $action, if there's any connection related errors, ### try it one additional time before giving up. This should take care of ### most server disconnects due to timeout and other generic connection ### errors, and will attempt to transparently re-establish a connection. ### sub with_retry { my $self = shift; my $action = shift; my $rv = $action->(); if ( $rv->code() == LDAP_OPERATIONS_ERROR || $rv->code() == LDAP_TIMELIMIT_EXCEEDED || $rv->code() == LDAP_BUSY || $rv->code() == LDAP_UNAVAILABLE || $rv->code() == LDAP_OTHER || $rv->code() == LDAP_SERVER_DOWN || $rv->code() == LDAP_TIMEOUT || $rv->code() == LDAP_NO_MEMORY || $rv->code() == LDAP_CONNECT_ERROR ) { $self->debug( "Error ". $rv->code() . ", retrying.\n" ); $self->{'ldap'} = undef; $rv = $action->(); } return $rv; } ### little. yellow. different. better. ### sub debug { my $self = shift; return unless $conf->{'debug'}; print "\e[33m"; print shift(); print "\e[0m"; return; } ### Autocomplete values: Returns cached children entries. ### sub autocomplete_cwd { my $self = shift; return @{ $self->{'cwd_entries'} }; } ### Autocomplete values: Returns previously set shelldap environment values. ### sub comp_setenv { my $self = shift; return @{ $self->{'env'} }; } ### Autocomplete values: Returns all objectClasses as defined ### by the LDAP server. ### sub comp_create { my $self = shift; return @{ $self->{'objectclasses'} }; } ### Autocomplete values: Returns all objectClasses as defined ### by the LDAP server, along with current children DNs. ### sub comp_inspect { my $self = shift; return ('_schema', @{ $self->{'objectclasses'} }, @{ $self->{'cwd_entries'} }); } ### Inject various autocomplete and alias routines into the symbol table. ### { no warnings; no strict 'refs'; # command, alias my %cmd_map = ( whoami => 'id', list => 'ls', grep => 'search', edit => 'vi', delete => 'rm', copy => 'cp', cat => 'read', move => 'mv', less => undef, cd => undef, passwd => undef ); # setup autocompletes foreach ( %cmd_map ) { next unless $_; my $sub = "comp_$_"; *$sub = \&autocomplete_cwd; } *comp_touch = \&comp_create; *comp_export = \&comp_setenv; # setup alias subs # # Term::Shell has an alias_* feature, but # it seems to work about 90% of the time. # that last 10% is something of a mystery. # $cmd_map{'create'} = 'touch'; foreach my $cmd ( keys %cmd_map ) { next unless defined $cmd_map{$cmd}; my $alias_sub = 'run_' . $cmd_map{$cmd}; my $real_sub = 'run_' . $cmd; *$alias_sub = \&$real_sub; } } ### Given an $arrayref, remove LDIF continuation wrapping in place, ### effectively making each entry a single line for LCS comparisons. ### sub unwrap_line { my $self = shift; my $array = shift; my $i = 1; while ( $i < scalar(@$array) ) { if ( $array->[$i] =~ /^\s/ ) { $array->[ $i - 1 ] =~ s/\n$//; $array->[ $i ] =~ s/^\s//; splice( @$array, $i - 1, 2, $array->[$i - 1] . $array->[$i] ); } else { $i++; } } } ######################################################################## ### S H E L L M E T H O D S ######################################################################## ### Don't die on a newline, just no-op. ### sub run_ { return; } ### Term::Shell hook. ### Write history for each command, print shell debug actions. ### sub precmd { my $self = shift; my ( $handler, $cmd, $args ) = @_; my $term = $self->term(); eval { $term->WriteHistory("$ENV{'HOME'}/.shelldap_history"); }; $self->debug( "$$cmd (" . ( join ' ', @$args ) . "), calling '$$handler'\n" ); return; } ### Display an entry as LDIF to the terminal. ### sub run_cat { my $self = shift; my $dn = shift; my @attrs = (@_) ? @_ : ('*'); $self->display( $dn, \@attrs, 0 ); } ### Display an entry as LDIF to the terminal with external pagination. ### sub run_less { my $self = shift; my $dn = shift; my @attrs = (@_) ? @_ : ('*'); $self->display( $dn, \@attrs, 1 ); } ### Change shelldap's idea of a current working 'directory', ### by adjusting the current default basedn for all searches. ### sub run_cd { my $self = shift; my $newbase = shift; # convert given path to a DN $newbase = $self->path_to_dn( $newbase ); unless ( $self->is_valid_dn( $newbase ) ) { print "No such object\n"; return; } # store old base $self->{'previous_base'} = $self->base(); # update new base $self->base( $newbase ); # get new 'cwd' listing my $s = $self->search({ scope => 'one', attrs => [ '1.1' ] }); if ( $s->{'code'} ) { print "$s->{'message'}\n"; return; } $self->update_entries( search => $s ); # reflect cwd change in prompt $self->update_prompt(); return; } ### Simply clear the screen. ### sub run_clear { my $self = shift; system( 'clear' ); return; } ### Fetch the source DN entry, modify it's DN data ### and write it back to the directory. ### sub run_copy { my $self = shift; my ( $s_dn, $d_dn ) = @_; unless ( $s_dn ) { print "No source DN provided.\n"; return; } unless ( $d_dn ) { print "No destination DN provided.\n"; return; } # convert given source path to DN $s_dn = $self->path_to_dn( $s_dn ); # sanity check source # my $s = $self->search({ base => $s_dn, vals => 1 }); unless ( $s->{'code'} == LDAP_SUCCESS ) { print "No such object\n"; return; } # see if we're copying the entry to a nonexistent path # my ( $new_dn, $old_dn ); ( $d_dn, $new_dn ) = ( $1, $2 ) if $d_dn =~ /^([\-\w=]+),(.*)$/; if ( $new_dn ) { # absolute unless ( $self->is_valid_dn( $new_dn ) ) { print "Invalid destination.\n"; return; } } else { # relative $new_dn = $self->base(); } $old_dn = $1 if $s_dn =~ /^[\-\w=]+,(.*)$/; # get the source entry object my $e = ${ $s->{'entries'} }[0]; $e->dn( $s_dn ); # add changes in new entry instead of modifying existing $e->changetype( 'add' ); $e->dn( "$d_dn,$new_dn" ); # get the unique attribute from the dn for modification # perhaps there is a better way to do this...? # my ( $uniqkey, $uniqval ) = ( $1, $2 ) if $d_dn =~ /^([\-\.\w]+)(?:\s+)?=(?:\s+)?([\-\.\s\w]+),?/; unless ( $uniqkey && $uniqval ) { print "Unable to parse unique values from RDN.\n"; return; } $e->replace( $uniqkey => $uniqval ); # update (which will actually create the new entry) # my $update = sub { return $e->update($self->ldap()) }; my $rv = $self->with_retry( $update ); print $rv->error(), "\n"; # clear caches # $self->{'cache'}->{ $new_dn } = {} if $new_dn; $self->{'cache'}->{ $old_dn } = {} if $old_dn; $self->update_entries( clearcache => 1 ); return; } ### Create a new entry from scratch, using attributes from ### what the server's schema says is available from the specified ### (optional) objectClass list. Populate a new LDIF file and ### present an editor to the user. ### sub run_create { my $self = shift; my @ocs = @_; # manually generate some boilerplate LDIF. # unless ( $self->{'create_file'} ) { my $fh; ( $fh, $self->{'create_file'} ) = File::Temp::tempfile( 'shelldap_XXXXXXXX', SUFFIX => '.ldif', DIR => '/tmp', UNLINK => 1 ); # first print out the dn and object classes. # print $fh 'dn: ???,', $self->base(), "\n"; foreach my $oc ( sort @ocs ) { print $fh "objectClass: $oc\n"; } # gather and print attributes for requested objectClasses # my ( $must_attr, $may_attr ) = $self->fetch_attributes( \@ocs ); print $fh "$_: \n" foreach @{ $must_attr }; print $fh "# $_: \n" foreach @{ $may_attr }; close $fh; } # checksum the file. # my $hash_orig = $self->chksum( $self->{'create_file'} ); system( $self->{'editor'}, $self->{'create_file'} ) && die "Unable to launch editor: $!\n"; # detect a total lack of change # if ( $hash_orig eq $self->chksum($self->{'create_file'}) ) { print "Entry not modified.\n"; unlink $self->{'create_file'}; $self->{'create_file'} = undef; return; } # load in LDIF # my $ldif = Net::LDAP::LDIF->new( $self->{'create_file'}, 'r', onerror => 'warn' ); my $e = $ldif->read_entry(); unless ( $e ) { print "Unable to parse LDIF.\n"; unlink $self->{'create_file'}; $self->{'create_file'} = undef; return; } # create the new entry. # $e->changetype('add'); my $create = sub { return $e->update($self->ldap()) }; my $rv = $self->with_retry( $create ); print $rv->error(), "\n"; if ( $rv->code() != LDAP_SUCCESS && $self->prompt_edit_again() ) { return $self->run_create(); } $self->update_entries( clearcache => 1 ); unlink $self->{'create_file'}; $self->{'create_file'} = undef; return; } ### Remove an entry (or entries) from the LDAP directory. ### sub run_delete { my $self = shift; my @args = @_; my @matches; my $s; my $verbose; unless ( scalar @args ) { print "No dn specified.\n"; return; } # Flags. # if ( $args[0] =~ /^\-v/ ) { $verbose = 1; shift @args; } # Separate real args from filter arguments. # foreach my $dn ( @args ) { if ( $dn eq '*' ) { $s = $self->search({ scope => 'one' }); map { push @matches, $_ } @{ $s->{'entries'} } if $s->{'code'} == LDAP_SUCCESS; } # Search by filter # else { my $filter = $self->make_filter( [$dn] ) or next; $s = $self->search({ scope => 'one', filter => $filter }); if ( scalar @{$s->{'entries'}} != 0 ) { map { push @matches, $_ } @{ $s->{'entries'} } if $s->{'code'} == LDAP_SUCCESS; } # Search by exact DN. # else { $dn = $self->path_to_dn( $dn ); $s = $self->search({ base => $dn, vals => 0 }); my $e = ${ $s->{'entries'} }[0]; push @matches, $e if $s->{'code'} == LDAP_SUCCESS; } } } # Unique the matchset for a consistent count, keyed by DN. # my @uniq_matches = keys %{{ map { $_->dn => 1 } @matches }}; my $mcount = scalar @uniq_matches; if ( $mcount == 0 ) { print "Nothing matched.\n"; return; } if ( $verbose ) { print "* $_\n" foreach @uniq_matches; } print "About to remove $mcount item(s). Are you sure? [Ny]: "; chomp( my $resp = ); return unless $resp =~ /^y/i; my %seen; foreach my $e ( @matches ) { my $dn = $e->dn(); next if $seen{ $dn }; my $rv = $self->ldap->delete( $dn ); $seen{ $dn }++; print "$dn: ", $rv->error(), "\n"; } $self->update_entries( clearcache => 1 ); return; } ### Fetch an entry from the directory, write it out to disk ### as LDIF, launch an editor, then compare changes and write ### it back to the directory. ### sub run_edit { my $self = shift; my $dn = shift; unless ( $dn ) { print "No dn provided.\n"; return; } # convert given path to DN $dn = $self->path_to_dn( $dn ); # sanity check # my $s = $self->search({ base => $dn, vals => 1 }); unless ( $s->{'code'} == LDAP_SUCCESS ) { print $s->{'message'} . "\n"; return; } # fetch entry. my $e = ${ $s->{'entries'} }[0]; $e->changetype( 'modify' ); # write it out to disk. # unless( $self->{'edit_again'} ) { my $ldif = $self->ldif(1); $ldif->write_entry( $e ); $ldif->done(); # force sync } # load it into an array for potential comparison open LDIF, "$self->{'ldif_fname'}" or return; my @orig_ldif = ; close LDIF; # append optional, unused attributes as comments for fast reference. # unless ( $self->{'edit_again'} ) { my %current_attrs = map { $_ => 1 } $e->attributes(); my ( $must_attr, $may_attr ) = $self->fetch_attributes( $e->get_value('objectClass', asref => 1) ); open LDIF, ">> $self->{'ldif_fname'}"; foreach my $opt_attr ( sort { $a cmp $b } @{$may_attr} ) { next if $current_attrs{ $opt_attr }; print LDIF "# " . $opt_attr . ":\n"; } close LDIF; } # checksum it, then open it in an editor # my $hash_orig = $self->chksum( $self->{'ldif_fname'} ); system( $self->{'editor'}, $self->{'ldif_fname'} ) && die "Unable to launch editor: $!\n"; # detect a total lack of change # if ( $hash_orig eq $self->chksum($self->{'ldif_fname'}) ) { print "Entry not modified.\n"; unlink $self->{'ldif_fname'}; $self->{'edit_again'} = undef; return; } # check changes for basic LDIF validity # while( ! $self->load_ldif($self->{'ldif_fname'}) ) { print "Unable to parse LDIF.\n"; if ( $self->prompt_edit_again() ) { system( $self->{'editor'}, $self->{'ldif_fname'} ); } else { unlink $self->{'ldif_fname'}; $self->{'edit_again'} = undef; return; } } # load changes into a new array for comparison # open LDIF, "$self->{'ldif_fname'}" or return; my @new_ldif = ; close LDIF; # parser subref # my $parse = sub { my $line = shift || $_; return if $line =~ /^\#/; # ignore comments my ( $attr, $val ) = ( $1, $2 ) if $line =~ /^(.+?): (.*)$/; return unless $attr; return if index($attr, ':') != -1; # ignore base64 return ( $attr, $val ); }; $self->unwrap_line( \@orig_ldif ); $self->unwrap_line( \@new_ldif ); my $diff = Algorithm::Diff->new( \@orig_ldif, \@new_ldif ); HUNK: while ( $diff->Next() ) { next if $diff->Same(); my $diff_bit = $diff->Diff(); my %seen_attr; # attr removal hunk # if ( $diff_bit == 1 ) { foreach ( $diff->Items(1) ) { my ( $attr, $val ) = $parse->( $_ ) or next; $self->debug("DELETE: $_"); $e->delete( $attr => [ $val ] ); } } # attr insertion hunk # if ( $diff_bit == 2 ) { foreach ( $diff->Items(2) ) { my ( $attr, $val ) = $parse->( $_ ) or next; $self->debug("INSERT: $_"); $e->add( $attr => $val ); } } # attr change hunk # if ( $diff_bit == 3 ) { # modification to existing line # foreach ( $diff->Items(2) ) { my ( $attr, $val ) = $parse->( $_ ) or next; $self->debug("MODIFY: $_"); my $cur_vals = $e->get_value( $attr, asref => 1 ) || []; my $cur_valcount = scalar @$cur_vals; next if $cur_valcount == 0; # should have been an 'add' # replace immediately # if ( $cur_valcount == 1 ) { $e->replace( $attr => $val ); } else { # retain attributes that allow multiples, so updating # one attribute doesn't inadvertently remove others with # the same name. # next if $seen_attr{ $attr }; my @new_vals; foreach my $line ( @new_ldif ) { my ( $new_attr, $new_val ) = $parse->( $line ) or next; next unless $new_attr eq $attr; $seen_attr{ $attr }++; push @new_vals, $new_val; } $e->replace( $attr => \@new_vals ); } } # deletion within the same hunk # foreach ( $diff->Items(1) ) { my ( $attr, $val ) = $parse->( $_ ) or next; my $cur_vals = $e->get_value( $attr, asref => 1 ) || []; my $cur_valcount = scalar @$cur_vals; next if $cur_valcount == 1; next if $seen_attr{ $attr }; $self->debug("DELETE: $_"); $e->delete( $attr => [ $val ] ); } } } my $update = sub { return $e->update( $self->ldap ); }; my $rv = $self->with_retry( $update ); print $rv->error(), "\n"; if ( $rv->code() != LDAP_SUCCESS && $self->prompt_edit_again() ) { $self->{'edit_again'} = 1; return $self->run_edit( $dn ); } unlink $self->{'ldif_fname'}; $self->{'edit_again'} = undef; return; } ### Display current tunable runtime settings. ### sub run_env { my $self = shift; foreach ( sort @{ $self->{'env'} } ) { print "$_: "; print $conf->{$_} ? $conf->{$_} : 0; print "\n" } } ### Alter settings. ### sub run_setenv { my $self = shift; my ( $key, $val ) = @_; ( $key, $val ) = split /=/, $key if $key && ! defined $val; return unless $key && defined $val; $key = lc $key; $conf->{$key} = $val; return; } ### Search across the directory and display matching entries. ### sub run_grep { my $self = shift; my ( $recurse, $filter, $base ) = @_; # set 'recursion' unless ( $recurse && $recurse =~ /\-r|recurse/ ) { # shift args to the left ( $recurse, $filter, $base ) = ( undef, $recurse, $filter ); } $filter = Net::LDAP::Filter->new( $filter ); unless ( $filter ) { print "Invalid search filter.\n"; return; } # support '*' $base = $self->base() if ! $base or $base eq '*'; unless ( $base ) { print "No search base specified.\n"; return; } # convert base path to DN $base = $self->path_to_dn( $base ); $self->debug("Filter parsed as: " . $filter->as_string() . "\n"); my $s = $self->search({ scope => $recurse ? 'sub' : 'one', base => $base, filter => $filter }); foreach my $e ( @{ $s->{'entries'} } ) { my $dn = $e->dn(); print "$dn\n"; } return; } ### Override internal help function with pod2usage output. ### sub run_help { return Pod::Usage::pod2usage( -exitval => 'NOEXIT', -verbose => 99, -sections => 'SHELL COMMANDS' ); } ### Generate and display a list of LDAP entries, relative to the current ### location the command was run from. ### sub run_list { my $self = shift; my @args = @_; my @attrs = (); my $filter; # flag booleans my ( $recurse, $long ); # parse arguments: [