Class: AdDir::Entry

Inherits:
Object
  • Object
show all
Includes:
DerivedAttributes
Defined in:
lib/ad_dir/entry.rb

Overview

Entry is basically a wrapper of Net::LDAP::Entry with some additional class methods that provide ActiveRecord-like finders.

Design Overview

Entry stores the original Net::LDAP::Entry object in an instance variable @ldap_entry. When the entry is fetched from the ActiveDirectory a snapshot of the entry's persisted attributes and its values is stored in a hash @persisted_attrs (using #dup!).

Whenever an attribute is changed the method #changes calculates the difference between the currrent values and the values in @persisted_attrs.

Attributes

List all attribute names:

user.attribute_names
  => [:dn, :objectclass, :cn, :sn, :givenname, :distinguishedname,
  :instancetype, :whencreated, :whenchanged, :displayname, :usncreated,
  :usnchanged, :directreports, :name, :objectguid, :useraccountcontrol,
  :badpwdcount, :codepage, :countrycode, :badpasswordtime, :lastlogoff,
  :lastlogon, :pwdlastset, :primarygroupid, :objectsid, :accountexpires,
  :logoncount, :samaccountname, :samaccounttype, :lockouttime,
  :objectcategory, :dscorepropagationdata,
  :msds-supportedencryptiontypes]

To get a hash of all attribute names and their values

user.attributes

Note: Mainly for debugging purposes there is the method #raw_attributes returning the original attributes as present in the Net::LDAP::Entry object (see also Retrieving Attribute Values)

Retrieving Attribute Values

Values of attributes can be accessed in two ways:

entry.sn# => "Doe"


# NOT RECOMMENDED!
entry[:sn]# => ["Doe"]

As a rule of thumbs use

  • #attr_name to get the values ready-to-use without wrapping array.
  • [:attr_name] only if you want to retrieve the original Net::LDAP::Entry values.

Create

  • Create an entry by specifying a DN and providing an conformant set of valid attributes:
jdoe = AdDir::Entry.create(dn: 'cn=John Doe,ou=mgrs,dc=my,dc=nice,dc=com',
  givenname: 'John',
  sn: 'Doe',
  objectclass: %w(top person organizationalPerson user),
  mail: '[email protected]')
  • Build a new entry first and then save it.
jdoe = AdDir::Entry.new('cn=John Doe,ou=mgrs,dc=my,dc=nice,dc=com')
jdoe.sn = 'Doe'
jdoe.givenname = 'John'
jdoe.objectclass = %w(top person organizationalPerson user)
jdoe.mail = '[email protected]'
jdoe.new_entry?# => true

jdoe.save

Read

.find

AdDir::Entry.find('jdoe')# => searches with an LDAP filter '(samaccountname=jdoe)'


AdDir::Entry.find_by_givenname('Doe*')# => '(givenname=Doe*)'

.where (Filter)

  • Using a Hash
  AdDir::Entry.where(cn: 'Doe', mail: '[email protected]')  # => '(&(cn=Doe)([email protected]))'

  • Using a LDAP-Filter-String
  AdDir::Entry.where('(|(sn=Foo)(cn=Bar))')

.all

  AdDir::Entry.all  # => retrieves all entries for the given 'tree_base'

Update

 jdoe = AdDir::Entry.find('jdoe')
 jdoe[:givenname] = 'Jonny'   # instead of 'John'
 jdoe.changed? # => true

 jdoe.changes # => {givenname: ['John', 'Jonny']}

 jdoe.save

Destroy

 jdoe.destroy

Direct Known Subclasses

Group, User

Constant Summary collapse

FIND_METHOD_REGEXP =

Regexp that matches find_xxx methods.

Note: Likewise ActiveRecord the difference between .find and .where is boldly that .find is used when you are really looking for a given entry, while the latter is used to filter on some condition.

/\Afind(_by_(\w+))?\Z/
OBJECTCATEGORY =

Define which category a record belongs to Active Directory knows: person, computer, group

