inejge / ldap3

A pure-Rust LDAP library using the Tokio stack
Apache License 2.0
220 stars 38 forks source link

streaming_search_with returns SearchEntries with no attrs or bin_attrs #55

Closed annmarie-switzer closed 4 years ago

annmarie-switzer commented 4 years ago

When I use the streaming_search_with method, I am getting results bask that have no attributes. Since every object in AD should have at least one attribute, in this case I am testing on objectSid, I don't know why this is happening.

Maybe I have misconfigured something?

use ldap3::{
    adapters::EntriesOnly,
    LdapConn, Scope, SearchEntry, EntryStream,
};

struct LdapIter<'a, 'b> {
    search_stream: EntryStream<'a, 'b, &'a str>,
    results: Vec<SearchEntry>,
    curr: usize,
}

impl<'a, 'b> LdapIter<'a, 'b> {
    fn new(
        base_dn: &'a str,
        filter: &'a str,
        scope: Scope,
        ldap: &'b mut LdapConn
    ) -> Self {
        let search_stream = ldap
            .streaming_search_with(
                EntriesOnly::new(),
                base_dn,
                scope,
                filter,
                vec!["objectSid"]
            )
            .unwrap();

        Self {
            search_stream,
            results: Vec::new(),
            curr: 0,
        }
    }
}

impl<'a, 'b> Iterator for LdapIter<'a, 'b> {
    type Item = SearchEntry;
    fn next(&mut self) -> Option<Self::Item> {
        if self.results.is_empty() {
            let res = self.search_stream.next().unwrap();

            match res {
                Some(res) => {
                    let search_entry = SearchEntry::construct(res);
                    self.results.push(search_entry);
                },
                None => return None,
            }
        }

        if self.results.len() <= self.curr {
            let res = self.search_stream.next().unwrap();

            match res {
                Some(res) => {
                    self.results = Vec::new();
                    self.curr = 0;
                    let search_entry = SearchEntry::construct(res);
                    self.results.push(search_entry);
                }
                None => return None,
            }
        }

        let res = self.results[self.curr].clone();
        self.curr += 1;

        Some(res)
    }
}

pub async fn test(
    server: &str,
    un: &str,
    pw: &str,
    base_dn: &str
) {
    let mut ldap = connect_ldap(server, un, pw).unwrap();

    let filter: String = format!(
        "(|(&(objectClass=user)(!(objectClass=computer)))(objectClass=group)(objectClass=organizationalUnit)(objectClass=domain))"
    );

    let queries: Vec<_> = LdapIter::new(base_dn, &filter, Scope::Subtree, &mut ldap)
        .map(|entry| {
            dbg!(&entry);

            // &entry = SearchEntry {
            //     dn: "CN=something,OU=another..."
            //     attrs: {}
            //     bin_attrs {}
            // }
       }
inejge commented 4 years ago

I ran the above search on a local OU, both with the given program and ldapsearch from OpenLDAP, and it appears that some entries don't have an objectSid at all. Where it exists, it will be in bin_attrs. Can you reproduce a situation where ldap3 returns no values for objectSid, and some other LDAP search utility has some values for the same result entry?

annmarie-switzer commented 4 years ago

I realize I was making an assumption that was wrong - I though all objects in AD had a SID but apparently that's not the case, so that explains why attrs and bin_attrs were empty for some objects.

Now, for those that do have an objectSid, I am having some trouble parsing them.

If the objectSid is in attrs, it looks like this: "\u{1}\u{2}\u{0}\u{0}\u{0}\u{0}\u{0}\u{5} \u{0}\u{0}\u{0} \u{2}\u{0}\u{0}"

If the objectSid is in bin_attrs, it looks like this: [ 1, 5, 0, 0, 0, 0, 0, 5, 21, 0, 0, 0, 132, 79, 208, 182, 78, 22, 60, 206, 137, 224, 237, 253, 232, 3, 0, 0, ]

I can't figure out how to parse either of these into a SID. It should look something this: S-1-5-21-1943420175-2293888159-2611305999-65646

inejge commented 4 years ago

I found an article describing the format. If the value happens to be representable as a String, its underlying raw byte slice should be the input to the conversion.

annmarie-switzer commented 4 years ago

Thanks, I will try this for the bin_attrs version.

Is there a way to have streaming_search return objectSid ALWAYS as a bin_attr? Or do you have any idea how the version returned in attrs can be converted? I'm not sure how that string is supposed to be parsed.

inejge commented 4 years ago

Is there a way to have streaming_search return objectSid ALWAYS as a bin_attr?

Afraid not; conversion to String is always tried first. I toyed with deserializing entries into structs, but never needed it that badly.

Or do you have any idea how the version returned in attrs can be converted?

With String::as_bytes(), as hinted above.

annmarie-switzer commented 4 years ago

Thank you for your help! Using that article as a guide, I made a helper function that will convert these binary streams into a human-readable SID that can be checked against the Active Directory UI:

use byteorder::*;

pub fn convert_sid_to_str(mut b_sid: Vec<u8>) -> String {
    let revision = b_sid.remove(0);
    let _dashes = b_sid.remove(0);
    let remaining = b_sid.split_off(6);
    let six = BigEndian::read_uint(&b_sid, 6);

    let mut ints = vec![];
    let chunks = remaining.chunks(4);
    for chunk in chunks {
        let int = format!("{}", LittleEndian::read_uint(chunk, 4));
        ints.push(int);
    };

    format!("S-{}-{}-{}", revision, six, ints.join("-"))
}