Class: LdapModel

Inherits:
Object
  • Object
show all
Defined in:
lib/ldapmodel/search.rb,
lib/ldapmodel/helpers.rb,
lib/ldapmodel/profiler.rb,
lib/ldapmodel/attr_conv.rb,
lib/ldapmodel/connection.rb,
lib/ldapmodel/class_store.rb

Overview

Connection management

Direct Known Subclasses

PuavoRest::BootServer, PuavoRest::ExternalFile, PuavoRest::ExternalService, PuavoRest::Group, PuavoRest::Host, PuavoRest::LegacyRole, PuavoRest::Organisation, PuavoRest::PrinterQueue, PuavoRest::SambaDomain, PuavoRest::SambaGroup, PuavoRest::School, PuavoRest::User

Defined Under Namespace

Classes: LdapHashError

Constant Summary

ESCAPES =

http://tools.ietf.org/html/rfc4515 lists these exceptions from UTF1 charset for filters. All of the following must be escaped in any normal string using a single backslash ('\') as escape.

{
  "\0" => '00', # NUL            = %x00 ; null character
  '*'  => '2A', # ASTERISK       = %x2A ; asterisk ("*")
  '('  => '28', # LPARENS        = %x28 ; left parenthesis ("(")
  ')'  => '29', # RPARENS        = %x29 ; right parenthesis (")")
  '\\' => '5C', # ESC            = %x5C ; esc (or backslash) ("\")
}
ESCAPE_RE =

Compiled character class regexp using the keys from the above hash.

Regexp.new(
  "[" +
  ESCAPES.keys.map { |e| Regexp.escape(e) }.join +
  "]"
)
PROF =
LdapSearchProfiler.new "ldapsearch"
KRB_LOCK =
Mutex.new
@@_class_store =

Store for ldap attribute mappings

{}

Instance Attribute Summary (collapse)

Class Method Summary (collapse)

Instance Method Summary (collapse)

Constructor Details

- (LdapModel) initialize(attrs = {}, options = {})

Returns a new instance of LdapModel



15
16
17
18
19
20
21
22
23
24
25
26
27
# File 'lib/ldapmodel/attr_conv.rb', line 15

def initialize(attrs={}, options={})
  @existing = !!options[:existing]
  @ldap_attr_store = options[:store] || {}

  if options[:serialize]
    @serialize_attrs = Set.new(options[:serialize].map{|a| a.to_sym})
  end

  @cache = {}
  @validation_errors = {}
  reset_pending
  update!(attrs)
end

Instance Attribute Details

- (Object) ldap_attr_store (readonly)

Returns the value of attribute ldap_attr_store



12
13
14
# File 'lib/ldapmodel/attr_conv.rb', line 12

def ldap_attr_store
  @ldap_attr_store
end

- (Object) serialize_attrs (readonly)

Returns the value of attribute serialize_attrs



13
14
15
# File 'lib/ldapmodel/attr_conv.rb', line 13

def serialize_attrs
  @serialize_attrs
end

Class Method Details

+ (Object) _class_store



4
5
6
# File 'lib/ldapmodel/class_store.rb', line 4

def self._class_store
  @@_class_store[self] ||= {}
end

+ (Object) after(*states, &hook_block) { ... }

Register block to be executed on the given states

Parameters:

  • *states (Symbol)

    :create, :update or :validate

  • hook_block (block)

    Hook block to be registered

Yields:

  • lol jee



43
44
45
46
47
48
# File 'lib/ldapmodel/attr_conv.rb', line 43

def self.after(*states, &hook_block)
  hooks[:after] ||= {}
  states.each do |state|
    (hooks[:after][state.to_sym] ||= []).push(hook_block)
  end
end

+ (Object) all(attrs = nil)

Return all ldap entries from the current base

See Also:



151
152
153
# File 'lib/ldapmodel/search.rb', line 151

def self.all(attrs=nil)
  filter(base_filter, attrs)
end

+ (Object) base_filter

When filtering models with filter this filter will be added to it automatically with AND operator (&). Usefull when there are multiple LdapModels is the same LDAP branch / base.

Override this in subclasses when needed.

Returns:

  • String



182
183
184
# File 'lib/ldapmodel/search.rb', line 182

def self.base_filter
  "(objectclass=*)"
end

+ (Object) before(*states, &hook_block) { ... }

Register block to be executed on the given states

Parameters:

  • *states (Symbol)

    :create, :update or :validate

  • hook_block (block)

    Hook block to be registered

Yields:

  • lol jee



35
36
37
38
39
40
# File 'lib/ldapmodel/attr_conv.rb', line 35

def self.before(*states, &hook_block)
  hooks[:before] ||= {}
  states.each do |state|
    (hooks[:before][state.to_sym] ||= []).push(hook_block)
  end
end

+ (Array<LdapModel>, LdapModel) by_attr(pretty_name, value, option = nil, attrs = nil)

Find model by it's mapped attribute. It's safe to call with user input since the value is escaped before ldap search.

Parameters:

  • pretty_name (Symbol)

    Mapped attribute

  • value (Object)

    Attribute value to match

  • option (Symbol) (defaults to: nil)

    Set to :multi to return an Array