''.freeze

Class Method Summary collapse

Instance Method Summary collapse

Methods included from DerivedAttributes

#created_at, #derived_attribute_names, #objectguid_decoded, #objectsid_decoded, #updated_at

Constructor Details

#initialize(dn = nil, attrs = {}) ⇒ Entry

We do not provide a constructor, but use the standard one of Net::LDAP::Entry

Net::LDAP::Entry.new(dn=nil)

436
437
438
439
440
441
442
443
# File 'lib/ad_dir/entry.rb', line 436

def initialize(dn = nil, attrs = {})
  #
  @ldap_entry = Net::LDAP::Entry.new(dn)
  attrs.each { |name, value| @ldap_entry[name] = value }
  @new_entry = true
  @persisted_attrs = {}
  self
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(method_sym, *args, &block) ⇒ Object (private)


484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
# File 'lib/ad_dir/entry.rb', line 484

def method_missing(method_sym, *args, &block)
  # Distinguish `method_sym` to speed up attribute setting and getting
  if method_sym.to_s.end_with?('=')    # Setter, e.g.  `:email=`

    @ldap_entry[method_sym] = args.first
  elsif @ldap_entry.attribute_names.include?(method_sym)    # Getter, i.e. a valid attribute name ( e.g.  `:email`)

    get_value(method_sym)
  elsif @ldap_entry.respond_to?(method_sym)    # any other Net::LDAP::Entry instance method
    # (e.g. `:attribute_names`)

    @ldap_entry.__send__(method_sym, *args)
  else
    super
  end
end

Class Method Details

._select_dn(dn) ⇒ Object

This method fetches a raw Net::LDAP::Entry given the DN. The hope is this is the most efficient way to fetch an entry. :nodoc:


287
288
289
290
291
292
293
294
# File 'lib/ad_dir/entry.rb', line 287

def self._select_dn(dn)
  args = {
    base:   dn,
    scope:  Net::LDAP::SearchScope_BaseObject,
    filter: category_filter
  }
  connection.search(args)
end

.allArray

Returns all objects.

Returns:

  • (Array)

    all objects


173
174
175
176
177
# File 'lib/ad_dir/entry.rb', line 173

def self.all
  search(base: @tree_base, filter: category_filter).collect do |e|
    from_ldap_entry(e)
  end
end

.category_filterObject

Use this to efficiently select entries from the Active Directory. The filter relays on the class instance variable @objectcategory


298
299
300
301
302
# File 'lib/ad_dir/entry.rb', line 298

def self.category_filter
  return @category_filter if @category_filter
  cat = const_get(:OBJECTCATEGORY).empty? ? '*' : const_get(:OBJECTCATEGORY)
  @category_filter = Net::LDAP::Filter.eq('objectcategory', cat)
end

.connectionObject

Returns the ActiveDirectory's connection


158
159
160
# File 'lib/ad_dir/entry.rb', line 158

def self.connection
  AdDir.connection
end

.create(dn, attributes) ⇒ Object

Instantiates an AdDir::Entry and saves it in the ActiveDirectory.

We try to create the entry in the ActiveDirectory and then return it again from there. Depending on your ActiveDirectory the set of mandatory attributes may vary. If you don't provide the correct set of attributes the ActiveDirectory will refuse to add the entry and fail.


251
252
253
254
255
256
257
258
259
# File 'lib/ad_dir/entry.rb', line 251

def self.create(dn, attributes)
  #
  success = connection.add(dn: dn, attributes: attributes)
  if success
    select_dn(dn)
  else
    connection.get_operation_result
  end
end

.from_ldap_entry(entry) ⇒ Object

Constructs a AdDir::Entry from a Net::LDAP::Entry.


262
263
264
265
266
267
268
# File 'lib/ad_dir/entry.rb', line 262

def self.from_ldap_entry(entry)
  e = new(entry.dn)
  e.instance_variable_set('@ldap_entry', entry)
  e.instance_variable_set('@new_entry', false)
  e.instance_variable_set('@persisted_attrs', e.raw_attributes.dup)
  e
