|
#!/usr/bin/ruby -Ku
|
|
|
|
#--
|
|
# LdapShadows, a Medium-level LDAP Access Library and Tool.
|
|
# Copyright (c) 2009 Marc Dequènes (Duck) <Duck@DuckCorp.org>
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
#++
|
|
|
|
# to allow in-place run for test
|
|
$: << File.join(File.dirname(__FILE__), "..", "lib")
|
|
|
|
# get locale from shell
|
|
locale = ENV['LANGUAGE'] || ENV['LC_ALL'] || ENV['LC_MESSAGES'] || ENV['LANG']
|
|
|
|
require 'ldap_shadows'
|
|
require 'ldap_shadows/display_utils'
|
|
require 'yaml'
|
|
require 'cmdparse2'
|
|
|
|
include LdapShadows
|
|
|
|
set_locale(locale)
|
|
|
|
config_str = IO.read(File.join(LdapShadows::Config::CFG_DIR, "test.conf"))
|
|
config = YAML.load(config_str)
|
|
config_str_prv_filelist = [
|
|
File.join(ENV['HOME'], ".shadowwalker"),
|
|
File.join(LdapShadows::Config::CFG_DIR, "private.conf")
|
|
]
|
|
config_str_prv_filelist.each do |file|
|
|
if File.exists?(file)
|
|
config_str_prv = IO.read(file)
|
|
config.merge!(YAML.load(config_str_prv))
|
|
break
|
|
end
|
|
end
|
|
config.recursive_symbolize_keys!
|
|
|
|
|
|
$program_options = {}
|
|
|
|
cmdparser = CmdParse::CommandParser.new(true)
|
|
cmdparser.banner = PRODUCT
|
|
cmdparser.program_name = File.basename(__FILE__)
|
|
cmdparser.program_version = VERSION.split(".")
|
|
|
|
cmdparser.options = CmdParse::OptionParserWrapper.new do |opt|
|
|
opt.separator "Global options:"
|
|
opt.on("-a", "--admin", "Output extra info for administrators") {|t| $program_options[:admin] = true }
|
|
opt.on("-d", "--debug", "Output debug info without being formated") {|t| $program_options[:debug] = true }
|
|
opt.on("-e", "--expert", "Output extra info for expert users") {|t| $program_options[:expert] = true }
|
|
opt.on("-H", "--handles", "Output with handles (objects/field/... keys used for manipulations)") {|t| $program_options[:handles] = true }
|
|
end
|
|
|
|
cmdparser.add_command(CmdParse::HelpCommand.new)
|
|
cmdparser.add_command(CmdParse::VersionCommand.new)
|
|
|
|
|
|
$ldapctl = Controller.new
|
|
$ldapctl.set_global_config(config[:presentation])
|
|
config[:aspects].each_pair do |aspect_name, aspect_data|
|
|
$ldapctl.set_aspect(aspect_name, aspect_data)
|
|
end
|
|
begin
|
|
ActiveLdap::Base.setup_connection(config[:ldap])
|
|
|
|
obj_config_dir = File.join(LdapShadows::Config::CFG_DIR, "objects")
|
|
obj_config_pattern = File.join(obj_config_dir, "**", "*.conf")
|
|
|
|
Dir.glob(obj_config_pattern).each do |f|
|
|
next if f[0..0] == "."
|
|
|
|
obj_name = File.basename(f).sub(".conf", "")
|
|
obj_config = YAML.load(IO.read(f))
|
|
obj_config.recursive_symbolize_keys!
|
|
|
|
$ldapctl.load_object(obj_name, obj_config)
|
|
end
|
|
|
|
$ldapctl.load_relations
|
|
rescue ActiveLdap::Error => e
|
|
STDERR.puts _("LDAP connection error: %s") % e.to_s
|
|
exit 2
|
|
end
|
|
|
|
|
|
class Command < CmdParse::Command
|
|
attr_accessor :usages_params
|
|
|
|
def usage
|
|
if @usages_params
|
|
base_usage_str = super.sub("[ARGS]", "")
|
|
usages = @usages_params.collect {|params| base_usage_str + params }
|
|
usages.join("\n")
|
|
else
|
|
super
|
|
end
|
|
end
|
|
|
|
protected
|
|
|
|
def params_shift_location(args)
|
|
raise SyntaxError, _("no location name given") if args.empty?
|
|
|
|
location = args.shift.downcase
|
|
raise SyntaxError, _("bad location") unless location =~ /^([a-zA-Z]+)\/(.+)$/
|
|
loc_obj_hdl = $1
|
|
loc_item_hdl = $2
|
|
|
|
split_args = [loc_obj_hdl, loc_item_hdl]
|
|
loc_obj_klass = params_shift_object(split_args)
|
|
loc_item = params_shift_item(loc_obj_klass, split_args)
|
|
|
|
[loc_obj_klass, loc_item]
|
|
end
|
|
|
|
def params_shift_object(args)
|
|
raise SyntaxError, _("no object name given") if args.empty?
|
|
|
|
obj_hdl = args.shift.downcase.singularize
|
|
obj_klass = $ldapctl.find_klass(obj_hdl)
|
|
raise PreProcessingError, _("No such object '%s'") % obj_hdl if obj_klass.nil?
|
|
obj_klass
|
|
end
|
|
|
|
def params_shift_item(obj_klass, args)
|
|
raise SyntaxError, _("no item name given") if args.empty?
|
|
|
|
attr_list = ["*"]
|
|
attr_list << "+" if $program_options[:admin]
|
|
|
|
item_hdl = args.shift.downcase
|
|
begin
|
|
item = obj_klass.find(item_hdl, :attributes => attr_list)
|
|
rescue ActiveLdap::EntryNotFound
|
|
raise PreProcessingError, _("No such item '%s/%s'") % [obj_klass.handle, item_hdl]
|
|
end
|
|
item
|
|
end
|
|
end
|
|
|
|
class ListCommand < Command
|
|
def initialize
|
|
super('list', false)
|
|
|
|
self.short_desc = "list objects, and items/aspects/fields of an object"
|
|
self.usages_params = [
|
|
":objects",
|
|
"<object>",
|
|
"<object> :aspects",
|
|
"<object> :fields"
|
|
]
|
|
end
|
|
|
|
def execute (args)
|
|
raise SyntaxError, _("no object name given") if args.empty?
|
|
|
|
obj_hdl = args.shift.downcase.singularize
|
|
case obj_hdl
|
|
when ':object'
|
|
list = $ldapctl.objects
|
|
puts "=== List of LDAP objects (#{list.size}) ==="
|
|
list.each do |obj_name|
|
|
puts " - #{obj_name}"
|
|
end
|
|
else
|
|
obj_klass = params_shift_object([obj_hdl])
|
|
|
|
if args.empty?
|
|
obj_human_name = Translator.translate_object_name(obj_hdl)
|
|
Display.display_item_list("List of #{obj_human_name.pluralize}", obj_klass.find(:all), $program_options)
|
|
else
|
|
subobj_hdl = args.shift.downcase.singularize
|
|
|
|
obj_human_name = Translator.translate_object_name(obj_hdl)
|
|
|
|
case subobj_hdl
|
|
when ':aspect'
|
|
list = obj_klass.possible_aspects
|
|
puts "=== List of LDAP Aspects of #{obj_human_name.pluralize} (#{list.size}) ==="
|
|
list.each do |aspect|
|
|
str = Translator.translate_aspect_name(aspect)
|
|
str += " [#{aspect}]" if $program_options[:handles]
|
|
puts " - #{str}"
|
|
end
|
|
when ':field'
|
|
list = obj_klass.possible_attributes
|
|
puts "=== List of LDAP Fields of #{obj_human_name.pluralize} (#{list.size}) ==="
|
|
list.each do |field|
|
|
str = Translator.translate_field_name(field)
|
|
str += " [#{field}]" if $program_options[:handles]
|
|
puts " - #{str}"
|
|
end
|
|
obj_klass.possible_aspects.each do |aspect|
|
|
aspect_human_name = Translator.translate_aspect_name(aspect)
|
|
list = obj_klass.possible_attributes_for_aspect(aspect)
|
|
puts "--- Sublist of LDAP Fields for Aspect #{aspect_human_name} (#{list.size}) ---"
|
|
list.each do |field|
|
|
str = Translator.translate_field_name(field)
|
|
str += " [#{field}]" if $program_options[:handles]
|
|
puts " - #{str}"
|
|
end
|
|
end
|
|
else
|
|
raise PreProcessingError, _("No such subobject '%s'") % subobj_hdl
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
cmdparser.add_command(ListCommand.new)
|
|
|
|
class ShowCommand < Command
|
|
def initialize
|
|
super('show', false)
|
|
|
|
self.short_desc = "show item information"
|
|
self.usages_params = [
|
|
"<object> <item>",
|
|
"<object> <item> <field>#?",
|
|
"<object> <item> <field>[#<index>]"
|
|
]
|
|
end
|
|
|
|
def execute (args)
|
|
obj_klass = params_shift_object(args)
|
|
item = params_shift_item(obj_klass, args)
|
|
|
|
if args.empty?
|
|
$program_options[:skip_binary] = true
|
|
Display.display_item(item, $program_options)
|
|
else
|
|
field_info = args.shift
|
|
field_name, file_no = field_info.split("#")
|
|
|
|
unless item.has_field?(field_name)
|
|
raise PreProcessingError, _("No such field '%s' in object '%s'") % [field_name, obj_klass.handle]
|
|
end
|
|
unless item.attribute_present?(field_name)
|
|
raise PreProcessingError, _("Field '%s' in item '%s/%s' is not present") %
|
|
[field_name, obj_klass.handle, item.name]
|
|
end
|
|
|
|
field_data = item.send(field_name, true)
|
|
|
|
result_is_binary = false
|
|
if file_no == "?"
|
|
result = field_data.size
|
|
else
|
|
file_no_i = file_no.to_i
|
|
file_no = (file_no_i.to_s == file_no) ? file_no_i : nil
|
|
|
|
attr_info = ActiveLdap::Base.schema.attribute(field_name)
|
|
|
|
field_single = attr_info.single_value?
|
|
if file_no.nil? and not field_single
|
|
raise PreProcessingError, _("Field '%s' in object '%s' has multiple values, but you didn't select one") %
|
|
[field_name, obj_klass.handle]
|
|
end
|
|
|
|
file_no = 0 if file_no.nil?
|
|
if file_no > 0 and field_single
|
|
raise PreProcessingError, _("Field '%s' in object '%s' has only a single value") %
|
|
[field_name, obj_klass.handle]
|
|
end
|
|
if file_no < 0 or file_no >= field_data.size
|
|
raise PreProcessingError, _("Field '%s' in object '%s' doesn't have such file number '%s' (select in range [0, %s])") %
|
|
[field_name, obj_klass.handle, file_no, field_data.size - 1]
|
|
end
|
|
|
|
result = field_data[file_no]
|
|
result_is_binary = attr_info.binary?
|
|
end
|
|
|
|
if args.empty?
|
|
if result_is_binary
|
|
# output binary data without newline, so it can be pipped or redirected easily
|
|
STDOUT.write result
|
|
else
|
|
puts result
|
|
end
|
|
else
|
|
file_name = args.shift
|
|
if File.exists?(file_name)
|
|
raise PreProcessingError, _("File '%s' already exists, and i won't overwrite it") % file_name
|
|
end
|
|
|
|
begin
|
|
open(file_name, "w") do |fp|
|
|
fp.write result
|
|
end
|
|
rescue
|
|
raise ProcessingError, _("Cannot save file: %s") % $!
|
|
end
|
|
|
|
puts "File saved."
|
|
end
|
|
end
|
|
end
|
|
end
|
|
cmdparser.add_command(ShowCommand.new)
|
|
|
|
class TreeCommand < Command
|
|
def initialize
|
|
super('tree', false)
|
|
|
|
self.short_desc = "show skeleton items tree"
|
|
self.usages_params = [
|
|
"[<location>]"
|
|
]
|
|
self.options = CmdParse::OptionParserWrapper.new do |opt|
|
|
opt.on( '-a', '--all', 'Display all objects in the tree.' ) { $program_options[:tree_all_objects] = true }
|
|
end
|
|
end
|
|
|
|
def execute (args)
|
|
puts "Tree:"
|
|
|
|
search_base = nil
|
|
unless args.empty?
|
|
loc_obj_klass, loc_item = params_shift_location(args)
|
|
search_base = loc_item.dn
|
|
end
|
|
|
|
if $program_options[:tree_all_objects]
|
|
dn_list = ActiveLdap::Base.search(:base => search_base, :scope => :sub, :attributes => ['']).collect do |entry|
|
|
ActiveLdap::DistinguishedName.parse(entry.first)
|
|
end
|
|
else
|
|
dn_list = []
|
|
gconfig = $ldapctl.get_global_config()
|
|
|
|
(gconfig[:tree_objects] || []).each do |obj_hdl|
|
|
obj_klass = $ldapctl.find_klass(obj_hdl.downcase)
|
|
unless obj_klass
|
|
raise PreProcessingError, _("Location object '%s' not defined") % obj_hdl
|
|
end
|
|
|
|
obj_klass.find(:all, :base => search_base, :scope => :sub).each do |obj|
|
|
dn_list << obj.dn_obj
|
|
end
|
|
end
|
|
end
|
|
|
|
base_dn = ActiveLdap::DistinguishedName.parse(LdapObject.base)
|
|
|
|
tree = {}
|
|
dn_list.each do |dn|
|
|
ptree = tree
|
|
pdn = nil
|
|
(dn - base_dn).to_s.split(',').reverse.each do |pdn|
|
|
ptree[pdn] = {} unless ptree.has_key?(pdn)
|
|
ptree = ptree[pdn]
|
|
end
|
|
ptree = nil
|
|
end
|
|
tree = {base_dn => tree}
|
|
|
|
Display.display_hash_tree(tree, 0)
|
|
end
|
|
end
|
|
cmdparser.add_command(TreeCommand.new)
|
|
|
|
class ModCommand < Command
|
|
def initialize
|
|
super('mod', false)
|
|
|
|
self.short_desc = "Modify attributes of an item"
|
|
self.usages_params = [
|
|
"<object> <item> (<field>|<relation>|:aspects)(=|+=|-=)<value> [(<field>|<relation>|:aspects)(=|+=|-=)<value>] ..."
|
|
]
|
|
end
|
|
|
|
def execute (args)
|
|
obj_klass = params_shift_object(args)
|
|
item = params_shift_item(obj_klass, args)
|
|
|
|
modification_done = false
|
|
args.each do |mod_info|
|
|
mod_done = item.item_modify_from_string(mod_info)
|
|
modification_done ||= mod_done
|
|
end
|
|
|
|
if modification_done
|
|
item.save!
|
|
puts "Modification done."
|
|
else
|
|
puts "Nothing to do."
|
|
end
|
|
end
|
|
end
|
|
cmdparser.add_command(ModCommand.new)
|
|
|
|
class DelCommand < Command
|
|
def initialize
|
|
super('del', false)
|
|
|
|
self.short_desc = "Delete an item"
|
|
self.usages_params = [
|
|
"<object> <item>"
|
|
]
|
|
self.options = CmdParse::OptionParserWrapper.new do |opt|
|
|
opt.on( '-r', '--recursive', 'Delete object and all children recursively.' ) { $program_options[:recursive_delete] = true }
|
|
end
|
|
end
|
|
|
|
def execute (args)
|
|
obj_klass = params_shift_object(args)
|
|
item = params_shift_item(obj_klass, args)
|
|
|
|
if $program_options[:recursive_delete]
|
|
item.class.delete_all(nil, :scope => :sub, :base => item.dn)
|
|
else
|
|
item.delete
|
|
end
|
|
|
|
puts "Deletion done."
|
|
end
|
|
end
|
|
cmdparser.add_command(DelCommand.new)
|
|
|
|
class CreateCommand < Command
|
|
def initialize
|
|
super('create', false)
|
|
|
|
self.short_desc = "Create an item"
|
|
self.usages_params = [
|
|
"<object> <new_item> <location> [<field>=<value>] ..."
|
|
]
|
|
end
|
|
|
|
def execute (args)
|
|
obj_klass = params_shift_object(args)
|
|
|
|
raise SyntaxError, _("no item name given") if args.empty?
|
|
item_hdl = args.shift.downcase
|
|
|
|
loc_obj_klass, loc_item = params_shift_location(args)
|
|
|
|
item = obj_klass.new(item_hdl)
|
|
item.base = loc_item.dn_obj - obj_klass.base_obj
|
|
|
|
|
|
modification_done = false
|
|
args.each do |mod_info|
|
|
mod_done = item.item_modify_from_string(mod_info)
|
|
modification_done ||= mod_done
|
|
end
|
|
|
|
item.save!
|
|
puts "Creation done."
|
|
end
|
|
end
|
|
cmdparser.add_command(CreateCommand.new)
|
|
|
|
class SearchCommand < Command
|
|
def initialize
|
|
super('search', false)
|
|
|
|
self.short_desc = "Search items"
|
|
self.usages_params = [
|
|
"[:objects=<object>[,<object>]...] [:aspects=<aspect>[,<aspect>]...] [<field>=<value>] ..."
|
|
]
|
|
end
|
|
|
|
def execute (args)
|
|
raise SyntaxError, _("no search arguments") if args.empty?
|
|
|
|
res = LdapObject.items_find_from_strings(args)
|
|
unless res.empty?
|
|
display_lines = []
|
|
res.each do |raw_item|
|
|
display_lines << LdapObject.raw_item_info(raw_item)[:name]
|
|
end
|
|
puts display_lines.join("\n")
|
|
end
|
|
end
|
|
end
|
|
cmdparser.add_command(SearchCommand.new)
|
|
|
|
|
|
begin
|
|
cmdparser.parse
|
|
rescue SyntaxError => e
|
|
STDERR.puts _("Syntax error: %s") % e.to_s
|
|
exit 1
|
|
rescue PreProcessingError => e
|
|
STDERR.puts _("Preprocessing error: %s") % e.to_s
|
|
exit 2
|
|
rescue ActiveLdap::EntryInvalid => e
|
|
# work around activeldap#26758 (for english output only :-/)
|
|
STDERR.puts e.to_s.sub(/invalid format: .*: required syntax:/, "invalid format. required syntax:")
|
|
exit 3
|
|
rescue ActiveLdap::OperationNotPermitted => e
|
|
STDERR.puts _("You don't have enough rights for this operation")
|
|
exit 3
|
|
rescue ActiveLdap::Error => e
|
|
STDERR.puts _("LDAP error: %s") % e.to_s
|
|
exit 3
|
|
rescue WeirdError =>e
|
|
STDERR.puts _("Weird error: %s") % e.to_s
|
|
exit 4
|
|
end
|