Returns:



114
115
116
117
118
119
120
121
122
123
124
# File 'lib/ldapmodel/search.rb', line 114

def self.by_attr(pretty_name, value, option=nil, attrs=nil)
  ldap_attr = pretty2ldap[pretty_name.to_sym]

  if ldap_attr.nil?
    # Would compile to invalid ldap search filter. Throw early with human
    # readable error message
    raise "Invalid pretty attribute #{ pretty_name } for #{ self }"
  end

  by_ldap_attr(ldap_attr, value, option, attrs)
end

+ (Array<LdapModel>, LdapModel) by_attr!(attr, value, option = nil, attrs = nil)

Find model by it's mapped attribute. It's safe to call with user input since the value is escaped before ldap search.

Raises NotFound if no models were found

Parameters:

  • pretty_name (Symbol)

    Mapped attribute

  • value (Object)

    Attribute value to match

  • option (Symbol) (defaults to: nil)

    Set to :multi to return an Array

Returns:



129
130
131
# File 'lib/ldapmodel/search.rb', line 129

def self.by_attr!(attr, value, option=nil, attrs=nil)
  by_ldap_attr!(pretty2ldap[attr.to_sym], value, option, attrs)
end

+ (Object) by_dn(dn, attrs = nil)

Find model by dn attribute.

Parameters:

  • dn (String)

    dn string

  • attrs (Array) (defaults to: nil)

    Limit return model attributes

Returns:

  • LdapModel

See Also:



192
193
194
195
# File 'lib/ldapmodel/search.rb', line 192

def self.by_dn(dn, attrs=nil)
  res = raw_by_dn(dn, pretty_attrs_to_ldap(attrs))
  from_ldap_hash(res) if res
end

+ (Object) by_dn!(*args)

Find model by dn attribute.

Raises NotFound if no models were found

Parameters:

  • dn (String)

    dn string

  • attrs (Array)

    Limit return model attributes

Returns:

  • LdapModel

See Also:



200
201
202
203
204
205
206
# File 'lib/ldapmodel/search.rb', line 200

def self.by_dn!(*args)
  res = by_dn(*args)
  if not res
    raise NotFound, :user => "Cannot find #{ self.class } by dn: #{ args.first.inspect }"
  end
  res
end

+ (Object) by_dn_array(dns)

Get array of models by their dn attributes.

Nonexistent DNs are ignored.

Parameters:

  • dns (Array<String>)

    array of dn string



214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
# File 'lib/ldapmodel/search.rb', line 214

def self.by_dn_array(dns)
  timer = PROF.start

  res = Array(dns).map do |dn|
    begin
      by_dn(dn)
    rescue LDAP::ResultError
      # Ignore broken dn pointers
    end
  end.compact

  timer.stop("#{ self.name }#by_dn_array(<with #{ dns.size } items>)")

  res
end

+ (Object) by_id(id)

Find model by id attribute.

Returns:

  • LdapModel

See Also:



137
138
139
# File 'lib/ldapmodel/search.rb', line 137

def self.by_id(id)
  by_attr(:id, id)
end

+ (Object) by_id!(id)

Find model by id attribute.

Raises NotFound if no models were found.

Returns:

  • LdapModel

See Also:



144
145
146
# File 'lib/ldapmodel/search.rb', line 144

def self.by_id!(id)
  by_attr!(:id, id)
end

+ (Array<LdapModel>, LdapModel) by_ldap_attr(attr, value, option = nil, attrs = nil)

Filter models by a attribute.

Parameters:

  • attr (Symbol)

    LDAP name of the attribute

  • value (Object)
  • option (Symbol) (defaults to: nil)

    nil or :multi. If multi an array if returned

  • attrs (Array) (defaults to: nil)

    Limit search results and return values to these attributes

Returns:



82
83
84
85
86
87
88
89
90
91
92
# File 'lib/ldapmodel/search.rb', line 82

def self.by_ldap_attr(attr, value, option=nil, attrs=nil)
  custom_filter = "(#{ escape attr }=#{ escape value })"
  full_filter = "(&#{ base_filter }#{ custom_filter })"

  res = Array(filter(full_filter, attrs))
  if option == :multi
    res
  else
    res.first
  end
end

+ (Array<LdapModel>, LdapModel) by_ldap_attr!(attr, value, option = nil, attrs = nil)

Filter models by a attribute.

Raises NotFound if no models were found

Parameters:

  • attr (Symbol)

    LDAP name of the attribute

  • value (Object)
  • option (Symbol) (defaults to: nil)

    nil or :multi. If multi an array if returned

  • attrs (Array) (defaults to: nil)

    Limit search results and return values to these attributes

Returns:



96
97
98
99
100
101
102
103
104
105
# File 'lib/ldapmodel/search.rb', line 96

def self.by_ldap_attr!(attr, value, option=nil, attrs=nil)
   res = by_ldap_attr(attr, value, option, attrs)
   if Array(res).empty?
    raise(
      NotFound,
      "Cannot find #{ self } by #{ attr }=#{ value }"
    )
   end
   res
end

