rom-ldap 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (104) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +251 -0
  3. data/CONTRIBUTING.md +18 -0
  4. data/README.md +172 -0
  5. data/TODO.md +33 -0
  6. data/config/responses.yml +328 -0
  7. data/lib/dry/monitor/ldap/colorizers/default.rb +17 -0
  8. data/lib/dry/monitor/ldap/colorizers/rouge.rb +31 -0
  9. data/lib/dry/monitor/ldap/logger.rb +58 -0
  10. data/lib/rom-ldap.rb +1 -0
  11. data/lib/rom/ldap.rb +22 -0
  12. data/lib/rom/ldap/alias.rb +30 -0
  13. data/lib/rom/ldap/associations.rb +6 -0
  14. data/lib/rom/ldap/associations/core.rb +23 -0
  15. data/lib/rom/ldap/associations/many_to_many.rb +18 -0
  16. data/lib/rom/ldap/associations/many_to_one.rb +22 -0
  17. data/lib/rom/ldap/associations/one_to_many.rb +32 -0
  18. data/lib/rom/ldap/associations/one_to_one.rb +19 -0
  19. data/lib/rom/ldap/associations/self_ref.rb +35 -0
  20. data/lib/rom/ldap/attribute.rb +327 -0
  21. data/lib/rom/ldap/client.rb +185 -0
  22. data/lib/rom/ldap/client/authentication.rb +118 -0
  23. data/lib/rom/ldap/client/operations.rb +233 -0
  24. data/lib/rom/ldap/commands.rb +6 -0
  25. data/lib/rom/ldap/commands/create.rb +41 -0
  26. data/lib/rom/ldap/commands/delete.rb +17 -0
  27. data/lib/rom/ldap/commands/update.rb +35 -0
  28. data/lib/rom/ldap/constants.rb +193 -0
  29. data/lib/rom/ldap/dataset.rb +286 -0
  30. data/lib/rom/ldap/dataset/conversion.rb +62 -0
  31. data/lib/rom/ldap/dataset/dsl.rb +299 -0
  32. data/lib/rom/ldap/dataset/persistence.rb +44 -0
  33. data/lib/rom/ldap/directory.rb +126 -0
  34. data/lib/rom/ldap/directory/capabilities.rb +71 -0
  35. data/lib/rom/ldap/directory/entry.rb +200 -0
  36. data/lib/rom/ldap/directory/env.rb +155 -0
  37. data/lib/rom/ldap/directory/operations.rb +282 -0
  38. data/lib/rom/ldap/directory/password.rb +122 -0
  39. data/lib/rom/ldap/directory/root.rb +187 -0
  40. data/lib/rom/ldap/directory/tokenization.rb +66 -0
  41. data/lib/rom/ldap/directory/transactions.rb +31 -0
  42. data/lib/rom/ldap/directory/vendors/active_directory.rb +129 -0
  43. data/lib/rom/ldap/directory/vendors/apache_ds.rb +27 -0
  44. data/lib/rom/ldap/directory/vendors/e_directory.rb +16 -0
  45. data/lib/rom/ldap/directory/vendors/open_directory.rb +12 -0
  46. data/lib/rom/ldap/directory/vendors/open_dj.rb +25 -0
  47. data/lib/rom/ldap/directory/vendors/open_ldap.rb +35 -0
  48. data/lib/rom/ldap/directory/vendors/three_eight_nine.rb +16 -0
  49. data/lib/rom/ldap/directory/vendors/unknown.rb +22 -0
  50. data/lib/rom/ldap/dsl.rb +76 -0
  51. data/lib/rom/ldap/errors.rb +47 -0
  52. data/lib/rom/ldap/expression.rb +77 -0
  53. data/lib/rom/ldap/expression_encoder.rb +174 -0
  54. data/lib/rom/ldap/extensions.rb +50 -0
  55. data/lib/rom/ldap/extensions/active_support_notifications.rb +26 -0
  56. data/lib/rom/ldap/extensions/compatibility.rb +11 -0
  57. data/lib/rom/ldap/extensions/dsml.rb +165 -0
  58. data/lib/rom/ldap/extensions/msgpack.rb +23 -0
  59. data/lib/rom/ldap/extensions/optimised_json.rb +25 -0
  60. data/lib/rom/ldap/extensions/rails_log_subscriber.rb +38 -0
  61. data/lib/rom/ldap/formatter.rb +26 -0
  62. data/lib/rom/ldap/functions.rb +207 -0
  63. data/lib/rom/ldap/gateway.rb +145 -0
  64. data/lib/rom/ldap/ldif.rb +74 -0
  65. data/lib/rom/ldap/ldif/exporter.rb +77 -0
  66. data/lib/rom/ldap/ldif/importer.rb +95 -0
  67. data/lib/rom/ldap/mapper_compiler.rb +19 -0
  68. data/lib/rom/ldap/matchers.rb +69 -0
  69. data/lib/rom/ldap/message_queue.rb +7 -0
  70. data/lib/rom/ldap/oid.rb +101 -0
  71. data/lib/rom/ldap/parsers/abstract_syntax.rb +91 -0
  72. data/lib/rom/ldap/parsers/attribute.rb +290 -0
  73. data/lib/rom/ldap/parsers/filter_syntax.rb +133 -0
  74. data/lib/rom/ldap/pdu.rb +285 -0
  75. data/lib/rom/ldap/plugin/pagination.rb +145 -0
  76. data/lib/rom/ldap/plugins.rb +7 -0
  77. data/lib/rom/ldap/projection_dsl.rb +38 -0
  78. data/lib/rom/ldap/relation.rb +135 -0
  79. data/lib/rom/ldap/relation/exporting.rb +72 -0
  80. data/lib/rom/ldap/relation/reading.rb +461 -0
  81. data/lib/rom/ldap/relation/writing.rb +64 -0
  82. data/lib/rom/ldap/responses.rb +17 -0
  83. data/lib/rom/ldap/restriction_dsl.rb +45 -0
  84. data/lib/rom/ldap/schema.rb +123 -0
  85. data/lib/rom/ldap/schema/attributes_inferrer.rb +59 -0
  86. data/lib/rom/ldap/schema/dsl.rb +13 -0
  87. data/lib/rom/ldap/schema/inferrer.rb +50 -0
  88. data/lib/rom/ldap/schema/type_builder.rb +133 -0
  89. data/lib/rom/ldap/scope.rb +19 -0
  90. data/lib/rom/ldap/search_request.rb +249 -0
  91. data/lib/rom/ldap/socket.rb +210 -0
  92. data/lib/rom/ldap/tasks/ldap.rake +103 -0
  93. data/lib/rom/ldap/tasks/ldif.rake +80 -0
  94. data/lib/rom/ldap/transaction.rb +29 -0
  95. data/lib/rom/ldap/type_map.rb +88 -0
  96. data/lib/rom/ldap/types.rb +158 -0
  97. data/lib/rom/ldap/version.rb +17 -0
  98. data/lib/rom/plugins/relation/ldap/active_directory.rb +182 -0
  99. data/lib/rom/plugins/relation/ldap/auto_restrictions.rb +69 -0
  100. data/lib/rom/plugins/relation/ldap/e_directory.rb +27 -0
  101. data/lib/rom/plugins/relation/ldap/instrumentation.rb +35 -0
  102. data/lib/rouge/lexers/ldap.rb +72 -0
  103. data/lib/rouge/themes/ldap.rb +49 -0
  104. metadata +231 -0
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'digest/sha1'
4
+ require 'digest/sha2'
5
+ require 'digest/md5'
6
+ require 'base64'
7
+ require 'securerandom'
8
+
9
+ # sha2-512 512bit or 64bytes
10
+ # sha 160bits or 20bytes
11
+ #
12
+ module ROM
13
+ module LDAP
14
+ class Directory
15
+
16
+ # @abstract
17
+ # Encode and validate passwords using md5, sha or ssha.
18
+ #
19
+ # @api public
20
+ class Password
21
+
22
+ # Generate an ecrypted password.
23
+ #
24
+ # @example
25
+ # Password.generate(:ssha, 'secret magic word')
26
+ #
27
+ # @param type [Symbol] Encryption type. [:md5, :sha, :ssha].
28
+ # @param password [String] Plain text password to be encrypted.
29
+ #
30
+ # @return [String]
31
+ #
32
+ # @raise [PasswordError]
33
+ #
34
+ # @api public
35
+ def self.generate(type, password, salt = secure_salt)
36
+ raise PasswordError, 'No password supplied' if password.nil?
37
+
38
+ case type
39
+ when :md5 then _encode(type, md5(password))
40
+ when :sha then _encode(type, sha(password))
41
+ when :ssha then _encode(type, ssha(password, salt))
42
+ when :ssha512 then _encode(type, ssha512(password, salt))
43
+ else
44
+ raise PasswordError, "Unsupported encryption type (#{type})"
45
+ end
46
+ end
47
+
48
+ def self.check_ssha512(password, encrypted)
49
+ decoded = Base64.decode64(encrypted.gsub(/^{SSHA512}/, EMPTY_STRING))
50
+ # hash = decoded[0..64]
51
+ salt = decoded[64..-1]
52
+ _encode(:ssha512, ssha512(password, salt)) == encrypted
53
+ end
54
+
55
+ # Validate plain password against encrypted SSHA password.
56
+ #
57
+ # @return [Boolean]
58
+ #
59
+ # @api public
60
+ def self.check_ssha(password, encrypted)
61
+ decoded = Base64.decode64(encrypted.gsub(/^{SSHA}/, EMPTY_STRING))
62
+ # hash = decoded[0..20]
63
+ salt = decoded[20..-1]
64
+ _encode(:ssha, ssha(password, salt)) == encrypted
65
+ end
66
+
67
+ private_class_method
68
+
69
+ # @return [String] Prepend type to encrypted string.
70
+ #
71
+ # @api private
72
+ def self._encode(type, encrypted)
73
+ "{#{type.upcase}}" + Base64.strict_encode64(encrypted).chomp
74
+ end
75
+
76
+ # Generate salt.
77
+ #
78
+ # @api private
79
+ def self.secure_salt
80
+ SecureRandom.random_bytes(16)
81
+ end
82
+
83
+ # @param str [String]
84
+ #
85
+ # @return [String] MD5 digest.
86
+ #
87
+ # @api private
88
+ def self.md5(str)
89
+ Digest::MD5.digest(str)
90
+ end
91
+
92
+ # @param str [String]
93
+ # @param salt [String]
94
+ #
95
+ # @return [String] SHA1 digest with salt.
96
+ #
97
+ # @api private
98
+ def self.ssha(str, salt)
99
+ Digest::SHA1.digest(str + salt) + salt
100
+ end
101
+
102
+ # @param str [String]
103
+ #
104
+ # @return [String] SHA1 digest without salt.
105
+ #
106
+ # @api private
107
+ def self.sha(str)
108
+ Digest::SHA1.digest(str)
109
+ end
110
+
111
+ # "{SSHA512}A1lCCGYzUEJ5/qQCrFUAztLVaTaWv959RnpzaOsWB9Ij4CBCeNh6i4XrZzrvwUMM/AWbEb8Gjc7FWOBSPnkRuHsexjzeQImm"
112
+ # initial
113
+ #
114
+ def self.ssha512(str, salt)
115
+ Digest::SHA512.digest(str + salt) + salt
116
+ end
117
+
118
+ end
119
+
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,187 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ROM
4
+ module LDAP
5
+ class Directory
6
+
7
+ module Root
8
+ # Identify the LDAP server vendor, type determines vendor extension to load.
9
+ #
10
+ # @see https://ldapwiki.com/wiki/Determine%20LDAP%20Server%20Vendor
11
+ #
12
+ # @return [Symbol]
13
+ #
14
+ # @api public
15
+ def type
16
+ case root.first('vendorName')
17
+ when /389/ then :three_eight_nine
18
+ when /Apache/ then :apache_ds
19
+ when /Apple/ then :open_directory
20
+ when /ForgeRock/ then :open_dj
21
+ when /IBM/ then :ibm
22
+ when /Netscape/ then :netscape
23
+ when /Novell/ then :e_directory
24
+ when /Oracle/ then :open_ds
25
+ when /Sun/ then :sun_microsystems
26
+ when nil
27
+ return :active_directory if ad?
28
+ return :open_ldap if od?
29
+ else
30
+ :unknown
31
+ end
32
+ end
33
+
34
+ # @return [String]
35
+ #
36
+ # @api public
37
+ def vendor_name
38
+ root.first('vendorName')
39
+ end
40
+
41
+ # @return [String]
42
+ #
43
+ # @api public
44
+ def vendor_version
45
+ root.first('vendorVersion')
46
+ end
47
+
48
+ # @example
49
+ # [ 'Apple', '510.30' ]
50
+ # [ 'Apache Software Foundation', '2.0.0-M24' ]
51
+ #
52
+ # @return [Array<String>]
53
+ #
54
+ # @api public
55
+ def vendor
56
+ [vendor_name, vendor_version]
57
+ end
58
+
59
+ # Distinguished name of subschema
60
+ #
61
+ # @return [String]
62
+ #
63
+ # @api public
64
+ def sub_schema_entry
65
+ root.first('subschemaSubentry')
66
+ end
67
+
68
+ # @return [Array<String>] Object classes known by directory
69
+ #
70
+ # @api public
71
+ def schema_object_classes
72
+ sub_schema['objectClasses'].sort
73
+ end
74
+
75
+ # Query directory for all known attribute types
76
+ #
77
+ # @return [Array<String>] Attribute types known by directory
78
+ #
79
+ # @api public
80
+ def schema_attribute_types
81
+ sub_schema['attributeTypes'].sort
82
+ end
83
+
84
+ # @return [Array<String>]
85
+ #
86
+ # @api public
87
+ def supported_extensions
88
+ root['supportedExtension'].sort
89
+ end
90
+
91
+ # @return [Array<String>]
92
+ #
93
+ # @api public
94
+ def supported_controls
95
+ root['supportedControl'].sort
96
+ end
97
+
98
+ # @return [Array<String>]
99
+ #
100
+ # @api public
101
+ def supported_mechanisms
102
+ root['supportedSASLMechanisms'].sort
103
+ end
104
+
105
+ # @return [Array<String>]
106
+ #
107
+ # @api public
108
+ def supported_features
109
+ root['supportedFeatures'].sort
110
+ end
111
+
112
+ # @return [Array<Integer>]
113
+ #
114
+ # @api public
115
+ def supported_versions
116
+ root['supportedLDAPVersion'].sort.map(&:to_i)
117
+ end
118
+
119
+ # @return [String]
120
+ #
121
+ # @api public
122
+ def contexts
123
+ root['namingContexts'].sort
124
+ end
125
+
126
+ private
127
+
128
+ # Representation of directory RootDSE
129
+ #
130
+ # @see https://ldapwiki.com/wiki/Retrieving%20RootDSE
131
+ #
132
+ # @return [Directory::Entry]
133
+ #
134
+ # @raise [ResponseError]
135
+ #
136
+ # @api private
137
+ def root
138
+ @root ||= query(
139
+ base: EMPTY_STRING,
140
+ scope: SCOPE_BASE,
141
+ attributes: ALL_ATTRS
142
+ ).first
143
+
144
+ @root || raise(ResponseError, 'Directory root failed to load')
145
+ end
146
+
147
+ # Representation of directory SubSchema
148
+ #
149
+ # @return [Directory::Entry]
150
+ #
151
+ # @raise [ResponseError]
152
+ #
153
+ # @api private
154
+ def sub_schema
155
+ @sub_schema ||= query(
156
+ base: sub_schema_entry,
157
+ scope: SCOPE_BASE,
158
+ attributes: %w[objectClasses attributeTypes],
159
+ filter: '(objectClass=subschema)',
160
+ max_results: 1
161
+ ).first
162
+
163
+ @sub_schema || raise(ResponseError, 'Directory schema failed to load')
164
+ end
165
+
166
+ # Check if vendor identifies as ActiveDirectory
167
+ #
168
+ # @return [Boolean]
169
+ #
170
+ # @api private
171
+ def ad?
172
+ !root['forestFunctionality'].nil?
173
+ end
174
+
175
+ # Check if vendor identifies as OpenLDAP
176
+ #
177
+ # @return [Boolean]
178
+ #
179
+ # @api private
180
+ def od?
181
+ root['objectClass']&.include?('OpenLDAProotDSE')
182
+ end
183
+ end
184
+
185
+ end
186
+ end
187
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rom/ldap/parsers/abstract_syntax'
4
+ require 'rom/ldap/parsers/filter_syntax'
5
+ require 'rom/ldap/parsers/attribute'
6
+
7
+ module ROM
8
+ module LDAP
9
+ class Directory
10
+
11
+ # Parsing Formats
12
+ #
13
+ # @api private
14
+ module Tokenization
15
+ # Allows adapters that subclass Directory to use custom parsers.
16
+ # Extends the class with filter abstraction behavior.
17
+ #
18
+ # @api private
19
+ def self.included(klass)
20
+ klass.class_eval do
21
+ extend Dry::Core::ClassAttributes
22
+
23
+ defines :attribute_class
24
+ attribute_class Parsers::Attribute
25
+
26
+ defines :filter_class
27
+ filter_class Parsers::FilterSyntax
28
+
29
+ defines :ast_class
30
+ ast_class Parsers::AbstractSyntax
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ # Convert abstract criteria or LDAP filter into an expression object.
37
+ # Check for parsed attributes to prevent recursion.
38
+ #
39
+ # @param input [Array, String] RFC2254 or AST
40
+ #
41
+ def to_expression(input)
42
+ attrs = !@attribute_types.nil? ? attribute_types : EMPTY_ARRAY
43
+
44
+ # Filter > AST
45
+ unless input.is_a?(Array)
46
+ input = self.class.filter_class.new(input, attrs).call
47
+ end
48
+
49
+ # AST > Expression
50
+ self.class.ast_class.new(input, attrs).call
51
+ end
52
+
53
+ # Create parsed attribute from definiton.
54
+ #
55
+ # @param attr_def [String]
56
+ #
57
+ # @return [Hash]
58
+ #
59
+ def to_attribute(attr_def)
60
+ self.class.attribute_class.new(attr_def).call
61
+ end
62
+ end
63
+
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ROM
4
+ module LDAP
5
+ class Directory
6
+
7
+ # https://ldapwiki.com/wiki/Lightweight%20Directory%20Access%20Protocol%20%28LDAP%29%20Transactions
8
+ # https://tools.ietf.org/html/rfc5805
9
+ # https://ldapwiki.com/wiki/EDirectory%20LDAP%20Transaction
10
+ #
11
+ module Transactions
12
+ # @example
13
+ # directory.transaction(opts) { yield(self) }
14
+ #
15
+ # @todo Transactions WIP
16
+ #
17
+ # @api public
18
+ def transaction(_opts)
19
+ # binding.pry
20
+
21
+ # OID[:transaction_start_request]
22
+ # OID[:transaction_spec_request]
23
+ # OID[:transaction_end_request]
24
+
25
+ yield()
26
+ end
27
+ end
28
+
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ROM
4
+ module LDAP
5
+ #
6
+ # Microsoft Active Directory Extension
7
+ #
8
+ # @api private
9
+ module ActiveDirectory
10
+ #
11
+ # @note
12
+ # Use the AD Forest configuration container as a search base.
13
+ #
14
+ # @see https://msdn.microsoft.com/en-us/library/ms684291(v=vs.85).aspx
15
+ #
16
+ # RootDSE domainFunctionality
17
+ # RootDSE domainControllerFunctionality
18
+ # RootDSE forestFunctionality
19
+ #
20
+ VERSION_NAMES = {
21
+ 0 => 'Windows Server 2000 (5.0)',
22
+ 1 => 'Windows Server 2003 (5.2)',
23
+ 2 => 'Windows Server 2003 R2 (5.2)',
24
+ 3 => 'Windows Server 2008 (6.0)',
25
+ 4 => 'Windows Server 2008 R2 (6.1)',
26
+ 5 => 'Windows Server 2012 (6.2)',
27
+ 6 => 'Windows Server 2012 R2 (6.3)',
28
+ 7 => 'Windows Server 2016 (10.0)',
29
+ 8 => 'Windows Server Latest Version (?)'
30
+ }.freeze
31
+
32
+ #
33
+ # RootDSE supportedLDAPPolicies
34
+ #
35
+ POLICIES = %w[
36
+ InitRecvTimeout
37
+ MaxBatchReturnMessages
38
+ MaxConnections
39
+ MaxConnIdleTime
40
+ MaxDatagramRecv
41
+ MaxNotificationPerConn
42
+ MaxPageSize
43
+ MaxPercentDirSyncRequests
44
+ MaxPoolThreads
45
+ MaxQueryDuration
46
+ MaxReceiveBuffer
47
+ MaxResultSetSize
48
+ MaxResultSetsPerConn
49
+ MaxTempTableSize
50
+ MaxValRange
51
+ MaxValRangeTransitive
52
+ MinResultSets
53
+ SystemMemoryLimitPercent
54
+ ThreadMemoryLimit
55
+ ].freeze
56
+
57
+ # @return [String]
58
+ #
59
+ def vendor_name
60
+ 'Microsoft'
61
+ end
62
+
63
+ # @return [String]
64
+ #
65
+ def vendor_version
66
+ VERSION_NAMES[domain_functionality]
67
+ end
68
+
69
+ # @return [Integer]
70
+ #
71
+ def controller_functionality
72
+ root.first('domainControllerFunctionality').to_i
73
+ end
74
+
75
+ # @return [Integer]
76
+ #
77
+ def forest_functionality
78
+ root.first('forestFunctionality').to_i
79
+ end
80
+
81
+ # @return [Integer]
82
+ #
83
+ def domain_functionality
84
+ root.first('domainFunctionality').to_i
85
+ end
86
+
87
+ # LDAP server internal clock
88
+ #
89
+ # @return [Time]
90
+ #
91
+ def directory_time
92
+ Functions[:to_time][root.first('currentTime')]
93
+ end
94
+
95
+ # @return [Array<String>]
96
+ #
97
+ def supported_capabilities
98
+ root['supportedCapabilities'].sort
99
+ end
100
+ end
101
+
102
+ # OID = OID.dup.merge!(
103
+ # extended_dn: '1.2.840.113556.1.4.529', # Extended DN control (Stateless)
104
+ # get_stats: '1.2.840.113556.1.4.970', # Get stats control (Stateless)
105
+ # verify_name: '1.2.840.113556.1.4.1338', # Verify name control (Stateless)
106
+ # domain_scope: '1.2.840.113556.1.4.1339', # LDAP_SERVER_DOMAIN_SCOPE_OID
107
+ # unknown: '1.2.840.113556.1.4.1340',
108
+ # # unknown: '1.2.840.113556.1.4.1341',
109
+ # # unknown: '1.2.840.113556.1.4.1413',
110
+ # # unknown: '1.2.840.113556.1.4.1504',
111
+ # # unknown: '1.2.840.113556.1.4.1852',
112
+ # # unknown: '1.2.840.113556.1.4.1907',
113
+ # # unknown: '1.2.840.113556.1.4.1948',
114
+ # # unknown: '1.2.840.113556.1.4.1974',
115
+ # # unknown: '1.2.840.113556.1.4.2026',
116
+ # # unknown: '1.2.840.113556.1.4.2064',
117
+ # # unknown: '1.2.840.113556.1.4.2065',
118
+ # # unknown: '1.2.840.113556.1.4.2066',
119
+ # # unknown: '1.2.840.113556.1.4.2090',
120
+ # # unknown: '1.2.840.113556.1.4.2204',
121
+ # # unknown: '1.2.840.113556.1.4.2205',
122
+ # # unknown: '1.2.840.113556.1.4.2206',
123
+ # # unknown: '1.2.840.113556.1.4.2211',
124
+ # # unknown: '1.2.840.113556.1.4.2239',
125
+ # # unknown: '1.2.840.113556.1.4.2255',
126
+ # # unknown: '1.2.840.113556.1.4.2256'
127
+ # ).freeze
128
+ end
129
+ end