end

.parent_nameObject

Return the name of the parent module/class.

This is important for inflection when inheriting classes. The method is copied from ActiveSupport::CoreExtensions::Module


224
225
226
227
# File 'lib/ad_dir/entry.rb', line 224

def self.parent_name
  return @parent_name if defined? @parent_name
  @parent_name = name =~ /::[^:]+\Z/ ? $`.freeze : nil
end

.primary_keyObject

Returns the name of the attribute that acts as primary_key.

The primary_key is used as default when searching.

AdDir::Entry.find('jdoe')

searches for an entry with 'samaccountname' = 'jdoe'.

See Also:

  • {#primary_key}.

215
216
217
# File 'lib/ad_dir/entry.rb', line 215

def self.primary_key
  @primary_key ||= 'samaccountname'
end

.primary_key=(value) ⇒ Object

Sets the name of the attribute that acts as primary_key.

class User < AdDir::Entry
  self.primary_key = :samaccountname
end

203
204
205
# File 'lib/ad_dir/entry.rb', line 203

def self.primary_key=(value)
  @primary_key = value && value.to_s
end

.select_dn(dn) ⇒ Object

Select an entry by its '''Distinguished Name''' (DN)

Example

AdDir::Entry.select_dn('CN=Joe Doe,OU=People,DC=acme,DC=com')

Parameters:

  • dn (String)

    Distinguished Name of an entry

Returns:

  • Entry or nil


278
279
280
281
# File 'lib/ad_dir/entry.rb', line 278

def self.select_dn(dn)
  success = _select_dn(dn)
  success && from_ldap_entry(success.first)
end

.sibling_klass(klass_name) ⇒ Object

Return a sibling klass for the given name

This is needed to construct some basic relationship between a User and Group model.

Parameters:

  • klass_name (String)

See Also:

  • {User{User#add_group}
  • {User{User.group_klass}
  • {Group{Group#add_user}
  • {Group{Group.user_klass}

238
239
240
241
# File 'lib/ad_dir/entry.rb', line 238

def self.sibling_klass(klass_name)
  composed_klass = "#{parent_name}::#{klass_name}"
  Object.const_get(composed_klass)
end

.tree_baseObject

Returns the tree_base of this class.

Returns:

  • String


193
194
195
# File 'lib/ad_dir/entry.rb', line 193

def self.tree_base
  @tree_base || nil
end

.tree_base=(value) ⇒ Object

Set the search base for a given class, e.g. the DevOps users in the Taka Tuka country.

class DevOpsUser
  self.tree_base = 'ou=DevOps,ou=taka tuka,dc=my,dc=company,dc=net'
end

This limits the :base DN when doing search operations on the AD.


187
188
189
# File 'lib/ad_dir/entry.rb', line 187

def self.tree_base=(value)
  @tree_base = value
end

.where(opts) ⇒ Array[Entry]

Returns an array of entries filtered by the conditions given in the arguments.

#where accepts conditions in the two following formats.

String Representation of LDAP Search Filter

A single string representing an filter as set out in RFC 4515

Examples: [http://tools.ietf.org/html/rfc4515#page-5]

AdDir::Entry.where('(samaccountname=jdoe)')
AdDir::Entry.where('(|(sn=*müller*)(givenname=*müller*))')

Hash

Pass in a hash of conditions. Each hash entry represents an equality condition where the key denotes the name of an attribute and the value its predicate. All conditions are logically combined by 'AND'.

Only equality conditions are allowed.

Examples:

AdDir::Entry.where(sn: 'Doe', givenname: 'John')

will search for entries having sn == 'Doe' && givenname == 'John'.

AdDir::Entry.where(sn: '*oe', mail: '@geo.uzh.ch')

will match all entries with sn ending with 'oe' and having a mail address in the geo.uzh.ch domain.

Parameters:

  • opts (String|Hash)

Returns:

  • (Array[Entry])

    | []


375
376
377
378
379
380
381
382
# File 'lib/ad_dir/entry.rb', line 375

def self.where(opts)
  if opts.instance_of?(Hash)
    filter = build_filter_from_hash(opts)
  else
    filter = Net::LDAP::Filter.from_rfc4515(opts)
  end
  search(filter: category_filter & filter).map { |e| from_ldap_entry(e) }
end

Instance Method Details

#[](attr_name) ⇒ Array<String>

Note:

This is a convenience method to speed up value retrieval, i.e. bypassing the method_missing way. The value returned is retrieved from the underlying Net::LDAP:Entry object and thus always wrapped in an Array.

Get the value of the attribute attr

Parameters:

  • attr_name (String, Symbol)

    The name of the attribute

Returns:

  • (Array<String>)

    value of attribute attr


523
524
525
# File 'lib/ad_dir/entry.rb', line 523

def [](attr_name)
  @ldap_entry[attr_name]
end

#[]=(name, value) ⇒ Object

Set the the attribute ''name'' to the value ''value''. If the attribute ''name'' exists its value is overwritten. If no attribute ''name'' exists a new attribute is created with the provided value.

Parameters:

  • name (String, Symbol)

    attribute name

  • value

    value of attribute

Returns:

  • value of attribute


534
535
536
# File 'lib/ad_dir/entry.rb', line 534

def []=(name, value)
  @ldap_entry[name] = value
end

#attribute_for_inspect(value) ⇒ Object

Returns an #inspect-like string for the value (based on it's class).

Copied that from activerecord method


649
650
651
652
653
654
655
656
657
658
659
660
661
662
# File 'lib/ad_dir/entry.rb', line 649

def attribute_for_inspect(value)
  # if value.is_a?(String) && value.length > 50
  #   "#{value[0, 50]}...".inspect
  if value.is_a?(String)
    string_inspect(value, 50)
  elsif value.is_a?(Date) || value.is_a?(Time)
    %("#{value.to_s(:db)}")
  elsif value.is_a?(Array) && value.size > 10
    inspected = value.first(10).inspect
    %(#{inspected[0...-1]}, ...])
  else
    value.inspect
  end
end

#attribute_present?(name) ⇒ Boolean

find out if attribute is present

Returns:

  • (Boolean)

630
631
632
# File 'lib/ad_dir/entry.rb', line 630

def attribute_present?(name)
  @ldap_entry.attribute_names.include?(name)
end

#attributesHash

Returns a hash with all attributes and (unwrapped) values. Any singled value array is unwrapped and the value itself is returned.

Returns:

  • (Hash)

See Also:


459
460
461
462
463
# File 'lib/ad_dir/entry.rb', line 459

def attributes
  @ldap_entry.attribute_names.each_with_object({}) do |key, hsh|
    hsh[key] = get_value(key)
  end
end

#changed?Boolean

compares the given hash with the internal @ldap_entry.attributes

Returns:

  • (Boolean)

569
570
571
# File 'lib/ad_dir/entry.rb', line 569

def changed?
  changes.size > 0
end

#changesObject

Returns a hash of changed attributes indicating their original and new values like attr => [original value, new value].

Examples:

user = AdDir::Entry.find('jdoe')
user[:sn]# => "Doe"

user[:sn] = 'Doey'
user.changes# => {:sn=>[["Doe"], ["Doey"]]}


# Adding a new attribute
user[:foo] = 'bar'
user.changes# => {:sn=>[["Doe"], ["Doey"]], :foo=>[nil, ["hopfen"]]}

589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
# File 'lib/ad_dir/entry.rb', line 589

def changes  #
  # Algorithm:
  #  1. Get a list of all relevant attributes
  #   'set' operation union which returns a new array by joining
  #   `@ldap_entry.attribute_names` with `persisted_attrs.keys`,
  #   excluding any duplicates.
  #  2. Loop and record for each key the 'old' (i.e. @persisted_attrs[key])
  #     and the 'new' (i.e. @ldap_entry[<key>]) value but only if they
  #     differ.

  return {} if @persisted_attrs.empty?
  (@ldap_entry.attribute_names | @persisted_attrs.keys)
    .each_with_object({}) do |key, memo|
    unless @ldap_entry[key] == @persisted_attrs[key]
      memo[key] = [@persisted_attrs[key], @ldap_entry[key]]
    end
  end
end

#connectionNet::LDAP

The Net::LDAP::Connection object used by this instance.

Returns:

  • (Net::LDAP)

448
449
450
# File 'lib/ad_dir/entry.rb', line 448

def connection
  self.class.connection
end

#destroyObject

Destroy the entry


609
610
611
# File 'lib/ad_dir/entry.rb', line 609

def destroy
  connection.delete(dn: dn)
end

#get_value(name) ⇒ Object

Retrieve the value of the given attribute.

Attribute values are always wrapped in an array, although most attributes are singled-valued.


507
508
509
510
511
512
# File 'lib/ad_dir/entry.rb', line 507

def get_value(name)
  val_arr = @ldap_entry[name]
  return val_arr if val_arr.empty?  #

  val_arr.size == 1 ? val_arr.first : val_arr
end

#inspectObject

Returns the cotent of the record as a nicely formatted string. (copied that from ActiveRecord)

See Also:

  • AdDir::Entry.{ActiveRecord{ActiveRecord::Base{ActiveRecord::Base#inspect}

637
638
639
640
641
642
# File 'lib/ad_dir/entry.rb', line 637

def inspect
  inspection = attribute_names.collect do |k|
    "#{k}: #{attribute_for_inspect(get_value(k))}"
  end.compact.join(', ')
  "#<#{self.class} #{inspection}>"
end

#modify(changes_hash) ⇒ Boolean

Modify attributes given as hash

Examples:

Modify the :sn (change) and :foo (add) attributes.

entry.modify({:sn=>[["Doe"], ["Doey"]],
              :foo=>[nil, ["hopfen"]]})

Returns:

  • (Boolean)

545
546
547
548
549
550
551
552
553
554
# File 'lib/ad_dir/entry.rb', line 545

def modify(changes_hash)
  ops     = prepare_modify_params(changes_hash)
  success = connection.modify(dn: dn, operations: ops)  #

  if success
    reload!
  else
    false
  end
end

#new_entry?Boolean

Returns true if the instance is not saved in the AD.

Returns:

  • (Boolean)

564
565
566
# File 'lib/ad_dir/entry.rb', line 564

def new_entry?
  @new_entry
end

#raw_attributesHash

Note:

The values are directly taken from the Net::LDAP::Entry object, i.e., each value is wrapped in an array.

Returns a hash with all attributes and (raw) values as present in the ActiveDirectory entry.

Returns:

  • (Hash)

See Also:


473
474
475
476
477
# File 'lib/ad_dir/entry.rb', line 473

def raw_attributes
  @ldap_entry.attribute_names.each_with_object({}) do |key, hsh|
    hsh[key] = @ldap_entry[key]
  end
end

#reload!Object

Reload the values from the ActiveDirectory and clear all current changes.

Returns:

  • true if successful

  • false if reloading failed.


617
618
619
620
621
622
623
624
625
626
627
# File 'lib/ad_dir/entry.rb', line 617

def reload!
  success = self.class._select_dn(dn)
  if success
    @new_entry       = false
    @ldap_entry      = success.first
    @persisted_attrs = raw_attributes.dup
    true
  else
    false
  end
end

#saveBoolean

Save the entry. If saving failed false is returned, otherwise true.

Returns:

  • (Boolean)

559
560
561
# File 'lib/ad_dir/entry.rb', line 559

def save
  create_or_update
end

#string_inspect(str, len) ⇒ Object

Shorten long strings for inspection


665
666
667
668
# File 'lib/ad_dir/entry.rb', line 665

def string_inspect(str, len)
  str = "#{str[0, len]}..." if str.length > len
  str.inspect
end