+ (Object) callable_from_instance(method)



3
4
5
6
7
8
# File 'lib/ldapmodel/helpers.rb', line 3

def self.callable_from_instance(method)
  klass = self
  define_method method do |*args|
    klass.send(method, *args)
  end
end

+ (Object) class_store(name)

Like double at sign attributes (@@foo) but they are not shared between subclasses

class Foo
  class_store :bar
  def get_baz
    bar[:baz]
  end
end

Foo.bar[:baz] = 1
assert Foo.new.get_baz == 1

Parameters:

  • name (Symbol)

    accessor name



22
23
24
25
26
27
28
29
30
# File 'lib/ldapmodel/class_store.rb', line 22

def self.class_store(name)
  _class_store[name] = {}
  define_method(name) do
    self.class._class_store[name]
  end
  define_singleton_method(name) do
    _class_store[name]
  end
end

+ (Object) clear_setup



152
153
154
# File 'lib/ldapmodel/connection.rb', line 152

def self.clear_setup
  self.settings = nil
end

+ (Object) computed_attr(attr, serialize_name = nil)

A method that will be executed and added to to_hash and to_json conversions of this models

Parameters:

  • attr (Symbol)

    method to be called in serializations

  • serialization_name (Symbol)

    Change the name in serialization



135
136
137
# File 'lib/ldapmodel/attr_conv.rb', line 135

def self.computed_attr(attr, serialize_name=nil)
  computed_attributes[attr.to_sym] = serialize_name || attr
end

+ (Object) connection



126
127
128
129
130
131
132
133
# File 'lib/ldapmodel/connection.rb', line 126

def self.connection
  if conn = settings[:credentials_cache][:current_connection]
    return conn
  end
  if settings[:credentials]
    settings[:credentials_cache][:current_connection] = create_connection
  end
end

+ (Object) create_connection

Create connection for LdapModel



54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
# File 'lib/ldapmodel/connection.rb', line 54

def self.create_connection
  raise "Cannot create connection without credentials" if settings[:credentials].nil?
  credentials = settings[:credentials]
  conn = nil

  if credentials[:kerberos]
    return sasl_bind(credentials[:kerberos])
  end

  if credentials[:dn].to_s.strip.empty?
    raise BadCredentials, "DN missing" if not credentials[:dn]
  end

  if credentials[:password].to_s.strip.empty?
    raise BadCredentials, "Password missing" if not credentials[:password]
  end

  begin
      conn = dn_bind(credentials[:dn], credentials[:password])
  rescue LDAP::ResultError => err
    if err.message == "Invalid credentials"
      raise BadCredentials, {
        :msg => "Invalid credentials (dn/pw)",
        :meta => {
          :dn => credentials[:dn],
          :username => credentials[:username]
      }}
    else
      raise LdapError, "Other LDAP error: #{ err.message }"
    end
  end

  if conn.nil?
      raise LdapError, "ldap bind returned nil instead of connection"
  end

  return conn
end

+ (Proc) create_filter_lambda(pretty_attr, &convert)

Return a lambda which converts ldap field value to ldap search filter

Example:

l = create_filter_lambda(:username) { |value| "*#{ v }*" }
filter = l.call("foo")
"(uid=*foo*)"

Parameters:

  • pretty_attr (Symbol)
  • &convert (Block)

Returns:

  • (Proc)


273
274
275
276
277
278
279
280
281
# File 'lib/ldapmodel/search.rb', line 273

def self.create_filter_lambda(pretty_attr, &convert)
  if convert.nil?
    convert = lambda { |v| "*#{ v }*" }
  end

  ldap_attr = pretty2ldap[pretty_attr.to_sym]
  raise "Unknown pretty attribute '#{ pretty_attr }' for #{ self }" if not ldap_attr
  lambda { |keyword| "(#{ ldap_attr }=#{ convert.call(escape(keyword)) })" }
end

+ (Object) dn_bind(dn, pw)

Do LDAP bind with dn and password

Parameters:

  • dn (String)
  • password (String)


45
46
47
48
49
50
51
# File 'lib/ldapmodel/connection.rb', line 45

def self.dn_bind(dn, pw)
  conn = LDAP::Conn.new(CONFIG["ldap"])
  conn.set_option(LDAP::LDAP_OPT_PROTOCOL_VERSION, 3)
  conn.start_tls
  conn.bind(dn, pw)
  conn
end

+ (Object) escape(string)

Escape unsafe user input for safe LDAP filter use



41
42
43
# File 'lib/ldapmodel/helpers.rb', line 41

def self.escape(string)
  string.to_s.gsub(ESCAPE_RE) { |char| "\\" + ESCAPES[char] }
end

+ (Object) filter(filter_, attrs = nil)

Do LDAP search with a filter. ldap_base will be used as the base.

Parameters:

  • filter_ (String)


66
67
68
69
70
71
# File 'lib/ldapmodel/search.rb', line 66

def self.filter(filter_, attrs=nil)
  ldap_attributes = pretty_attrs_to_ldap(attrs)
  raw_filter(ldap_base, filter_, ldap_attributes).map! do |entry|
    from_ldap_hash(entry, attrs)
  end
