inejge / ldap3

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

Add support for RFC 3062: LDAP Password Modify Extended Operation #60

Closed johanfleury closed 4 years ago

johanfleury commented 4 years ago

Hello.

I’m working on a web service where I’d like the user to be able to change their LDAP password and I understand that the best way to do this is by using the Password Modify extended operation defined in RFC 3062.

I’m new to LDAP and ANS1, but I’ve had a look at the doc and I think a modify_password function could look like this (not tested):

use bytes::BytesMut;
use ldap3::asn1::{write, ASNTag, OctetString, Sequence, Tag};
use ldap3::exop::Exop;

pub fn modify_password(dn: &str, old_password: &str, new_password: &str) -> Result<(), Error> {
    let seq = Tag::Sequence(Sequence {
        inner: vec![
            Tag::OctetString(OctetString {
                inner: dn.as_bytes().to_vec(),
                ..Default::default()
            }),
            Tag::OctetString(OctetString {
                inner: old_password.as_bytes().to_vec(),
                ..Default::default()
            }),
            Tag::OctetString(OctetString {
                inner: new_password.as_bytes().to_vec(),
                ..Default::default()
            }),
        ],
        ..Default::default()
    })
    .into_structure();

    let mut buf = BytesMut::with_capacity(XXXXX);
    write::encode_into(&mut buf, seq).expect("encoded");

    let operation = Exop {
        name: Some("1.3.6.1.4.1.4203.1.11.1".to_string()),
        val: Some(Vec::from(&buf[..])),
    };

    conn.extended(operation)?.success()?;
}

I’m willing to work on this feature if needed, but this is a whole new world to me so I might need a bit of help.

inejge commented 4 years ago

Using the Password Modify exop is indeed the cleanest option if your LDAP infrastructure is set up to support it. Your sketch of a Password Modify issuing function has the right elements, but several details would either create an invalid exop encoding, or make it not conform to the expected structure of an exop.

The first thing to note is that, according to spec, all components of a PasswordModifyRequestValue are optional. The presence of an individual field is signalled by tagging it with a context-specific numeric tag. For example, userIdentity is specified as userIdentity [0] OCTET STRING OPTIONAL, with the numeric tag of zero when present. The correct encoding in the source would be:

Tag::OctetString(OctetString {
    id: 0,
    class: TagClass::Context,
    inner: Vec::from(...),
}

Furthermore, if all components are absent, which the spec allows, the value element of the encoded exop should not be present at all.

If you look at the implentations of existing exops in the library, you'll see that all of them have an exop-specific struct and a From<CustomStruct> for Exop implementation. Putting this all together, the request side could be written as follows:

use bytes::BytesMut;
use ldap3::asn1::{write, ASNTag, OctetString, Sequence, Tag, TagClass};
use ldap3::exop::Exop;

pub const PASSMOD_OID: &str = "1.3.6.1.4.1.4203.1.11.1";

struct PasswordModify<'a> {
    user_id: Option<&'a str>,
    old_pass: Option<&'a str>,
    new_pass: Option<&'a str>,
}

impl<'a> From<PasswordModify<'a>> for Exop {
    fn from(pm: PasswordModify<'a>) -> Exop {
        let pm_val = if pm.user_id.is_none() && pm.old_pass.is_none() && pm.new_pass.is_none() {
            None
        } else {
            let mut pm_vec = vec![];
            if let Some(user_id) = pm.user_id {
                pm_vec.push(Tag::OctetString(OctetString {
                    id: 0,
                    class: TagClass::Context,
                    inner: Vec::from(user_id.as_bytes()),
                }));
            }
            if let Some(old_pass) = pm.old_pass {
                pm_vec.push(Tag::OctetString(OctetString {
                    id: 1,
                    class: TagClass::Context,
                    inner: Vec::from(old_pass.as_bytes()),
                }));
            }
            if let Some(new_pass) = pm.new_pass {
                pm_vec.push(Tag::OctetString(OctetString {
                    id: 2,
                    class: TagClass::Context,
                    inner: Vec::from(new_pass.as_bytes()),
                }));
            }
            let pm_val = Tag::Sequence(Sequence {
                inner: pm_vec,
                ..Default::default()
            })
            .into_structure();
            let mut buf = BytesMut::new();
            write::encode_into(&mut buf, pm_val).expect("encoded");
            Some(Vec::from(&buf[..]))
        };
        Exop {
            name: Some(PASSMOD_OID.to_owned()),
            val: pm_val,
        }
    }
}

This should be enough to get you started. Password Modify has the response structure, which will contain a server-generated password if you ask for one by not supplying the new password value in the request, but that's a bit of a niche use case, and not something I would recommend on security grounds.

I will add a full Password Modify implementation to the library, and I'll keep this issue open until I do so (soon, I hope).

johanfleury commented 4 years ago

Thank you very much for your reply and for your work on this library, I understand a bit more how the RFC translates to Rust code within your library and and how the crate is structured.

I will add a full Password Modify implementation to the library, and I'll keep this issue open until I do so (soon, I hope).

If you feel that this can be a good first issue for a beginner, let me know :)

inejge commented 4 years ago

If you feel that this can be a good first issue for a beginner, let me know :)

It might have been, but the code above is 75% of the feature already :slightly_smiling_face: Commit incoming.