lom 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 383b6cdf0a55888e40558ca5b24f46a7f5e5d033f27fc873e4625c42b385c571
4
+ data.tar.gz: cef1eef2f7ee9b2b39118e9f54b2d15f1ba5d35518ed04e98cb09a047cbbb34e
5
+ SHA512:
6
+ metadata.gz: dd9eab41714ae145477e2756300b0041dd2d0a12f90485a3997e95266bbb121a33e8a3ed3660d8fd14e7372fd0cfa0fca6da6f800f61599a1c995da221b67b4f
7
+ data.tar.gz: f435226cb86088cfa48db588b105396b79fc0172eec4a967168b507dffb1039f201f6d7261d371edf63883c7fe519ca0d6167c649259f282a7d38e592ee929e1
data/README.md ADDED
@@ -0,0 +1,152 @@
1
+ LDAP Object Mapper
2
+ ==================
3
+
4
+ Allow to map LDAP object to ruby object.
5
+
6
+ It is best used with dry-struct and dry-struct-setters libraries
7
+
8
+
9
+ Examples
10
+ ========
11
+
12
+ ~~~ruby
13
+ require 'net/ldap'
14
+ require 'lom/ldap'
15
+
16
+ using LOM::LDAP::Extensions
17
+
18
+ # Define LDAP handler used by LOM
19
+ LH = Net::LDAP.connect('ldap://127.0.0.1')
20
+ LH.auth 'uid=root,ou=Admins,dc=example,dc=com', 'foobar'
21
+ ~~~
22
+
23
+ ~~~ruby
24
+ # Defining mapping between LDAP and ruby using Dry::Struct
25
+ #
26
+ class User < Dry::Struct
27
+ include Dry::Struct::Setters
28
+ using LOM::LDAP::Extensions
29
+
30
+ ADMINS_BRANCH = 'ou=Admins,dc=example,dc=com'
31
+ TEAMS_BRANCH = 'ou=Team,dc=example,dc=com'
32
+
33
+ #
34
+ # Defining LDAP mapping
35
+ #
36
+ extend LOM::Mapper
37
+
38
+ ldap_branch "ou=People,dc=example,dc=com"
39
+ ldap_filter '(objectClass=inetOrgPerson)'
40
+ ldap_attrs '*', '+'
41
+ ldap_prefix :uid
42
+
43
+ ldap_from do
44
+ {
45
+ :firstname => first(:givenName, String ),
46
+ :lastname => first(:sn, String ),
47
+ :email => first(:mail, String ),
48
+ :homepage => first(:labeledURI, String ),
49
+ :address => first(:postalAddress, String ),
50
+ :title => first(:title, String ),
51
+ :type => all(:objectClass, String )
52
+ .map(&:downcase)
53
+ .include?('posixaccount') ? :full : :minimal,
54
+ :login => first(:uid, String ),
55
+ :password => nil,
56
+ :managers => all(:manager, String )
57
+ .map {|m| User.ldap_dn_to_id(m) },
58
+ :locked => first(:pwdAccountLockedTime, Time ),
59
+ :uid => first(:uidNumber, Integer ),
60
+ :gid => first(:gidNumber, Integer ),
61
+ :home => first(:homeDirectory, String ),
62
+ :teams => all(:memberOf, String ).map{|m|
63
+ LOM.id_from_dn(m, TEAMS_BRANCH, :cn)
64
+ }.compact,
65
+ }.compact
66
+ end
67
+
68
+ ldap_to do
69
+ oclass = [ 'inetOrgPerson' ]
70
+ if type == :full
71
+ oclass += [ 'posixAccount', 'sambaSamAccount', 'pwdPolicy' ]
72
+ { :gecos => fullname,
73
+ :loginShell => '/bin/bash'
74
+ }
75
+ end
76
+
77
+ { :givenName => firstname,
78
+ :sn => lastname,
79
+ :cn => fullname,
80
+ :mail => email,
81
+ :labeledURI => homepage,
82
+ :postalAddress => address,
83
+ :title => title,
84
+ :uid => login,
85
+ :manager => managers.map {|m| User.ldap_dn_from_id(m) },
86
+ :pwdAccountLockedTime => locked,
87
+ :uidNumber => uid,
88
+ :gidNumber => gid,
89
+ :homeDirectory => home.to_s,
90
+ }
91
+ end
92
+
93
+ ldap_list :locked, ->(predicate=true) do
94
+ Filtered.exists(:pwdAccountLockedTime, predicate: predicate)
95
+ end
96
+
97
+ ldap_list :manager, ->(manager) do
98
+ Filtered.has(:manager, manager) {|m|
99
+ case m
100
+ when true, nil then Filtered::ANY
101
+ when false, :none then Filtered::NONE
102
+ else User.ldap_dn_from_id(m.to_str)
103
+ end
104
+ }
105
+ end
106
+
107
+
108
+ #
109
+ # Object structure
110
+ #
111
+
112
+ transform_keys(&:to_sym)
113
+
114
+ attribute :firstname, Types::String
115
+ attribute :lastname, Types::String
116
+ attribute :email, Types::EMail
117
+ attribute? :homepage, Types::WebPage.optional
118
+ attribute? :address, Types::String.optional
119
+ attribute :title, Types::String
120
+ attribute :type, Types::Symbol.enum(:minimal, :full)
121
+ attribute :login, Types::Login
122
+ attribute? :password, Types::Password.optional
123
+ attribute? :managers, Types::Array.of(Types::Login)
124
+ attribute? :locked, Types::Time.optional
125
+ attribute? :uid, Types::Integer
126
+ attribute? :gid, Types::Integer
127
+ attribute? :home, Types::Pathname
128
+ attribute :teams, Types::Array.of(Types::Team)
129
+
130
+ # Various User representation that can be used in processing
131
+ # as string, in sql statement, as JSON
132
+ def to_s ; self.login ; end
133
+ def to_str ; self.login ; end
134
+ def sql_literal(ds) ; ds.literal(self.login) ; end
135
+ def to_json(*a) ; self.to_hash.compact.to_json(*a) ; end
136
+
137
+ # User full name.
138
+ def fullname
139
+ [ firstname, lastname ].join(' ')
140
+ end
141
+ end
142
+ ~~~
143
+
144
+
145
+ ~~~ruby
146
+ # Return user id of users for which account has been locked and
147
+ # with "John Doe" as manager
148
+ User.locked(true).manager('jdoe').list
149
+
150
+ # Return list of users (as User instance) without managers
151
+ User.manager(false).all
152
+ ~~~
data/lib/lom.rb ADDED
@@ -0,0 +1,5 @@
1
+ require_relative 'lom/core'
2
+ require_relative 'lom/ldap'
3
+ require_relative 'lom/handler'
4
+ require_relative 'lom/mapper'
5
+
data/lib/lom/core.rb ADDED
@@ -0,0 +1,49 @@
1
+ require_relative 'version'
2
+
3
+ class LOM
4
+ # Standard Error
5
+ class Error < StandardError
6
+ end
7
+
8
+ # Entry not found
9
+ class EntryNotFound < Error
10
+ end
11
+
12
+ # Mapping error
13
+ class MappingError < Error
14
+ end
15
+
16
+ # Conversion error
17
+ class ConvertionError < Error
18
+ end
19
+
20
+
21
+ # Time format used in ldap
22
+ TIME_FORMAT = "%Y%m%d%H%M%SZ"
23
+
24
+
25
+ # Convert a Date/Time object to an ldap string representation
26
+ #
27
+ # @param [Date, Time] ts
28
+ #
29
+ # @return [String] string representation of time in ldap
30
+ #
31
+ def self.to_ldap_time(ts)
32
+ case ts
33
+ when Date, Time then ts.strftime(TIME_FORMAT)
34
+ when nil then nil
35
+ else raise ArgumentError
36
+ end
37
+ end
38
+
39
+ # Get debugging mode
40
+ def self.debug
41
+ @@debug ||= []
42
+ end
43
+
44
+ # Set debugging mode
45
+ # @param [Array<:dry,:verbose>] debugging options
46
+ def self.debug=(v)
47
+ @@debug = v
48
+ end
49
+ end
@@ -0,0 +1,171 @@
1
+ require 'date'
2
+ require_relative 'ldap/converters'
3
+ require_relative 'ldap/extensions'
4
+
5
+ class LOM
6
+
7
+ class Filtered
8
+ include Enumerable
9
+
10
+ using LDAP::Extensions
11
+ using LDAP::Converters
12
+
13
+ NONE = Object.new.freeze
14
+ ANY = Object.new.freeze
15
+
16
+ def initialize(src, filter = nil, paged: nil)
17
+ @src = src
18
+ @filter = filter
19
+ @paged = paged
20
+ end
21
+ attr_reader :src, :filter, :paged
22
+
23
+ # Join two filter using a or operation
24
+ def |(o)
25
+ _operator_2('|', o)
26
+ end
27
+
28
+ # Join two filter using a and operation
29
+ def &(o)
30
+ _operator_2('&', o)
31
+ end
32
+
33
+ # Take the negation of this fileter
34
+ def ~@
35
+ _operator_1('!')
36
+ end
37
+
38
+
39
+ # Ask for paginated data.
40
+ #
41
+ # @note That is not supported by net/ldap and is emulated by taking
42
+ # a slice of the retrieved data. Avoid using.
43
+ #
44
+ # @param [Integer] page index (starting from 1)
45
+ # @param [Integer] page size
46
+ #
47
+ # @return [self]
48
+ def paginate(page, page_size)
49
+ @paged = [ page, page_size ]
50
+ self
51
+ end
52
+
53
+ # Iterate over matching data
54
+ def each(*args, &block)
55
+ @src.each(*args, filter: @filter, paged: self.paged, &block)
56
+ end
57
+
58
+ # Retrieve matching data as a list of object
59
+ #
60
+ # @return [Array<Object>]
61
+ #
62
+ def all
63
+ each(:object).to_a
64
+ end
65
+
66
+ # Retrieve matching data as a list of id
67
+ #
68
+ # @return [Array<String>]
69
+ #
70
+ def list
71
+ each(:id).to_a
72
+ end
73
+
74
+ # Escape (and convert) a value for correct processing.
75
+ #
76
+ # Before escaping, the value will be converted to string using
77
+ # if possible #to_ldap, #to_str, and #to_s in case of symbol
78
+ #
79
+ # @param [Object] val value to be escaped
80
+ #
81
+ def self.escape(val)
82
+ val = if val.respond_to?(:to_ldap) then val.to_ldap
83
+ elsif val.respond_to?(:to_str ) then val.to_str
84
+ elsif val.kind_of?(Symbol) then val.to_s
85
+ else raise ArgumentError, 'can\'t convert to string'
86
+ end
87
+ Net::LDAP::Filter.escape(val)
88
+ end
89
+
90
+ # Test if an attribute exists
91
+ def self.exists(attr, predicate: true)
92
+ case predicate
93
+ when true, nil then "(#{attr}=*)"
94
+ when false, :none then "(!(#{attr}=*))"
95
+ else raise ArgumentError
96
+ end
97
+ end
98
+
99
+ # Test if an attribute is of the specified value
100
+ def self.is(attr, val, predicate: true)
101
+ case predicate
102
+ when true, nil then "(#{attr}=#{escape(val)})"
103
+ when false then "(!(#{attr}=#{escape(val)}))"
104
+ else raise ArgumentError
105
+ end
106
+ end
107
+
108
+ # Test if an attribute has the specified value.
109
+ # Using NONE will test for absence, ANY for existence
110
+ def self.has(attr, val)
111
+ val = yield(val) if block_given?
112
+
113
+ case val
114
+ when ANY then "(#{attr}=*)"
115
+ when NONE then "(!(#{attr}=*))"
116
+ else "(#{attr}=#{escape(val)})"
117
+ end
118
+ end
119
+
120
+ # Test if an attribute as a time before the specified timestamp
121
+ # If an integer is given it is added to the today date
122
+ def self.before(attr, ts, predicate: true)
123
+ ts = Date.today + ts if ts.kind_of?(Integer)
124
+ ts = LOM.to_ldap_time(ts)
125
+ "(#{attr}<=#{ts})".then {|f| predicate ? f : "(!#{f})" }
126
+ end
127
+
128
+ # Test if an attribute as a time after the specified timestamp
129
+ # If an integer is given it is subtracted to the today date
130
+ def self.after(attr, ts, predicate: true)
131
+ ts = Date.today - ts if ts.kind_of?(Integer)
132
+ ts = LOM.to_ldap_time(ts)
133
+ "(#{attr}>=#{ts})".then {|f| predicate ? f : "(!#{f})" }
134
+ end
135
+
136
+ private
137
+
138
+ # Operation with 2 elements
139
+ def _operator_2(op, o)
140
+ if @src != o.src
141
+ raise ArgumentError, 'filter defined with different sources'
142
+ end
143
+ _filter = if !@filter.nil? && !o.filter.nil?
144
+ then Net::LDAP.filter(op, @filter, o.filter)
145
+ else @filter || o.filter
146
+ end
147
+ Filtered.new(@src, _filter, paged: o.paged || self.paged)
148
+ end
149
+
150
+ # Operation with 1 element
151
+ def _operator_1(op)
152
+ Filtered.new(@src, Net::LDAP.filter(op, @filter),
153
+ paged: self.paged)
154
+ end
155
+
156
+ # Check if an ldap_list has been defined with that name
157
+ def respond_to_missing?(method_name, include_private = false)
158
+ @src.ldap_listing.include?(method_name) || super
159
+ end
160
+
161
+ # Call the ldap_list defined with that name
162
+ def method_missing(method_name, *args, &block)
163
+ if @src.ldap_listing.include?(method_name)
164
+ self & @src.send(method_name, *args, &block)
165
+ else
166
+ super
167
+ end
168
+ end
169
+
170
+ end
171
+ end
@@ -0,0 +1,43 @@
1
+ require 'forwardable'
2
+
3
+ class LOM
4
+ using LDAP::Extensions
5
+
6
+
7
+ def self.lh=(lh)
8
+ @lh = lh
9
+ end
10
+
11
+ # Get the LDAP handler to use
12
+ #
13
+ # In order of preference:
14
+ #
15
+ # * the handler set using lh=
16
+ # * the LH constant in this scope or parent scope
17
+ # * the one defined in $lh
18
+ #
19
+ def self.lh
20
+ @lh || const_get(:LH) || $lh
21
+ end
22
+
23
+
24
+ # extend Forwardable
25
+ #
26
+ # def self.connect(*args)
27
+ # self.new(Net::LDAP.connect(*args))
28
+ # end
29
+ #
30
+ # def initialize(lh)
31
+ # @lh = lh
32
+ # end
33
+ #
34
+ # def_delegator :@lh, :search
35
+ # def_delegator :@lh, :update
36
+ # def_delegator :@lh, :modify
37
+ # def_delegator :@lh, :add
38
+ # def_delegator :@lh, :delete
39
+
40
+ end
41
+
42
+
43
+
data/lib/lom/ldap.rb ADDED
@@ -0,0 +1,33 @@
1
+ require_relative 'ldap/converters'
2
+ require_relative 'ldap/extensions'
3
+
4
+ class LOM
5
+ using LDAP::Extensions
6
+
7
+ # Retrieve the identifier.
8
+ #
9
+ # The given `dn` should be a direct child of the `branch`,
10
+ # and if `attr` is specified, the attribute name should also match.
11
+ #
12
+ # ~~~
13
+ # dn = "uid=jdoe,ou=People,dc=example,dc=com"
14
+ # LOM.id_from_dn(dn, "ou=People,dc=example,dc=com", :uid)
15
+ # ~~~
16
+ #
17
+ # @param [String] dn DN of the object
18
+ # @param [String] branch Branch the DN should belong
19
+ # @param [Symbol,String] attr Attribute name
20
+ #
21
+ # @return [String] Identifier
22
+ # @return [nil] Unable to extract identifier
23
+ #
24
+ def self.id_from_dn(dn, branch, attr = nil)
25
+ if sub = Net::LDAP::DN.sub?(dn, branch)
26
+ k, v, o = sub.to_a
27
+ if o.nil? && (!attr.nil? || (k == attr.to_s))
28
+ v
29
+ end
30
+ end
31
+ end
32
+
33
+ end
@@ -0,0 +1,132 @@
1
+ require 'date'
2
+ require 'set'
3
+
4
+ require_relative '../core'
5
+
6
+
7
+ module LOM::LDAP
8
+ module Converters
9
+
10
+ #
11
+ # Integer
12
+ #
13
+
14
+ refine Integer do
15
+ def to_ldap
16
+ self.to_s
17
+ end
18
+ end
19
+
20
+ refine Integer.singleton_class do
21
+ def from_ldap(v)
22
+ Integer(v)
23
+ end
24
+ end
25
+
26
+
27
+
28
+ #
29
+ # String
30
+ #
31
+
32
+ refine String do
33
+ def to_ldap
34
+ self
35
+ end
36
+ end
37
+
38
+ refine String.singleton_class do
39
+ def from_ldap(v)
40
+ v
41
+ end
42
+ end
43
+
44
+
45
+
46
+ #
47
+ # Date / Time
48
+ #
49
+
50
+ refine Date do
51
+ def to_ldap
52
+ self.to_time.to_ldap
53
+ end
54
+ end
55
+
56
+ refine Date.singleton_class do
57
+ def from_ldap(date)
58
+ return nil if date.nil?
59
+ Date.parse(date)
60
+ end
61
+ end
62
+
63
+ refine Time do
64
+ def to_ldap
65
+ self.gmtime.strftime("%Y%m%d%H%M%SZ")
66
+ end
67
+ end
68
+
69
+ refine Time.singleton_class do
70
+ def from_ldap(time)
71
+ return nil if time.nil?
72
+ self::gm(time[0,4].to_i, time[4,2].to_i, time[6,2].to_i,
73
+ time[8,2].to_i, time[10,2].to_i, time[12,2].to_i)
74
+ end
75
+ end
76
+
77
+
78
+
79
+ #
80
+ # Boolean
81
+ #
82
+
83
+ refine TrueClass do
84
+ def to_ldap
85
+ 'TRUE'
86
+ end
87
+ end
88
+
89
+ refine TrueClass.singleton_class do
90
+ def from_ldap(v)
91
+ v == 'TRUE'
92
+ end
93
+ end
94
+
95
+ refine FalseClass do
96
+ def to_ldap
97
+ 'FALSE'
98
+ end
99
+ end
100
+
101
+
102
+
103
+ #
104
+ # Array / Set
105
+ #
106
+
107
+ refine Set do
108
+ def to_ldap
109
+ self.to_a.to_ldap
110
+ end
111
+ end
112
+
113
+ refine Array do
114
+ def to_ldap
115
+ self.map { |val|
116
+ if val.respond_to?(:to_ldap) then val.to_ldap
117
+ elsif val.respond_to?(:to_str ) then val.to_str
118
+ elsif val.kind_of?(Symbol) then val.to_s
119
+ else raise LOM::ConvertionError,
120
+ "can't convert to string (#{val.class})"
121
+ end
122
+ }.tap {|list|
123
+ if err = list.find {|e| ! e.kind_of?(String) }
124
+ raise LOM::ConvertionError,
125
+ "detected a non-string element (#{err.class})"
126
+ end
127
+ }
128
+ end
129
+ end
130
+
131
+ end
132
+ end
@@ -0,0 +1,257 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ require 'net/ldap'
4
+ require 'net/ldap/dn'
5
+
6
+ require_relative '../core'
7
+
8
+
9
+ module LOM::LDAP
10
+
11
+ # Ensure that the Converters module exists.
12
+ #
13
+ # NOTE: If the optional refinements provided by this modules are required,
14
+ # they need to be defined/loaded before requiring this file
15
+ # For example: require 'lom/ldap/converters'
16
+ module Converters
17
+ end
18
+
19
+
20
+ # Provide refinements to ease development with the net/ldap library:
21
+ #
22
+ # * Net::LDAP instance can be created from an URI
23
+ # using Net::LDAP.connect
24
+ #
25
+ # * Net::LDAP#search can use symbols for
26
+ # scope: :base, :one, :sub
27
+ # deref: :never, :search, :find, :always
28
+ #
29
+ # * Net::LDAP#get method allows retrieving the first entry of a DN
30
+ # (it is just a customized search query)
31
+ #
32
+ # * Net::LDAP#update method that try to intelligently update an
33
+ # LDAP attribute (to be used instead of Net::LDAP#modify)
34
+ #
35
+ # * Net::LDAP::Entry has been enhanced to easy casting of retrieved
36
+ # attributes
37
+ #
38
+ # * Net::LDAP::DN.sub? has been added to test if a DN is included
39
+ # in another, and will return the sub part
40
+ #
41
+ # * Net::LDAP::DN.escape and Net::LDAP:Filter.escape have been
42
+ # redefined to fix some issues
43
+ #
44
+ module Extensions
45
+ refine Net::LDAP.singleton_class do
46
+ def filter(op, *args)
47
+ op, check = case op
48
+ when :or, '|' then [ '|', 1.. ]
49
+ when :and, '&' then [ '&', 1.. ]
50
+ when :not, '!' then [ '!', 1 ]
51
+ when :ge, '>=' then [ '>=', 2 ]
52
+ when :eq, '=' then [ '=', 2 ]
53
+ when :le, '<=' then [ '<=', 2 ]
54
+ else raise ArgumentError, 'Unknown operation'
55
+ end
56
+ args = args.compact.map(&:strip).reject(&:empty?).map {|a|
57
+ if ( a[0] == '(' ) && ( a[-1] == ')' ) then a
58
+ elsif ( a[0] != '(' ) && ( a[-1] != ')' ) then "(#{a})"
59
+ else raise ArgumentError, "Bad LDAP filter: #{a}"
60
+ end
61
+ }
62
+ case args.size
63
+ when 0 then nil
64
+ when 1 then args[0]
65
+ else "(#{op}#{args.join})"
66
+ end
67
+ end
68
+
69
+ def connect(uri=nil, **opts)
70
+ if uri
71
+ uri = URI(uri)
72
+ case uri.scheme
73
+ when 'ldap' then
74
+ when 'ldaps' then opts[:encryption] = :simple_tls
75
+ else raise ArgumentError, "Unsupported protocol #{proto}";
76
+ end
77
+ opts[:host] = uri.host
78
+ opts[:port] = uri.port
79
+ end
80
+ self.new(opts)
81
+ end
82
+ end
83
+
84
+ refine Net::LDAP do
85
+ def close
86
+ end
87
+
88
+ def search(args={}, &block)
89
+ if deref = case args[:deref]
90
+ when :never then Net::LDAP::DerefAliases_Never
91
+ when :search then Net::LDAP::DerefAliases_Search
92
+ when :find then Net::LDAP::DerefAliases_Find
93
+ when :always then Net::LDAP::DerefAliases_Always
94
+ end
95
+ args[:deref] = deref
96
+ end
97
+ if scope = case args[:scope]
98
+ when :base then Net::LDAP::SearchScope_BaseObject
99
+ when :one then Net::LDAP::SearchScope_SingleLevel
100
+ when :sub then Net::LDAP::SearchScope_WholeSubtree
101
+ end
102
+ args[:scope] = scope
103
+ end
104
+ super(args, &block)
105
+ end
106
+
107
+ def get(dn:, attributes: nil, attributes_only: false,
108
+ return_result: true, time: nil, deref: :never, &block)
109
+ search(:base => dn,
110
+ :scope => :base,
111
+ :attributes => attributes,
112
+ :attributes_only => attributes_only,
113
+ :return_result => return_result,
114
+ :time => time,
115
+ :deref => deref,
116
+ &block)
117
+ .then {|r| return_result ? r&.first : r }
118
+ end
119
+
120
+ # Update an existing dn entry.
121
+ # The necessary operation (add/modify/replace) will be built
122
+ # accordingly.
123
+ #
124
+ # @note the dn can be specified, either in the dn parameter
125
+ # or as a key in the attributes parameter
126
+ #
127
+ # @param dn
128
+ # @param attributes
129
+ #
130
+ # @return [nil] dn doesn't exist so it can't be updated
131
+ # @return [Boolean] operation success
132
+ #
133
+ # @raise [ArgumentError] if DN missing or incoherent
134
+ #
135
+ def update(dn: nil, attributes: {})
136
+ # Normalize keys
137
+ attributes = attributes.to_h.dup
138
+ attributes.transform_keys! {|k| k.downcase.to_sym }
139
+ attributes.transform_values! {|v| Array(v) }
140
+ attributes.transform_values! {|v| v.empty? ? nil : v }
141
+
142
+ # Sanitize
143
+ _dn = attributes[:dn]
144
+ if _dn && _dn.size > 1
145
+ raise ArgumentError, 'only one DN can be specified'
146
+ end
147
+ if dn.nil? && _dn.nil?
148
+ raise ArgumentError, 'missing DN'
149
+ elsif dn && _dn && dn != _dn.first
150
+ raise ArgumentError, 'attribute DN doesn\'t match provided DN'
151
+ end
152
+
153
+ dn ||= _dn.first
154
+ attributes[:dn] = [ dn ]
155
+
156
+ # Retrieve existing attributes
157
+ # Note: dn is always present in entries
158
+ entries = get(dn: dn, attributes: attributes.keys)
159
+
160
+ # Entry not found
161
+ return nil if entries.nil?
162
+
163
+ # Identify keys
164
+ changing = attributes.compact.keys
165
+ removing = attributes.select {|k, v| v.nil? }.keys
166
+ existing = entries.attribute_names
167
+ add = changing - existing
168
+ modify = changing & existing
169
+ delete = removing & existing
170
+
171
+ # Remove key from update if same content
172
+ modify.reject! {|k| attributes[k] == entries[k] }
173
+
174
+ # Build operations
175
+ # Note: order is delete/modify/add
176
+ # to avoid "Object Class Violation" due to possible
177
+ # modification of objectClass
178
+ ops = []
179
+ ops += delete.map {|k| [ :delete, k, nil ] }
180
+ ops += modify.map {|k| [ :replace, k, attributes[k] ] }
181
+ ops += add .map {|k| [ :add, k, attributes[k] ] }
182
+
183
+ # Apply
184
+ if LOM.debug.include?(:verbose)
185
+ $stderr.puts "Update: #{dn}"
186
+ $stderr.puts ops.inspect
187
+ end
188
+ if LOM.debug.include?(:dry)
189
+ return true
190
+ end
191
+ return true if op.empty? # That's a no-op
192
+ modify(:dn => dn, :operations => ops) # Apply modifications
193
+ end
194
+ end
195
+
196
+ refine Net::LDAP::Filter.singleton_class do
197
+ def escape(str)
198
+ str.gsub(/([\x00-\x1f*()\\])/) { '\\%02x' % $1[0].ord }
199
+ end
200
+ end
201
+
202
+ refine Net::LDAP::DN.singleton_class do
203
+ def sub?(dn, prefix)
204
+ _dn = Net::LDAP::DN.new(dn ).to_a
205
+ _prefix = Net::LDAP::DN.new(prefix).to_a
206
+ return nil if _dn.size <= _prefix.size
207
+ sub = _dn[0 .. - (_prefix.size + 1)]
208
+ return nil if sub.empty?
209
+ Net::LDAP::DN.new(*sub)
210
+ end
211
+ end
212
+
213
+ refine Net::LDAP::DN.singleton_class do
214
+ def escape(str)
215
+ str.gsub(/([\x00-\x1f])/ ) { '\\%02x' % $1[0].ord } \
216
+ .gsub(/([\\+\"<>;,\#=])/ ) { '\\' + $1 }
217
+ end
218
+ end
219
+
220
+ refine Net::LDAP::Entry do
221
+ using LOM::LDAP::Converters
222
+
223
+ def _cast(val, cnv=nil, &block)
224
+ if cnv && block
225
+ raise ArgumentError,
226
+ 'converter can\'t be pass as parameter and as block'
227
+ elsif block
228
+ cnv = block
229
+ end
230
+
231
+ case cnv
232
+ when Method, Proc then cnv.call(val)
233
+ when Class then cnv.from_ldap(val)
234
+ when nil then val
235
+ else raise ArgumentError, "unhandled converter type (#{cnv.class})"
236
+ end
237
+ end
238
+ private :_cast
239
+
240
+ def [](name, cnv=nil, &block)
241
+ values = super(name)
242
+ if cnv.nil? && block.nil?
243
+ then values
244
+ else values.map {|e| _cast(e, cnv, &block) }
245
+ end
246
+ end
247
+ alias :all :[]
248
+
249
+ def first(name, cnv=nil, &block)
250
+ if value = super(name)
251
+ _cast(value, cnv, &block)
252
+ end
253
+ end
254
+ end
255
+ end
256
+
257
+ end
data/lib/lom/mapper.rb ADDED
@@ -0,0 +1,296 @@
1
+ require_relative 'ldap/converters'
2
+ require_relative 'ldap/extensions'
3
+ require_relative 'filtered'
4
+
5
+ class LOM
6
+ using LDAP::Extensions
7
+ using LDAP::Converters
8
+
9
+ # This module is to be prepend to Entry instance when processing
10
+ # block `from_ldap`
11
+ #
12
+ # It will allow the use of refined methods #first, #[], #all
13
+ # without requiring an explicit import of LDAPExt refinement
14
+ # in the class being mapped.
15
+ #
16
+ module EntryEnhanced
17
+ def first(*args) ; super ; end
18
+ def [](*args) ; super ; end
19
+ alias :all :[]
20
+ end
21
+
22
+
23
+
24
+ # Instance methods to be injected in the class being mapped.
25
+ #
26
+ module Mapper
27
+ module InstanceMethods
28
+ # LDAP handler
29
+ def lh
30
+ self.class.lh
31
+ end
32
+
33
+ # Save object to ldap.
34
+ #
35
+ # If object already exists, it will be updated otherwise created.
36
+ #
37
+ # @return [true, false]
38
+ #
39
+ def save!
40
+ attrs = instance_exec(self, &self.class._ldap_to)
41
+ .transform_values {|v|
42
+ # Don't use Array(), not what you think on
43
+ # some classes such as Time
44
+ v = [ ] if v.nil?
45
+ v = [ v ] unless v.is_a?(Array)
46
+ v.to_ldap
47
+ }
48
+ id, _ = Array(attrs[self.class._ldap_prefix])
49
+ raise MappingError, 'prefix for dn has multiple values' if _
50
+ dn = self.class.ldap_dn_from_id(id)
51
+
52
+ lh.update(dn: dn, attributes: attrs).then {|res|
53
+ break res unless res.nil?
54
+ attrs.reject! {|k, v| Array(v).empty? }
55
+ lh.add(dn: dn, attributes: attrs)
56
+ }
57
+ end
58
+ end
59
+ end
60
+
61
+
62
+
63
+ # Class methods to be injected in the class being mapped,
64
+ # and performs initialization thanks to #extend_object
65
+ #
66
+ module Mapper
67
+ def self.extend_object(o)
68
+ super
69
+ o.include Mapper::InstanceMethods
70
+ o.extend Enumerable
71
+ o.const_set(:Filtered, LOM::Filtered)
72
+ o.__ldap_init
73
+ end
74
+
75
+ def __ldap_init
76
+ @__ldap_branch = nil
77
+ @__ldap_prefix = nil
78
+ @__ldap_scope = :one
79
+ @__ldap_filter = nil
80
+ @__ldap_attrs = nil
81
+ @__ldap_from = nil
82
+ @__ldap_to = nil
83
+ @__ldap_list = []
84
+ @__ldap_lh = nil
85
+ end
86
+
87
+ # Get the LDAP handler to use
88
+ #
89
+ # In order of preference:
90
+ #
91
+ # * the handler set using lh=
92
+ # * the LH constant in this scope or parent scope
93
+ # * the one provided by LOM.lh
94
+ #
95
+ def lh
96
+ @__ldap_lh || const_get(:LH) || LOM.lh
97
+ end
98
+
99
+ # Set the LDAP handler to use
100
+ def lh=(lh)
101
+ @__ldap_lh = lh
102
+ end
103
+
104
+
105
+ def ldap_listing
106
+ @__ldap_list
107
+ end
108
+
109
+ def ldap_list(name, body=nil, &block)
110
+ if body && block
111
+ raise ArgumentError
112
+ elsif body.nil? && block.nil?
113
+ raise ArgumentError
114
+ elsif block
115
+ body = block
116
+ end
117
+
118
+ @__ldap_list << name
119
+ define_singleton_method(name) do |*args|
120
+ filter = body.call(*args)
121
+ LOM::Filtered.new(self, filter)
122
+ end
123
+ end
124
+
125
+
126
+ def ldap_branch(v)
127
+ @__ldap_branch = v
128
+ end
129
+
130
+ def ldap_prefix(v)
131
+ @__ldap_prefix = v
132
+ end
133
+
134
+ def ldap_scope(v)
135
+ @__ldap_scope = v
136
+ end
137
+
138
+ def ldap_filter(v)
139
+ @__ldap_filter = v[0] == '(' ? v : "(#{v})"
140
+ end
141
+
142
+ def ldap_attrs(*v)
143
+ @__ldap_attrs = v
144
+ end
145
+
146
+ # @note block will be executed in the Net::LDAP::Entry instance
147
+ def ldap_from(p=nil, &b)
148
+ if (! p.nil? ^ b.nil?) || (p && !p.kind_of?(Proc))
149
+ raise ArgumentError,
150
+ 'one and only one of proc/lamba/block need to be defined'
151
+ end
152
+ @__ldap_from = p || b
153
+ end
154
+
155
+ # @note block will be executed in the mapped object instance
156
+ def ldap_to(p=nil, &b)
157
+ if (! p.nil? ^ b.nil?) || (p && !p.kind_of?(Proc))
158
+ raise ArgumentError,
159
+ 'one and only one of proc/lamba/block need to be defined'
160
+ end
161
+ @__ldap_to = p || b
162
+ end
163
+
164
+
165
+
166
+ def ldap_dn_to_id(dn)
167
+ prefix = _ldap_prefix.to_s
168
+ branch = _ldap_branch
169
+
170
+ if sub = Net::LDAP::DN.sub?(dn, branch)
171
+ case prefix
172
+ when String, Symbol
173
+ k, v, _ = sub.to_a
174
+ raise ArgumentError, "not a direct child" if _
175
+ raise ArgumentError, "wrong prefix" if k.casecmp(prefix) != 0
176
+ v
177
+ end
178
+ end
179
+ end
180
+
181
+ def ldap_dn_from_id(id)
182
+ Net::LDAP::DN.new(_ldap_prefix.to_s, id, _ldap_branch).to_s
183
+ end
184
+
185
+ def _ldap_to_obj(entry)
186
+ raise EntryNotFound if entry.nil?
187
+ entry.extend(EntryEnhanced)
188
+ args = entry.instance_exec(entry, &_ldap_from)
189
+ args = [ args ] unless args.kind_of?(Array)
190
+ self.new(*args)
191
+ end
192
+
193
+
194
+ def each(type = :object, filter: nil, paged: nil)
195
+ # Create Enumerator if no block given
196
+ unless block_given?
197
+ return enum_for(:each, type, filter: filter, paged: paged)
198
+ end
199
+
200
+ # Merging filters
201
+ filters = [ filter, _ldap_filter ].compact
202
+ filter = filters.size == 2 ? "(&#{filters.join})" : filters.first
203
+
204
+ # Define attributes/converter according to selected type
205
+ attributes, converter =
206
+ case type
207
+ when :id then [ :dn, ->(e) { ldap_dn_to_id(e.dn) } ]
208
+ when :object then [ _ldap_attrs, ->(e) { _ldap_to_obj(e) } ]
209
+ else raise ArgumentError, 'type must be either :object or :id'
210
+ end
211
+
212
+ # Paginate
213
+ # XXX: pagination is emulated, should be avoided
214
+ skip, count = if paged
215
+ page, page_size = paged
216
+ [ (page - 1) * page_size, page_size ]
217
+ end
218
+
219
+ # Perform search
220
+ lh.search(:base => _ldap_branch,
221
+ :filter => filter,
222
+ :attributes => attributes,
223
+ :scope => _ldap_scope) {|entry|
224
+
225
+ if paged.nil?
226
+ yield(converter.(entry))
227
+ elsif skip > 0
228
+ skip -= 1
229
+ elsif count <= 0
230
+ break
231
+ else
232
+ count -= 1
233
+ yield(converter.(entry))
234
+ end
235
+ }
236
+ end
237
+
238
+ def paginate(page, page_size)
239
+ LOM::Filtered.new(self, paged: [ page, page_size ])
240
+ end
241
+
242
+ def all
243
+ each(:object).to_a
244
+ end
245
+
246
+ def list
247
+ each(:id).to_a
248
+ end
249
+
250
+ def get(name)
251
+ dn = ldap_dn_from_id(name)
252
+ attrs = _ldap_attrs
253
+ entry = lh.get(:dn => dn, :attributes => attrs)
254
+
255
+ _ldap_to_obj(entry)
256
+ end
257
+
258
+ def delete!(name)
259
+ dn = ldap_dn_from_id(name)
260
+ lh.delete(:dn => dn)
261
+ end
262
+
263
+ alias [] get
264
+
265
+ private
266
+
267
+ def _ldap_branch
268
+ @__ldap_branch || (raise MappingError, 'ldap_branch not defined')
269
+ end
270
+
271
+ def _ldap_prefix
272
+ @__ldap_prefix || (raise MappingError, 'ldap_prefix not defined')
273
+ end
274
+
275
+ def _ldap_scope
276
+ @__ldap_scope
277
+ end
278
+
279
+ def _ldap_filter
280
+ @__ldap_filter
281
+ end
282
+
283
+ def _ldap_attrs
284
+ @__ldap_attrs
285
+ end
286
+
287
+ def _ldap_from
288
+ @__ldap_from || (raise MappingError, 'ldap_from not defined' )
289
+ end
290
+
291
+ def _ldap_to
292
+ @__ldap_to || (raise MappingError, 'ldap_to not defined' )
293
+ end
294
+
295
+ end
296
+ end
@@ -0,0 +1,3 @@
1
+ class LOM
2
+ VERSION = '0.9.0'
3
+ end
data/lom.gemspec ADDED
@@ -0,0 +1,31 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require_relative 'lib/lom/version'
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = 'lom'
7
+ s.version = LOM::VERSION
8
+ s.summary = "LDAP Object Mapper"
9
+ s.description = <<~EOF
10
+
11
+ Ease processing of parameters in Sinatra framework.
12
+ Integrates well with dry-types, sequel, ...
13
+
14
+ Example:
15
+ want! :user, Dry::Types::String, User
16
+ want? :expired, Dry::Types::Params::Bool.default(true)
17
+ EOF
18
+
19
+ s.homepage = 'https://gitlab.com/sdalu/lom'
20
+ s.license = 'MIT'
21
+
22
+ s.authors = [ "Stéphane D'Alu" ]
23
+ s.email = [ 'stephane.dalu@insa-lyon.fr' ]
24
+
25
+ s.files = %w[ README.md lom.gemspec ] +
26
+ Dir['lib/**/*.rb']
27
+
28
+ s.add_dependency 'net-ldap'
29
+ s.add_development_dependency 'yard', '~>0'
30
+ s.add_development_dependency 'rake', '~>13'
31
+ end
metadata ADDED
@@ -0,0 +1,103 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lom
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.9.0
5
+ platform: ruby
6
+ authors:
7
+ - Stéphane D'Alu
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2021-05-26 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: net-ldap
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: yard
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '13'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '13'
55
+ description: |2
56
+
57
+ Ease processing of parameters in Sinatra framework.
58
+ Integrates well with dry-types, sequel, ...
59
+
60
+ Example:
61
+ want! :user, Dry::Types::String, User
62
+ want? :expired, Dry::Types::Params::Bool.default(true)
63
+ email:
64
+ - stephane.dalu@insa-lyon.fr
65
+ executables: []
66
+ extensions: []
67
+ extra_rdoc_files: []
68
+ files:
69
+ - README.md
70
+ - lib/lom.rb
71
+ - lib/lom/core.rb
72
+ - lib/lom/filtered.rb
73
+ - lib/lom/handler.rb
74
+ - lib/lom/ldap.rb
75
+ - lib/lom/ldap/converters.rb
76
+ - lib/lom/ldap/extensions.rb
77
+ - lib/lom/mapper.rb
78
+ - lib/lom/version.rb
79
+ - lom.gemspec
80
+ homepage: https://gitlab.com/sdalu/lom
81
+ licenses:
82
+ - MIT
83
+ metadata: {}
84
+ post_install_message:
85
+ rdoc_options: []
86
+ require_paths:
87
+ - lib
88
+ required_ruby_version: !ruby/object:Gem::Requirement
89
+ requirements:
90
+ - - ">="
91
+ - !ruby/object:Gem::Version
92
+ version: '0'
93
+ required_rubygems_version: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - ">="
96
+ - !ruby/object:Gem::Version
97
+ version: '0'
98
+ requirements: []
99
+ rubygems_version: 3.0.8
100
+ signing_key:
101
+ specification_version: 4
102
+ summary: LDAP Object Mapper
103
+ test_files: []