end

+ (Object) from_ldap_hash(ldap_attrs, serialize_attrs = nil)



10
11
12
# File 'lib/ldapmodel/helpers.rb', line 10

def self.from_ldap_hash(ldap_attrs, serialize_attrs=nil)
  new({}, :serialize => serialize_attrs, :existing => true).ldap_merge!(ldap_attrs)
end

+ (Object) inherited(subclass)

copy attributes to inherited subclasses

Parameters:

  • subclass (subclass)


34
35
36
37
38
39
# File 'lib/ldapmodel/class_store.rb', line 34

def self.inherited(subclass)
  _class_store.keys.each do |k|
    subclass._class_store[k] ||= {}
    subclass._class_store[k].merge!(_class_store[k])
  end
end

+ (Object) is_dn(s)



14
15
16
17
18
# File 'lib/ldapmodel/helpers.rb', line 14

def self.is_dn(s)
  # Could be slightly better I think :)
  # but usernames should have no commas or equal signs
  s && s.include?(",") && s.include?("=")
end

+ (Boolean) is_not_found?(err)

Returns:

  • (Boolean)


210
211
212
# File 'lib/ldapmodel/connection.rb', line 210

def self.is_not_found?(err)
  !!(err && err.class == LDAP::ResultError && err.message == "No such object")
end

+ (Array<Symbol>) ldap_attrs

Returns LDAP attributes that will be converted

Returns:

  • (Array<Symbol>)

    LDAP attributes that will be converted



341
342
343
# File 'lib/ldapmodel/attr_conv.rb', line 341

def self.ldap_attrs
  ldap2pretty.keys
end

+ (Object) ldap_base

Override in a subclass

Returns:

  • String



8
9
10
# File 'lib/ldapmodel/search.rb', line 8

def self.ldap_base
  raise "ldap_base is not implemented for #{ self.name }"
end

+ (Object) ldap_map(ldap_name, pretty_name, options = nil) {|value| ... }

Define conversion between LDAP attribute and the JSON attribute. This will create a getter method named by the pretty_name param

Parameters:

  • ldap_name (Symbol)

    LDAP attribute to transform

  • pretty_name (Symbol)

    Pretty name for the attribute which will used the access the value from the model instance

  • options (Hash, LdapConverters::Base) (defaults to: nil)

    A LdapConverters::Base subclass or an options hash

  • transform (Block)

    Block used to transform the attribute value when reading

Options Hash (options):

Yield Parameters:

  • value (Array, Object)

    Raw value from ldap. Usually an Array



68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
# File 'lib/ldapmodel/attr_conv.rb', line 68

def self.ldap_map(ldap_name, pretty_name, options=nil, &transform_block)
  pretty_name = pretty_name.to_sym
  ldap_name = ldap_name.to_sym
  pretty2ldap[pretty_name] = ldap_name
  ldap2pretty[ldap_name] = pretty_name

  mapping_string = "#{ self }.ldap_map(:#{ ldap_name }, :#{ pretty_name })"

  if ![NilClass, Class, Hash].include?(options.class)
    raise "#{mapping_string} has invalid options argument: #{ options.inspect }"
  end


  transform = LdapConverters::SingleValue
  default_value = nil

  if options.class == Hash
    transform = options[:transform] if options[:transform]
    default_value = options[:default]
  elsif options
    transform = options
  end

  if transform_block && transform.class != Class
    raise "#{mapping_string} cannot use both transform instance and transform block"
  end

  if transform_block && transform.class == Class
    # Inherit the transform class and override the read method with the given
    # block
    transform = Class.new(transform)
    transform.send(:define_method, :read, &transform_block)
  end

  attr_options[pretty_name] = {
    :default => default_value,
    :transform => transform
  }

  # Create simple getter for the attribute if no custom one is defined
  if not method_defined?(pretty_name)
    define_method pretty_name do
      get_own(pretty_name)
    end
  end

  setter_method = (pretty_name.to_s + "=").to_sym
  if not method_defined?(setter_method)
    define_method setter_method do |value|
      error = transform.new(self).validate(value)
      if error
        add_validation_error(pretty_name, error[:code], error[:message])
        # Raise type check validation error early here because later it can
        # cause more weird errors during hooks and validation
        assert_validation
      else
        write_raw(ldap_name, transform.new(self).write(value))
      end
    end
  end
end

+ (Object) ldap_op(method, *args, &block)

ruby-ldap operation wrapper

The raw ruby-ldap gives very little information on errors. So wrap it and add a lot more details to the error wrapper.

Log start and end of the operation to syslog. It should make it lot easier to see which slapd log messages are related to the ruby-ldap operation. Slapd logs levels must be raised in order the take advantage of this.

Convert LDAP::ResultError: "No such object" errors to nil return values to make it consistent with every other not found error.

Each operation are given an UUID so the user response, puavo-rest log and syslog logs can be combined



172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
# File 'lib/ldapmodel/connection.rb', line 172

