Skip to content
This repository was archived by the owner on May 14, 2024. It is now read-only.

v3 server.search change scope names #845

Closed
ahaenggli opened this issue Mar 5, 2023 · 4 comments
Closed

v3 server.search change scope names #845

ahaenggli opened this issue Mar 5, 2023 · 4 comments

Comments

@ahaenggli
Copy link

Following the example of an in memory server server.search uses 3 scopes: base, one, sub
With v3 it looks like one is now single and sub is now called subtree. This looks like a breaking change for server implementations to me. The adjustment is of course made very quickly on server side code.

@jsumners
Copy link
Member

jsumners commented Mar 5, 2023

Are you suggesting that the v2 values are not working? They should be according to the tests -- https://github.com/ldapjs/messages/blob/4c82e476d1749cd1b699c49f070dd95b8c0fa3cf/lib/messages/search-request.test.js#L187-L202

@ahaenggli
Copy link
Author

The v2 values would work. But with v3, server.search receives the v3 values as req.scope.
So the v3 values must be added in each server.search implementation, otherwise they will not be processed:

server.search(SUFFIX, authorize, (req, res, next) => {
  const dn = req.dn.toString();
...
...
  switch (req.scope) {
  case 'base':
...
  case 'one':
  case 'single:
...
  case 'sub':
  case 'subtree':
...
}
...
...

@ahaenggli
Copy link
Author

ahaenggli commented Mar 5, 2023

I added a demo as attachment: inMem-ex.js.txt
Running with v2.3.3 the output is:

LDAP server up at: ldap://127.0.0.1:1389
server.search Search for => DN: dc=domain,dc=tld; Scope: sub; Filter: (&(objectclass=*)); Attributes: dn,namingcontexts,dc;
client.search searchRequest:  undefined
client.search entry: {"dn":"dc=domain,dc=tld","controls":[],"dc":"domain","namingContexts":"dc=domain,dc=tld"}
client.search entry: {"dn":"cn=users,dc=domain,dc=tld","controls":[],"dc":"domain","namingContexts":"dc=domain,dc=tld"}
client.search status: 0

Running with v3.0.0 the output is:

LDAP server up at: ldap://127.0.0.1:1389
server.search Search for => DN: dc=domain,dc=tld; Scope: subtree; Filter: (&(objectClass=*)); Attributes: dn,namingContexts,dc;
server.js server.search TypeError: scopeCheck is not a function
    at Server.<anonymous> (C:\Users\ahaen\source\repos\AzureAD-LDAP-wrapper\inMem-ex.js:82:18)
    at messageIIFE (C:\Users\ahaen\source\repos\AzureAD-LDAP-wrapper\node_modules\ldapjs\lib\server.js:432:63)
    at Parser.<anonymous> (C:\Users\ahaen\source\repos\AzureAD-LDAP-wrapper\node_modules\ldapjs\lib\server.js:455:8)
    at Parser.emit (node:events:513:28)
    at Parser.write (C:\Users\ahaen\source\repos\AzureAD-LDAP-wrapper\node_modules\ldapjs\lib\messages\parser.js:135:8)
    at Socket.<anonymous> (C:\Users\ahaen\source\repos\AzureAD-LDAP-wrapper\node_modules\ldapjs\lib\server.js:474:19)
    at Socket.emit (node:events:513:28)
    at addChunk (node:internal/streams/readable:324:12)
    at readableAddChunk (node:internal/streams/readable:297:9)
    at Readable.push (node:internal/streams/readable:234:10)
client.search searchRequest:  1

Running with v3.0.0 and implemented single and subtree the output is:

LDAP server up at: ldap://127.0.0.1:1389
server.search Search for => DN: dc=domain,dc=tld; Scope: subtree; Filter: (&(objectClass=*)); Attributes: dn,namingContexts,dc;
client.search searchRequest:  1
client.search entry: undefined
client.search entry: undefined
client.search status: 0

client.search entry: undefined is also a bit strange, but maybe because of the dummy data.


example server
'use strict';
const ldap = require('ldapjs');

// server part
const server = ldap.createServer();

const SUFFIX = '';
const db = {
    "dc=domain,dc=tld": {
        "objectClass": "domain",        
        "dc": "domain",
        "entryDN": "dc=domain,dc=tld",
        "namingContexts": "dc=domain,dc=tld",
        "structuralObjectClass": "domain",
        "subschemaSubentry": "cn=subschema",
        "creatorsName": "uid=root,cn=users,dc=domain,dc=tld",
        "modifiersName": "uid=root,cn=users,dc=domain,dc=tld"
    },
    "cn=users,dc=domain,dc=tld": {
        "objectClass": "organizationalRole",
        "cn": "users",
        "entryDN": "cn=users,dc=domain,dc=tld",
        "hasSubordinates": "TRUE",
        "subschemaSubentry": "cn=subschema",
        "creatorsName": "uid=root,cn=users,dc=domain,dc=tld",
        "modifiersName": "uid=root,cn=users,dc=domain,dc=tld"
    }
};


server.bind(SUFFIX, (req, res, next) => {
    res.end();
    return next();
});

server.search(SUFFIX, (req, res, next) => {
    const dn = req.dn.toString().toLowerCase().replace(/ /g, '');
    console.log('server.search', 'Search for => DN: ' + dn + '; Scope: ' + req.scope + '; Filter: ' + req.filter + '; Attributes: ' + req.attributes + ';');

    if (!db[dn])
        return next(new ldap.NoSuchObjectError(dn));


    try {
        let scopeCheck;

        switch (req.scope) {
            case 'base':

                if (req.filter.matches(db[dn])) {
                   res.send({
                        dn: dn,
                        attributes: db[dn]
                    });
                }

                res.end();
                return next();

            case 'one':
            //case 'single':
                scopeCheck = (k) => {
                    if (req.dn.equals(k))
                        return true;

                    const parent = ldap.parseDN(k).parent();
                    return (parent ? parent.equals(req.dn) : false);
                };
                break;

            case 'sub':
            //case 'subtree':
                scopeCheck = (k) => {
                    return (req.dn.equals(k) || req.dn.parentOf(k));
                };

                break;
        }

        const keys = Object.keys(db);
        for (const key of keys) {
            if (!scopeCheck(key))
                continue;

            if (req.filter.matches(db[dn])) {
                res.send({
                    dn: key,
                    attributes: db[dn]
                });
            }
        }

        res.end();
        return next();
    }
    catch (error) {
        console.log("server.js", "server.search", error);
    }
});

///--- Fire it up
server.listen(1389, "127.0.0.1", () => {
    console.log('LDAP server up at: %s', server.url);
});

// Client part

const client = ldap.createClient({ url: ['ldap://127.0.0.1:1389'] });
const opts = {
    filter: '(&(objectClass=*))',
    scope: 'sub',
    attributes: ['dn', 'namingContexts', 'dc']
};


client.search('dc=domain,dc=tld', opts, (err, res) => {
    res.on('searchRequest', (searchRequest) => {
        console.log('client.search', 'searchRequest: ', searchRequest.messageId);
    });
    res.on('searchEntry', (entry) => {
        console.log('client.search', 'entry: ' + JSON.stringify(entry.object));
    });
    res.on('searchReference', (referral) => {
        console.log('client.search', 'referral: ' + referral.uris.join());
    });
    res.on('error', (err) => {
        console.error('client.search', 'error: ' + err.message);
    });
    res.on('end', (result) => {
        console.log('client.search', 'status: ' + result.status);
    });
});

jsumners added a commit that referenced this issue Mar 8, 2023
This issue resolves issue #845 by updating `@ldapjs/messages` to the
latest version and adding a test that shows how the module should be
used to evaluate search request scopes.
jsumners added a commit that referenced this issue Mar 8, 2023
This issue resolves issue #845 by updating `@ldapjs/messages` to the
latest version and adding a test that shows how the module should be
used to evaluate search request scopes.
@jsumners
Copy link
Member

jsumners commented Mar 8, 2023

Thank you for the example; it was a big help.

I believe the original design was faulty. We replicated it without understanding how it was being used. The fix is shown in the adjusted example server below, and also in the unit test included in PR #847. Basically, such comparisons should be made against the actual scope constants. We changed how ServerRequest.scope behaves in @ldapjs/messages@1.0.1 to return the code instead of a friendly name. We also added SearchRequest.scopeName in case someone really wants the friendly name.

You should be able to update to the following releases to get the fixes:

I have also updated the v3.0.0 release notes to note the change in 3.0.0 and the recommended approach with 3.0.1 onward.

example patch
*** index.orig.js	2023-03-08 15:47:15
--- index.js	2023-03-08 15:47:48
*************** const ldap = require('ldapjs');
*** 1,5 ****
--- 1,6 ----
  'use strict';
  const ldap = require('ldapjs');
+ const { SearchRequest } = require('@ldapjs/messages');
  
  // server part
  const server = ldap.createServer();
*************** const db = {
*** 7,13 ****
  const SUFFIX = '';
  const db = {
      "dc=domain,dc=tld": {
!         "objectClass": "domain",        
          "dc": "domain",
          "entryDN": "dc=domain,dc=tld",
          "namingContexts": "dc=domain,dc=tld",
--- 8,14 ----
  const SUFFIX = '';
  const db = {
      "dc=domain,dc=tld": {
!         "objectClass": "domain",
          "dc": "domain",
          "entryDN": "dc=domain,dc=tld",
          "namingContexts": "dc=domain,dc=tld",
*************** server.search(SUFFIX, (req, res, next) => {
*** 45,51 ****
          let scopeCheck;
  
          switch (req.scope) {
!             case 'base':
  
                  if (req.filter.matches(db[dn])) {
                     res.send({
--- 46,52 ----
          let scopeCheck;
  
          switch (req.scope) {
!             case SearchRequest.SCOPE_BASE:
  
                  if (req.filter.matches(db[dn])) {
                     res.send({
*************** server.search(SUFFIX, (req, res, next) => {
*** 57,63 ****
                  res.end();
                  return next();
  
!             case 'one':
              //case 'single':
                  scopeCheck = (k) => {
                      if (req.dn.equals(k))
--- 58,64 ----
                  res.end();
                  return next();
  
!             case SearchRequest.SCOPE_SINGLE:
              //case 'single':
                  scopeCheck = (k) => {
                      if (req.dn.equals(k))
*************** server.search(SUFFIX, (req, res, next) => {
*** 68,74 ****
                  };
                  break;
  
!             case 'sub':
              //case 'subtree':
                  scopeCheck = (k) => {
                      return (req.dn.equals(k) || req.dn.parentOf(k));
--- 69,75 ----
                  };
                  break;
  
!             case SearchRequest.SCOPE_SUBTREE:
              //case 'subtree':
                  scopeCheck = (k) => {
                      return (req.dn.equals(k) || req.dn.parentOf(k));
*************** server.search(SUFFIX, (req, res, next) => {
*** 95,104 ****
      }
      catch (error) {
          console.log("server.js", "server.search", error);
      }
  });
  
  ///--- Fire it up
  server.listen(1389, "127.0.0.1", () => {
      console.log('LDAP server up at: %s', server.url);
! });
\ No newline at end of file
--- 96,106 ----
      }
      catch (error) {
          console.log("server.js", "server.search", error);
+         res.send(error)
      }
  });
  
  ///--- Fire it up
  server.listen(1389, "127.0.0.1", () => {
      console.log('LDAP server up at: %s', server.url);
! });
modified example server
'use strict';
const ldap = require('ldapjs');
const { SearchRequest } = require('@ldapjs/messages');

// server part
const server = ldap.createServer();

const SUFFIX = '';
const db = {
    "dc=domain,dc=tld": {
        "objectClass": "domain",
        "dc": "domain",
        "entryDN": "dc=domain,dc=tld",
        "namingContexts": "dc=domain,dc=tld",
        "structuralObjectClass": "domain",
        "subschemaSubentry": "cn=subschema",
        "creatorsName": "uid=root,cn=users,dc=domain,dc=tld",
        "modifiersName": "uid=root,cn=users,dc=domain,dc=tld"
    },
    "cn=users,dc=domain,dc=tld": {
        "objectClass": "organizationalRole",
        "cn": "users",
        "entryDN": "cn=users,dc=domain,dc=tld",
        "hasSubordinates": "TRUE",
        "subschemaSubentry": "cn=subschema",
        "creatorsName": "uid=root,cn=users,dc=domain,dc=tld",
        "modifiersName": "uid=root,cn=users,dc=domain,dc=tld"
    }
};


server.bind(SUFFIX, (req, res, next) => {
    res.end();
    return next();
});

server.search(SUFFIX, (req, res, next) => {
    const dn = req.dn.toString().toLowerCase().replace(/ /g, '');
    console.log('server.search', 'Search for => DN: ' + dn + '; Scope: ' + req.scope + '; Filter: ' + req.filter + '; Attributes: ' + req.attributes + ';');

    if (!db[dn])
        return next(new ldap.NoSuchObjectError(dn));


    try {
        let scopeCheck;

        switch (req.scope) {
            case SearchRequest.SCOPE_BASE:

                if (req.filter.matches(db[dn])) {
                   res.send({
                        dn: dn,
                        attributes: db[dn]
                    });
                }

                res.end();
                return next();

            case SearchRequest.SCOPE_SINGLE:
            //case 'single':
                scopeCheck = (k) => {
                    if (req.dn.equals(k))
                        return true;

                    const parent = ldap.parseDN(k).parent();
                    return (parent ? parent.equals(req.dn) : false);
                };
                break;

            case SearchRequest.SCOPE_SUBTREE:
            //case 'subtree':
                scopeCheck = (k) => {
                    return (req.dn.equals(k) || req.dn.parentOf(k));
                };

                break;
        }

        const keys = Object.keys(db);
        for (const key of keys) {
            if (!scopeCheck(key))
                continue;

            if (req.filter.matches(db[dn])) {
                res.send({
                    dn: key,
                    attributes: db[dn]
                });
            }
        }

        res.end();
        return next();
    }
    catch (error) {
        console.log("server.js", "server.search", error);
        res.send(error)
    }
});

///--- Fire it up
server.listen(1389, "127.0.0.1", () => {
    console.log('LDAP server up at: %s', server.url);
});

@jsumners jsumners closed this as completed Mar 8, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants