Project

General

Profile

« Previous | Next » 

Revision 02029fe9

Added by Marc Dequènes about 15 years ago

  • ID 02029fe9d1397d8e64ba6261260446520bb6207a

Initial release

View differences:

.gitignore
# backup files (mostly made by editors)
*~
\#*#
# generated by installer
.config
lib/ldapshadows/config.rb
data/locale
#
conf
var
#
config/private.conf
README
Medium-level LDAP Access Library and Tool.
If Net::LDAP is raw-level, ActiveLdap is low-level, LDAP Shadows
presents is step into abstraction and presentation of the
directory data, allowing access to 'shadows' of the real data.
It can be used with the provided tool as an enhanced LDAP editor,
or used as a library for a really high-level application.
config/test.conf
---
objects:
bot:
mapping:
dn_attribute: uid
prefix: ''
classes: ['bot']
sort_by: uid
presentation:
desc_attribute: cn
optional_classes: ['primary']
allowed_aspects: ['mail', 'fs', 'shell', 'ftp', 'web', 'jabber']
hidden_attributes: ['objectClass']
expert_attributes: ['uidNumber', 'gidNumber', 'gecos']
associations:
secondaryGroups:
type: :belongs_to
object: group
many: uniqueMember
foreign_key: dn
individual:
mapping:
dn_attribute: uid
prefix: ''
classes: ['individual']
sort_by: uid
presentation:
desc_attribute: cn
optional_classes: ['primary']
allowed_aspects: ['mail', 'fs', 'shell', 'ftp', 'web', 'jabber']
hidden_attributes: ['objectClass']
expert_attributes: ['uidNumber', 'gidNumber', 'gecos']
associations:
secondaryGroups:
type: :belongs_to
object: group
many: uniqueMember
foreign_key: dn
group:
mapping:
dn_attribute: cn
prefix: ''
classes: ['posixGroup', 'groupOfMembers']
sort_by: cn
presentation:
desc_attribute: description
optional_classes: []
allowed_aspects: []
hidden_attributes: ['objectClass']
expert_attributes: ['gidNumber']
associations:
individuals:
type: :has_many
object: individual
foreign_key: uniqueMember
primary_key: dn
bots:
type: :has_many
object: bot
foreign_key: uniqueMember
primary_key: dn
aspects:
mail:
mapping:
classes: ['emailUser']
presentation:
associations:
fs:
mapping:
classes: ['fsUser']
presentation:
skipped_attributes: ['loginShell']
associations:
shell:
mapping:
classes: ['shellUser']
presentation:
associations:
ftp:
mapping:
classes: ['ftpUser']
presentation:
skipped_attributes: ['loginShell']
associations:
web:
mapping:
classes: ['webUser']
presentation:
associations:
jabber:
mapping:
classes: ['jabberUser']
presentation:
associations:
primary:
mapping:
classes: ['primaryAccount']
presentation:
associations:
locale/en.yml
---
en:
attribute_types:
cn: "FullName"
description: "Description"
gidNumber: "Primary Group (numeric)"
givenName: "FirstName"
homeDirectory: "Home Directory"
host: "Shell Allowed Hosts"
jid: "Jabber ID"
keyFingerPrint: "GPG/PGP Key Fingerprint"
loginShell: "Shell Interpreter"
mail: "eMail address(es)"
mailForward: "eMail Forward Adress(es)"
mailQuota: "Maximum Mailbox Size"
manager: "Manager(s)"
owner: "Owner(s)"
sn: "Surname"
uid: "Identifier"
uidNumber: "Identifier (numeric)"
uniqueMember: "Group member"
webVirtualHost: "Hosted Web Sites"
associations:
primaryGroup: "Primary Group"
secondaryGroups: "Secondary Groups"
aspects:
ftp: "FTP Account"
web: "Web Account"
shell: "Shell Account"
fs: "FileSystem Account"
mail: "eMail Account"
jabber: "Jabber Account"
test.rb
#!/usr/bin/ruby -Ku
$KCODE = 'UTF8'
require 'jcode'
require 'yaml'
require 'active_ldap'
require 'cmdparse2'
require 'pathname'
config_str_prv = IO.read("config/private.conf")
config_str = IO.read("config/test.conf")
config = YAML.load(config_str_prv).merge(YAML.load(config_str))
ActiveLdap::Base.setup_connection(config['ldap'])
cmdparser = CmdParse::CommandParser.new(true)
cmdparser.program_name = ""
cmdparser.program_version = [0, 0, 1]
cmdparser.options = CmdParse::OptionParserWrapper.new do |opt|
opt.separator "Global options:"
opt.on("--debug", "Output debug info without being formated") {|t| $debug_opt = true }
opt.on("--expert", "Output extra info for expert users") {|t| $expert_opt = true }
end
cmdparser.add_command(CmdParse::HelpCommand.new)
cmdparser.add_command(CmdParse::VersionCommand.new)
module LdapMapper
class LdapObject < ActiveLdap::Base
class_inheritable_accessor :presentation, :mapper
def name
self[dn_attribute].is_a?(Array) ? self[dn_attribute][0] : self[dn_attribute]
end
def description
[self.class.presentation[:desc_attribute], 'displayName', 'cn', 'description'].each do |attr|
if self.has_attribute?(attr) and self.attribute_present?(attr)
return self[attr].is_a?(Array) ? self[attr][0] : self[attr]
end
end
return ""
end
def aspects
present_aspects = {}
self.class.presentation[:allowed_aspects].each do |aspect|
aspect_data = self.class.mapper.get_aspect(aspect)
aspect_mapping = aspect_data['mapping']
present_aspects[aspect] = aspect_data if self.classes & aspect_mapping['classes'] == aspect_mapping['classes']
end
present_aspects
end
end
class Controller
def initialize(mod_container = LdapMapper::Objects)
@mod_container = mod_container
@object_definitions = {}
@aspects = {}
end
def set_aspect(aspect_name, aspect_def)
@aspects[aspect_name] = aspect_def
end
def get_aspect(aspect_name)
@aspects[aspect_name]
end
def self.object_name_to_klass_name(obj_name)
"LdapObject" + obj_name.capitalize
end
def load_object(obj_name, obj_def)
obj_def.symbolize_keys!
obj_mapping = obj_def[:mapping].symbolize_keys
klass_name = self.class.object_name_to_klass_name(obj_name)
# create class
@mod_container.module_eval(<<-EOS)
class #{klass_name} < LdapMapper::LdapObject; end
EOS
# configure class
klass = find_klass(obj_name)
klass.presentation = obj_def[:presentation].symbolize_keys
klass.mapper = self
klass.ldap_mapping obj_mapping.reject {|key, val| not ActiveLdap::Base::VALID_LDAP_MAPPING_OPTIONS.include?(key) }
# store definition for later associations processing
@object_definitions[obj_name] = obj_def
end
def find_klass(obj_name)
klass_name = self.class.object_name_to_klass_name(obj_name)
return nil unless @mod_container.const_defined?(klass_name)
@mod_container.const_get(klass_name)
end
# run it _once_ when all objects are loaded
def load_associations
@object_definitions.each_pair do |obj_name, obj_def|
next unless obj_def.include?(:associations)
obj_assoc = obj_def[:associations]
klass = find_klass(obj_name)
obj_assoc.each_pair do |field_name, assoc|
assoc.symbolize_keys!
foreign_klass = find_klass(assoc[:object])
assoc[:class_name] = foreign_klass.to_s
case assoc[:type]
when :belongs_to
klass.belongs_to field_name, assoc.reject {|key, val| not ActiveLdap::Associations::ClassMethods::VALID_BELONGS_TO_OPTIONS.include?(key) }
when :has_many
klass.has_many field_name, assoc.reject {|key, val| not ActiveLdap::Associations::ClassMethods::VALID_HAS_MANY_OPTIONS.include?(key) }
else
raise "bug in '#{obj_name}' object associations (wrong type)"
end
end
end
end
end
# default location for mapped objects
module Objects
end
end
I18n.load_path += Dir[File.join(Pathname.new(".").realpath, "locale", "*.yml")]
I18n.default_locale = :en
ldapctl = LdapMapper::Controller.new
config['aspects'].each_pair do |aspect_name, aspect_data|
ldapctl.set_aspect(aspect_name, aspect_data)
end
config['objects'].each_pair do |obj_name, obj_data|
ldapctl.load_object(obj_name, obj_data)
end
ldapctl.load_associations
cmd = CmdParse::Command.new('list', false)
cmd.short_desc = "list objects"
cmd.set_execution_block do |args|
if args.size != 1
STDERR.puts "syntax error: no object name given"
exit 1
end
obj_name = args[0]
obj_klass = ldapctl.find_klass(obj_name)
if obj_klass.nil?
STDERR.puts "No such object '#{obj_name}'."
exit 2
end
obj_klass.find(:all).each do |obj|
puts "#{obj.name}: #{obj.description}"
end
end
cmdparser.add_command(cmd)
def objectclasses_attr_list(objectclass_list)
objectclass_list = [objectclass_list] unless objectclass_list.is_a? Array
list = []
objectclass_list.each do |objectclass|
objectclass_obj = ActiveLdap::Base.schema.object_class(objectclass)
attr_list = objectclass_obj.must + objectclass_obj.may
list += attr_list.collect{|attr| attr.human_attribute_name }
end
list
end
def display_attributes(item, attr_list = nil)
attr_list = item.attributes.keys.sort if attr_list.nil?
attr_list.each do |key|
next if item.class.presentation[:hidden_attributes].include?(key)
next if not $expert_opt and item.class.presentation[:expert_attributes].include?(key)
att = ActiveLdap::Base.schema.attribute(key)
next if att.binary?
val = item[key]
item_name = I18n.t(att.human_attribute_name, :scope => 'attribute_types', :default => att.human_attribute_description)
puts item_name + ": " + (val.is_a?(Array) ? val.sort.collect{|v| v.to_s }.join(", ") : val.to_s)
end
end
cmd = CmdParse::Command.new('show', false)
cmd.short_desc = "show object information"
cmd.set_execution_block do |args|
if args.size < 1
STDERR.puts "syntax error: no object name given"
exit 1
end
if args.size < 2
STDERR.puts "syntax error: no item name given"
exit 1
end
obj_name = args[0]
obj_klass = ldapctl.find_klass(obj_name)
if obj_klass.nil?
STDERR.puts "No such object '#{obj_name}'."
exit 2
end
item_name = args[1]
begin
item = obj_klass.find(item_name)
rescue ActiveLdap::EntryNotFound
STDERR.puts "No such item '#{obj_name}/#{item_name}'"
exit 2
end
aspects = item.aspects
if $debug_opt
puts item.to_s
puts "=== Detected Info ==="
puts "aspects: " + aspects.keys.sort.join(", ")
else
used_attributes = []
attr_list = (objectclasses_attr_list(item.required_classes + item.class.presentation[:optional_classes]) & item.attributes.keys).sort
display_attributes(item, attr_list)
used_attributes += attr_list
aspects.keys.sort.each do |r|
aspect_display_name = I18n.t(r, :scope => 'aspects', :default => "Aspect: #{r}")
puts "=== #{aspect_display_name} ==="
skipped_attributes = defined?(aspects[r]['presentation']['skipped_attributes']) ? aspects[r]['presentation']['skipped_attributes'] : []
attr_list = ((objectclasses_attr_list(aspects[r]['mapping']['classes']) & item.attributes.keys) - used_attributes - skipped_attributes).sort
display_attributes(item, attr_list)
used_attributes += attr_list
end
end
puts "=== Associations ==="
item.associations.each do |assoc|
assoc_display_name = I18n.t(assoc, :scope => 'associations', :default => assoc.to_s)
puts "#{assoc_display_name}: " + item.send(assoc).collect{|g| g.name }.join(", ")
end
end
cmdparser.add_command(cmd)
cmdparser.parse
# TODO: each aspect should be able to "reserve" attributetypes (instead of the ugly 'skipped_attributes' mechanism)
# if no aspect reserves it, then if the object can take it if it is in its attr_list

Also available in: Unified diff