def self.ldap_op(method, *args, &block)
  res = nil
  ldap_op_uuid = (0...25).map{('a'..'z').to_a[rand(10)] }.join

  Syslog.log(Syslog::LOG_NOTICE, "START(#{ ldap_op_uuid })> #{ connection.class }##{ method }")
  err = nil

  begin
    res = connection.send(method, *args, &block)
  rescue Exception => _err
    err = _err

    # not really an error. Just convert to nil response
    return if is_not_found?(err)

    message = "\n#{ err.class }: #{ err }\n\n    was raised by\n\n"
    message += "UUID: #{ ldap_op_uuid }\n"
    message += "#{ connection.class }##{ method }(#{ args.map{|a| a.inspect }.join(", ")})\n"

    raise LdapError, {
      :user => "#{ err.class }: #{ err.message } (LDAP OP UUID: #{ ldap_op_uuid })",
      :message => message,
      :op_uuid => ldap_op_uuid,
      :original_error => err,
      :args => args,
      :method => method
    }
  ensure
    end_msg = "OK"
    if err
      end_msg = " ERROR: #{ err.class } #{ err.message }"
    end
    Syslog.log(Syslog::LOG_NOTICE, "END(#{ ldap_op_uuid })> #{ end_msg }")
  end

  res
end

+ (Object) organisation

Get configured organisation

Returns:

  • String



144
145
146
147
148
149
150
# File 'lib/ldapmodel/connection.rb', line 144

def self.organisation
  if settings[:organisation].nil?
    raise BadInput, :user => "Cannot configure organisation for this request"
  else
    settings[:organisation]
  end
end

+ (Boolean) organisation?

Return true if an organisation is configured

Returns:

  • (Boolean)

    PuavoRest::Organisation



138
139
140
# File 'lib/ldapmodel/connection.rb', line 138

def self.organisation?
  !!settings[:organisation]
end

+ (Object) pretty_attrs_to_ldap(attrs = nil)

Convert array of pretty names to ldap attribute names

Parameters:

  • attrs (Array<Symbol>) (defaults to: nil)


55
56
57
58
59
60
# File 'lib/ldapmodel/search.rb', line 55

def self.pretty_attrs_to_ldap(attrs=nil)
  if attrs
    attrs = attrs.map{|a| pretty2ldap[a.to_sym]}.compact
  end
  attrs
end

+ (Object) raw_by_dn(dn, attributes = nil)

Find any ldap entry by dn

Parameters:

  • dn (String)
  • attributes (Array of Strings) (defaults to: nil)


159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
# File 'lib/ldapmodel/search.rb', line 159

def self.raw_by_dn(dn, attributes=nil)
  attributes ||= ldap_attrs.map{ |a| a.to_s }

  timer = PROF.start

  if connection.nil?
    raise "Connection is not setup!"
  end

  res = nil
  raw_filter(dn, "(objectclass=*)", attributes) do |entry|
    res = entry.to_hash
    break
  end
  res
end

+ (Array) raw_filter(base, filter, attributes = nil, &block)

LDAP::LDAP_SCOPE_SUBTREE filter search for #ldap_base

Parameters:

  • base (String)

    LDAP base

  • filter (String)

    LDAP filter

  • attributes (Array) (defaults to: nil)

    Limit search results and return values to these attributes

Returns:

  • (Array)

See Also:



19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# File 'lib/ldapmodel/search.rb', line 19

def self.raw_filter(base, filter, attributes=nil, &block)
  res = []
  attributes ||= ldap_attrs

  if not connection
    raise "Cannot search without a connection"
  end

  timer = PROF.start

  if block.nil?
    block = lambda do |entry|
      res.push(entry.to_hash) if entry.dn != base
    end
  end

  begin
    ldap_op(
      :search,
      base,
      LDAP::LDAP_SCOPE_SUBTREE,
      filter,
      attributes.map{ |a| a.to_s },
      &block
    )
  ensure
    timer.stop("#{ self.name }#raw_filter(#{ filter.inspect }) base:#{ base } attributes:#{ attributes.inspect } found #{ res.size } items")
    PROF.count(timer)
  end


  res
end

+ (Object) sasl_bind(ticket)

Do LDAP sasl bind with a kerberos ticket



12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# File 'lib/ldapmodel/connection.rb', line 12

def self.sasl_bind(ticket)
  conn = LDAP::Conn.new(CONFIG["ldap"])
  conn.set_option(LDAP::LDAP_OPT_PROTOCOL_VERSION, 3)
  conn.sasl_quiet = true
  conn.start_tls
  KRB_LOCK.synchronize do
    begin
      kg = Krb5Gssapi.new(CONFIG["fqdn"], CONFIG["keytab"])
      kg.copy_ticket(ticket)
      username, org = kg.display_name.split("@")
      settings[:credentials][:username] = username
      LdapModel.setup(:organisation => PuavoRest::Organisation.by_domain(org.downcase))
      conn.sasl_bind('', 'GSSAPI')
    rescue GSSAPI::GssApiError => err
      if err.message.match(/Clock skew too great/)
        raise KerberosError, :user => "Your clock is messed up"
      else
        raise KerberosError, :user => err.message
      end
    rescue Krb5Gssapi::NoDelegation => err
      raise KerberosError, :user =>
        "Credentials are not delegated! '--delegation always' missing?"
    ensure
      kg.clean_up
    end
  end
  conn
