Skip to content

Commit 1cc8d0d

Browse files
committed
feat: implement sort controls
1 parent 10c3753 commit 1cc8d0d

File tree

3 files changed

+87
-20
lines changed

3 files changed

+87
-20
lines changed

README.md

+53-4
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,61 @@ client = LDAP::Client.new(socket, tls)
3838
client.authenticate(user, pass)
3939

4040
# Can now perform LDAP operations
41+
```
42+
43+
To use the non-standard `LDAPS` (LDAP Secure, commonly known as LDAP over SSL) protocol then pass in a `OpenSSL::SSL::Socket::Client` directly: `LDAP::Client.new(tls_socket)`
44+
45+
### Querying
46+
47+
You can perform search requests
48+
49+
```crystal
50+
51+
# You can use LDAP string filters directly
52+
client.search(base: "dc=example,dc=com", filter: "(|(uid=einstein)(uid=training))")
53+
54+
# There are options to select particular attributes and limit response sizes
4155
filter = LDAP::Request::Filter.equal("objectClass", "person")
42-
client.search(base: "dc=example,dc=com", filter: filter, size: 6, attributes: ["hasSubordinates", "objectClass"])
56+
client.search(
57+
base: "dc=example,dc=com",
58+
filter: filter,
59+
size: 6,
60+
attributes: ["hasSubordinates", "objectClass"]
61+
)
4362
44-
# Note how filters can be combined and standard LDAP queries can be parsed
45-
filter = (filter & LDAP::Request::Filter.equal("sn", "training")) | LDAP::Request::FilterParser.parse("(uid=einstein)")
63+
# Filters can be combined using standard operations
64+
filter = (
65+
LDAP::Request::Filter.equal("objectClass", "person") &
66+
LDAP::Request::Filter.equal("sn", "training")) |
67+
LDAP::Request::FilterParser.parse("(uid=einstein)"
68+
)
4669
client.search(base: "dc=example,dc=com", filter: filter)
70+
4771
```
4872

49-
To use the non-standard `LDAPS` (LDAP Secure, commonly known as LDAP over SSL) protocol then pass in a `OpenSSL::SSL::Socket::Client` directly: `LDAP::Client.new(tls_socket)`
73+
Search responses are `Array(Hash(String, Array(String)))`
74+
75+
```crystal
76+
77+
[
78+
{
79+
"dn" => ["uid=einstein,dc=example,dc=com"],
80+
"objectClass" => ["inetOrgPerson", "organizationalPerson", "person", "top"],
81+
"cn" => ["Albert Einstein"],
82+
"sn" => ["Einstein"],
83+
"uid" => ["einstein"],
84+
"mail" => ["[email protected]"],
85+
"telephoneNumber" => ["314-159-2653"]
86+
},
87+
{
88+
"dn" => ["uid=training,dc=example,dc=com"],
89+
"uid" => ["training"],
90+
"objectClass" => ["inetOrgPerson", "organizationalPerson", "person", "top"],
91+
"cn" => ["FS Training"],
92+
"sn" => ["training"],
93+
"mail" => ["[email protected]"],
94+
"telephoneNumber" => ["888-111-2222"]
95+
}
96+
]
97+
98+
```

shard.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: ldap
2-
version: 1.0.0
2+
version: 0.9.0
33

44
dependencies:
55
bindata:

src/ldap/request.cr

+33-15
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ class LDAP::Request
4242
}, Tag::BindRequest))
4343
end
4444

45-
alias SortControl = NamedTuple(name: String, rule: String, reverse: Bool)
45+
alias SortControl = NamedTuple(name: String, rule: String, reverse: Bool) | NamedTuple(name: String, reverse: Bool)
4646

4747
PAGED_RESULTS = "1.2.840.113556.1.4.319" # Microsoft evil from RFC 2696
4848
DELETE_TREE = "1.2.840.113556.1.4.805"
@@ -52,26 +52,31 @@ class LDAP::Request
5252
def encode_sort_controls(*sort_controls : String | SortControl)
5353
sort_controls = sort_controls.map do |control|
5454
if control.is_a?(SortControl)
55-
{
55+
LDAP.sequence({
5656
BER.new.set_string(control[:name], UniversalTags::OctetString),
57-
BER.new.set_string(control[:rule], UniversalTags::OctetString),
57+
BER.new.set_string(control[:rule]? || "", UniversalTags::OctetString),
5858
BER.new.set_boolean(control[:reverse]),
59-
}
59+
})
6060
else
61-
{
61+
LDAP.sequence({
6262
BER.new.set_string(control, UniversalTags::OctetString),
6363
BER.new.set_string("", UniversalTags::OctetString),
6464
BER.new.set_boolean(false),
65-
}
65+
})
6666
end
6767
end
6868

69-
# TODO:: convert to actual message
70-
{
69+
# Control sequence needs to be encoded as an OctetString
70+
# https://tools.ietf.org/html/rfc2891
71+
controls = BER.new.set_string("", UniversalTags::OctetString)
72+
controls.payload = LDAP.sequence(sort_controls).to_slice
73+
74+
# convert to actual message
75+
LDAP.sequence({
7176
BER.new.set_string(SORT_REQUEST, UniversalTags::OctetString),
7277
BER.new.set_boolean(false),
73-
sort_controls,
74-
}
78+
controls,
79+
})
7580
end
7681

7782
# base: https://tools.ietf.org/html/rfc4511#section-4.5.1.1
@@ -94,16 +99,15 @@ class LDAP::Request
9499
size : Int = 0,
95100
time : Int = 0,
96101
paged_searches_supported : Bool = false,
97-
sort_control : BER? = nil
102+
sort : String | SortControl | BER | Nil = nil
98103
)
99104
attributes = attributes.map { |a| BER.new.set_string(a.to_s, UniversalTags::OctetString) }
100105

101106
# support string based filters
102107
filter = FilterParser.parse(filter) if filter.is_a?(String)
103108

104-
# TODO:: sort controls
105-
106-
build(LDAP.app_sequence({
109+
# Build search request
110+
search_request = LDAP.app_sequence({
107111
BER.new.set_string(base, UniversalTags::OctetString),
108112
BER.new.set_integer(scope.to_u8, UniversalTags::Enumerated),
109113
BER.new.set_integer(dereference.to_u8, UniversalTags::Enumerated),
@@ -112,6 +116,20 @@ class LDAP::Request
112116
BER.new.set_boolean(attributes_only),
113117
filter.to_ber,
114118
LDAP.sequence(attributes),
115-
}, Tag::SearchRequest))
119+
}, Tag::SearchRequest)
120+
121+
# Sort controls
122+
if sort
123+
sort_control = case sort
124+
when String | SortControl
125+
encode_sort_controls(sort)
126+
when BER
127+
sort
128+
end
129+
130+
build(search_request, sort_control)
131+
else
132+
build(search_request)
133+
end
116134
end
117135
end

0 commit comments

Comments
 (0)