end

+ (Object) search(keywords)

Search for models using keywords

keywords can be a string of + or space separated keywords or an array of keywords. Those models are returned which have a match for all the keywords.

Each model using this method must implement a search_filters method which returns an array of lambdas (Proc) which generate the approciate ldap search filters. See create_filter_lambda



245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
# File 'lib/ldapmodel/search.rb', line 245

def self.search(keywords)
  if keywords.kind_of?(String)
    keywords = keywords.gsub("+", " ").split(" ")
  end

  return [] if keywords.nil?
  return [] if keywords.empty?

  filter_string = "(&" + keywords.map do |keyword|
    "(|" + search_filters.map do |sf|
      sf.call(keyword)
    end.join("") + ")"
  end.join("") + ")"

  filter(filter_string)
end

+ (Array<Proc>) search_filters

Returns:

  • (Array<Proc>)

See Also:



232
233
234
# File 'lib/ldapmodel/search.rb', line 232

def self.search_filters
  raise "search_filters not implemented for #{ self }"
end

+ (Object) settings



93
94
95
# File 'lib/ldapmodel/connection.rb', line 93

def self.settings
  Thread.current[:ldap_hash_settings] || { :credentials_cache => {} }
end

+ (Object) settings=(settings)



97
98
99
# File 'lib/ldapmodel/connection.rb', line 97

def self.settings=(settings)
  Thread.current[:ldap_hash_settings] = settings
end

+ (Object) setup(opts, &block)

Configure LDAP bind for LdapModel

Parameters:

  • opts (Hash)
  • &block (Block)

    If block is passed the configuration is active only during the exection of the block

Options Hash (opts):

  • :credentials (Hash)

    Credentials for LDAP bind. Must have :dn and :password or :kerberos.

  • :organisation (PuavoRest::Organisation)
  • :rest_root (Hash)

    puavo-rest mount point url



111
112
113
114
115
116
117
118
119
120
121
122
123
124
# File 'lib/ldapmodel/connection.rb', line 111

def self.setup(opts, &block)
  prev = self.settings
  self.settings = prev.merge(opts)

  if opts[:credentials]
    self.settings[:credentials_cache] = {}
  end

  if block
    res = block.call
    self.settings = prev
  end
  res
end

+ (Object) skip_serialize(*attrs)

Skip this attribute(s) from serializations such as to_hash and to_json

Parameters:

  • attrs (Symbol, Array<Symbol>)


142
143
144
# File 'lib/ldapmodel/attr_conv.rb', line 142

def self.skip_serialize(*attrs)
  attrs.each { |a| skip_serialize_attrs[a.to_sym] = true }
end

Instance Method Details

- (Object) [](pretty_name)

Deprecated.


325
326
327
# File 'lib/ldapmodel/attr_conv.rb', line 325

def [](pretty_name)
  send(pretty_name.to_sym)
end

- (Object) []=(pretty_name, value)

Deprecated.


330
331
332
# File 'lib/ldapmodel/attr_conv.rb', line 330

def []=(pretty_name, value)
  set(pretty_name, value)
end

- (Object) add(pretty_name, value)

Append value to LdapConverters::ArrayValue attribute. Value is persisted on the next #save! call

Parameters:

  • pretty_name (Symbol)

    Pretty name of the attribute

  • value (Object)

    Value to be appended to the attribute



238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
# File 'lib/ldapmodel/attr_conv.rb', line 238

def add(pretty_name, value)
  pretty_name = pretty_name.to_sym
  ldap_name = pretty2ldap[pretty_name]
  transform = attr_options[pretty_name][:transform]

  # if not LdapConverters::ArrayValue or subclass of it
  if !(transform <= LdapConverters::ArrayValue)
    raise "add! can be called only on LdapConverters::ArrayValue values. Not #{ transform }"
  end

  if new?
    raise "Cannot call add on new models. Just set the attribute directly"
  end

  if @previous_values[pretty_name].nil?
    @previous_values[pretty_name] = send(pretty_name)
  end

  value = transform.new(self).write(value)
  @pending_mods.push(LDAP::Mod.new(LDAP::LDAP_MOD_ADD, ldap_name.to_s, value))
  @cache[pretty_name] = nil
  current_val = @ldap_attr_store[ldap_name.to_sym]
  @ldap_attr_store[ldap_name.to_sym] = Array(current_val) + value
end

- (Object) add_validation_error(attr, code, message)

Add validation error. Error will be raised on the next #save! call

Parameters:

  • attr (Symbol)

    Attribute name this error relates to

  • code (Symbol)

    Unique symbol for this name

  • message (String)

    Human readable message for this error



307
308
309
310
311
312
313
314
315
# File 'lib/ldapmodel/attr_conv.rb', line 307

def add_validation_error(attr, code, message)
  current = @validation_errors[attr.to_sym] ||= []
  current = current.select{|err| err[:code] != code}
  current.push(
    :code => code,
    :message => message
  )
  current = @validation_errors[attr.to_sym] = current
end

- (Object) as_json

Returns Object

Returns:

  • Object



416
417
418
# File 'lib/ldapmodel/attr_conv.rb', line 416

def as_json(*)
  to_hash
end

- (Object) assert_validation(message = nil)

Raises ValidationError if #add_validation_error was called at least once

Parameters:

  • message (String) (defaults to: nil)

    Optional custom error message

Raises:



448
449
450
451
452
453
454
455
456
457
458
# File 'lib/ldapmodel/attr_conv.rb', line 448

def assert_validation(message=nil)
  return if @validation_errors.empty?
  errors = @validation_errors
  @validation_errors = {}
  raise ValidationError, {
    :message => message || "Validation error",
    :className => self.class.name,
    :dn => dn,
    :invalid_attributes => errors
  }
end

- (Boolean) changed?(pretty_name)

Returns true if this value is going to be written to ldap on next #save! call

Parameters:

  • pretty_name (Symbol)

Returns:

  • (Boolean)


219
220
221
222
223
224
225
226
227
228
229
230
231
# File 'lib/ldapmodel/attr_conv.rb', line 219

def changed?(pretty_name)
  pretty_name = pretty_name.to_sym
  ldap_name = pretty2ldap[pretty_name]
  if !respond_to?(pretty_name)
    raise NoMethodError, "undefined method `#{ pretty_name }' for #{ self.class }"
  end

  return true if new?
  return false if !@previous_values.key?(ldap_name)
  current_val = send(pretty_name)
  prev_val = @previous_values[pretty_name]
  return current_val != prev_val
end

- (Object) create!(_dn = nil)

Save new model to LDAP

Parameters:

  • _dn (String) (defaults to: nil)

    Set to use custom dn



265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
# File 'lib/ldapmodel/attr_conv.rb', line 265

def create!(_dn=nil)
  if @existing
    raise "Cannot call create! on existing model"
  end

  validate!("Creating")

  run_hook :before, :create

  _dn = dn if _dn.nil?
  mods = @pending_mods.select do |mod|
    mod.mod_type != "dn"
  end

  res = self.class.ldap_op(:add, dn, mods)
  reset_pending
  @existing = true

  run_hook :after, :create

  res
end

- (Boolean) dirty?

Returns true when the model has unsaved changes in attributes

Returns:

  • (Boolean)


320
321
322
# File 'lib/ldapmodel/attr_conv.rb', line 320

def dirty?
  !@pending_mods.empty?
end

- (Boolean) empty?

Returns trur when the model has no values

Returns:

  • (Boolean)


336
337
338
# File 'lib/ldapmodel/attr_conv.rb', line 336

def empty?
  @ldap_attr_store.empty?
end

- (Object) get_own(pretty_name)

Parameters:

  • pretty_name (Symbol)

    Get the transformed attribute value of this model

Returns:

  • (Object)


148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
# File 'lib/ldapmodel/attr_conv.rb', line 148

def get_own(pretty_name)
  pretty_name = pretty_name.to_sym
  return @cache[pretty_name] if not @cache[pretty_name].nil?

  ldap_name = pretty2ldap[pretty_name]
  default_value = attr_options[pretty_name][:default]

  value = Array(@ldap_attr_store[ldap_name])

  # String values in our LDAP are always UTF-8
  value = value.map do |item|
    if item.respond_to?(:force_encoding)
      item.force_encoding("UTF-8")
    else
      item
    end
  end

  if Array(value).empty? && !default_value.nil?
    return default_value
  end

  @cache[pretty_name] = transform(pretty_name, :read, value)
end

- (Object) get_raw(ldap_name)

Parameters:

  • ldap_name (Symbol)

    Get raw ldap value by ldap attribute name

Returns:

  • (Object)


193
194
195
# File 'lib/ldapmodel/attr_conv.rb', line 193

def get_raw(ldap_name)
  @ldap_attr_store[ldap_name.to_sym]
end

- (Object) ldap_merge!(hash)

Merge hash of ldap attributes to this model

Parameters:



361
362
363
364
365
366
# File 'lib/ldapmodel/attr_conv.rb', line 361

def ldap_merge!(hash)
  hash.each do |ldap_name, value|
    ldap_set(ldap_name, value)
  end
  self
end

- (Object) ldap_set(ldap_name, value)

Set attribute using the original ldap attribute

Parameters:

  • ldap_name (Symbol)
  • value (Object)


349
350
351
352
# File 'lib/ldapmodel/attr_conv.rb', line 349

def ldap_set(ldap_name, value)
  return if ldap2pretty[ldap_name.to_sym].nil?
  @ldap_attr_store[ldap_name.to_sym] = value
end


214
215
216
# File 'lib/ldapmodel/connection.rb', line 214

def link(path)
  self.class.settings[:rest_root] + path
end

- (Object) merge(other)

Merge value from other LdapModel to this one

Parameters:



370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
# File 'lib/ldapmodel/attr_conv.rb', line 370

def merge(other)
  h = nil
  _serialize_attrs = nil

  if other.kind_of?(self.class)
    h = other.ldap_attr_store
    _serialize_attrs = other.serialize_attrs
  else
    h = other # Assume something Hash like
  end

  _ldap_attrs = @ldap_attr_store.dup
  h.each do |pretty_name, value|
    _ldap_attrs[pretty2ldap[pretty_name.to_sym]] = value
  end

  self.class.new({}, {
    :serialize => _serialize_attrs,
    :store => _ldap_attrs
  })
end

- (Boolean) new?

Returns true if the model is present in LDAP

Returns:

  • (Boolean)


52
53
54
# File 'lib/ldapmodel/attr_conv.rb', line 52

def new?
  !@existing
end

- (Object) object_model



426
427
428
# File 'lib/ldapmodel/attr_conv.rb', line 426

def object_model
  self.class.to_s
end

- (Object) save!

Save changes to LDAP



289
290
291
292
293
294
295
296
297
298
299
300
# File 'lib/ldapmodel/attr_conv.rb', line 289

def save!
  return create! if !@existing

  validate!("Updating")

  run_hook :before, :update
  res = self.class.ldap_op(:modify, dn, @pending_mods)
  reset_pending
  run_hook :after, :update

  res
end

- (Object) set(pretty_name, value)

Deprecated.


355
356
357
# File 'lib/ldapmodel/attr_conv.rb', line 355

def set(pretty_name, value)
  @cache[pretty_name.to_sym] = value
end

- (Object) to_hash

Convert model to Hash

Returns:

  • Hash



394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
# File 'lib/ldapmodel/attr_conv.rb', line 394

def to_hash
  h = {}
  pretty2ldap.each do |pretty_name, _|
    next if @serialize_attrs && !@serialize_attrs.include?(pretty_name)

    if !skip_serialize_attrs[pretty_name.to_sym]
      h[pretty_name.to_s] = send(pretty_name)
    end
  end

  computed_attributes.each do |method_name, serialize_name|
    next if @serialize_attrs && !@serialize_attrs.include?(serialize_name)
    h[serialize_name.to_s] = send(method_name)
  end
  h
end

- (Object) to_json

Returns String

Returns:

  • String



421
422
423
# File 'lib/ldapmodel/attr_conv.rb', line 421

def to_json(*)
  as_json.to_json
end

- (Object) to_ldap_hash



411
412
413
# File 'lib/ldapmodel/attr_conv.rb', line 411

def to_ldap_hash
  @ldap_attr_store.dup
end

- (Object) transform(pretty_name, method, value)

Transform value using the attribute transformer

Parameters:

  • pretty_name (Symbol)

    Pretty name of the attribute

  • method (Symbol)

    :read or :write

  • value (Object)

    value to transform



179
180
181
182
# File 'lib/ldapmodel/attr_conv.rb', line 179

def transform(pretty_name, method, value)
  transformer = attr_options[pretty_name][:transform]
  transformer.new(self).send(method, value)
end

- (Object) update!(h)

Parameters:

  • h (Hash)

    Update model attributes from hash



185
186
187
188
189
# File 'lib/ldapmodel/attr_conv.rb', line 185

def update!(h)
  h.each do |k,v|
    send((k.to_s + "=").to_sym, v)
  end
end

- (Object) validate

Validation method called before saving. Override it and call #add_validation_error for any errors



432
433
# File 'lib/ldapmodel/attr_conv.rb', line 432

def validate
end

- (Object) validate!(message = nil)

Run hooks and validations. May raise ValidationError

Parameters:

  • message (String) (defaults to: nil)

    Optional custom error message



462
463
464
465
466
467
# File 'lib/ldapmodel/attr_conv.rb', line 462

def validate!(message=nil)
  run_hook :before, :validate
  validate
  assert_validation(message)
  run_hook :after, :validate
end

- (Object) validate_unique(pretty_name)

Validate uniqueness of an attribute

Parameters:

  • pretty_name (Symbol)


437
438
439
440
441
442
443
444
# File 'lib/ldapmodel/attr_conv.rb', line 437

def validate_unique(pretty_name)
  return if !changed?(pretty_name)
  ldap_name = pretty2ldap[pretty_name.to_sym]
  val = Array(get_raw(ldap_name)).first
  if self.class.by_attr(pretty_name, val)
    add_validation_error(pretty_name, "#{ pretty_name.to_s }_not_unique".to_sym, "#{ pretty_name }=#{ val } is not unique")
  end
end

- (Object) write_raw(ldap_name, new_val)

Write raw ldap value

Parameters:

  • ldap_name (Symbol)

    LDAP attribute name

  • new_val (Object)

    Value to be written



201
202
203
204
205
206
207
208
209
210
211
212
213
214
# File 'lib/ldapmodel/attr_conv.rb', line 201

def write_raw(ldap_name, new_val)
  ldap_name = ldap_name.to_sym

  pretty_name = ldap2pretty[ldap_name]
  if pretty_name
    @previous_values[pretty_name] = send(pretty_name)
    @cache[pretty_name] = nil
  end

  @ldap_attr_store[ldap_name] = new_val
  @pending_mods.push(LDAP::Mod.new(LDAP::LDAP_MOD_REPLACE, ldap_name.to_s, new_val))

  new_val
end