Mock LDAP server for testing

This commit is contained in:
Matvey Aksenov 2015-03-31 21:44:18 +00:00
parent 1a5aa30733
commit aa4f17b354
9 changed files with 201 additions and 90 deletions

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
dist/ dist/
.cabal-sandbox/ .cabal-sandbox/
cabal.sandbox.config cabal.sandbox.config
node_modules

View File

@ -1,5 +1,5 @@
ldap-client ldap-client
----------- ===========
**NOTE: This is work in progress. Don't use it! If you really need LDAP integration, check out [LDAP][LDAP]** **NOTE: This is work in progress. Don't use it! If you really need LDAP integration, check out [LDAP][LDAP]**
@ -25,6 +25,15 @@ LDAP over TLS | - | ✔
``` ```
% git grep '\bString\b' | wc -l % git grep '\bString\b' | wc -l
2 2
```
Testing
-------
```shell
% sudo apt-get install npm
% npm install ldapjs
% cabal test
``` ```
[rfc4511]: https://tools.ietf.org/html/rfc4511 [rfc4511]: https://tools.ietf.org/html/rfc4511

View File

@ -50,10 +50,11 @@ test-suite spec
hs-source-dirs: hs-source-dirs:
test test
main-is: main-is:
Spec.hs Main.hs
other-modules: other-modules:
Ldap.ClientSpec Ldap.ClientSpec
build-depends: build-depends:
base >= 4.7 && < 5 base >= 4.7 && < 5
, hspec , hspec
, ldap-client , ldap-client
, process

View File

@ -21,6 +21,7 @@ module Ldap.Client
, Type.Scope(..) , Type.Scope(..)
, Attr(..) , Attr(..)
, SearchEntry(..) , SearchEntry(..)
, SearchError(..)
, search , search
, searchEither , searchEither
, searchAsync , searchAsync

View File

@ -5,108 +5,93 @@ module Ldap.ClientSpec (spec) where
import Data.Monoid ((<>)) import Data.Monoid ((<>))
import Test.Hspec import Test.Hspec
import Ldap.Client (Host(..), Dn(..), Password(..), Filter(..), Scope(..), scope, size, Attr(..), BindError(..), ResultCode(..)) import Ldap.Client (Dn(..), Password(..), Filter(..), Scope(..), Attr(..))
import qualified Ldap.Client as Ldap import qualified Ldap.Client as Ldap
import SpecHelper (port)
spec :: Spec spec :: Spec
spec = do spec = do
let locally = Ldap.with localhost port
search l f = Ldap.search l (Dn "o=localhost") (Ldap.scope WholeSubtree <> Ldap.typesOnly True) f []
context "public LDAP server at MIT\ context "bind" $ do
\<https://github.com/ezyang/ldap-haskell/blob/371a200f14317f8943d2aebdcc56a09dac46c0ed/testsrc/Tests.hs>" $ do it "can bind" $ do
res <- locally $ \l -> do
Ldap.bind l (Dn "cn=admin") (Password "secret")
res `shouldBe` Right ()
it "searches the whole tree for the entries that have uid attribute" $ do it "can try to bind with a wrong password" $ do
Right () <- Ldap.with mit 389 $ \l -> do res <- locally $ \l -> do
res <- Ldap.search l (Dn "ou=People,dc=scripts,dc=mit,dc=edu") Ldap.bind l (Dn "cn=admin") (Password "public")
(scope WholeSubtree) res `shouldBe` Left (Ldap.BindError (Ldap.BindErrorCode Ldap.InvalidCredentials))
(Present (Attr "uid"))
[]
res `shouldSatisfy` (not . null)
return ()
it "searches the single level for the first 10 entries that have uid attribute" $ do it "can login as another user" $ do
Right () <- Ldap.with mit 389 $ \l -> do res <- locally $ \l -> do
res <- Ldap.search l (Dn "ou=People,dc=scripts,dc=mit,dc=edu") Ldap.bind l (Dn "cn=admin") (Password "secret")
(scope SingleLevel <> size 10) Ldap.SearchEntry udn _ : []
(Present (Attr "uid")) <- search l (Attr "cn" := "pikachu")
[] Ldap.bind l udn (Password "i-choose-you")
length res `shouldBe` 10 res `shouldBe` Right ()
return ()
it "searches the single level for the first 10 entries that do not have uid attribute" $ do context "search" $ do
Right () <- Ldap.with mit 389 $ \l -> do it "cannot search as pikachu" $ do
res <- Ldap.search l (Dn "ou=People,dc=scripts,dc=mit,dc=edu") res <- locally $ \l -> do
(scope SingleLevel <> size 10) Ldap.bind l (Dn "cn=pikachu,o=localhost") (Password "i-choose-you")
(Not (Present (Attr "uid"))) search l (Present (Attr "password"))
[] res `shouldBe` Left (Ldap.SearchError (Ldap.SearchErrorCode Ldap.InsufficientAccessRights))
res `shouldBe` []
return ()
context "online LDAP test server \ it "can use present filter" $ do
\<http://www.forumsys.com/tutorials/integration-how-to/ldap/online-ldap-test-server>" $ do res <- locally $ \l -> do
res <- search l (Present (Attr "password"))
dns res `shouldBe` [Dn "cn=pikachu,o=localhost"]
res `shouldBe` Right ()
context "bind" $ do it "can use equality match filter" $ do
it "can bind" $ do res <- locally $ \l -> do
Right () <- Ldap.with forumsys 389 $ \l -> do res <- search l (Attr "type" := "flying")
Ldap.bind l (Dn "cn=read-only-admin,dc=example,dc=com") dns res `shouldMatchList` [Dn "cn=butterfree,o=localhost", Dn "cn=charizard,o=localhost"]
(Password "password") res `shouldBe` Right ()
return ()
it "can try to bind with a wrong password" $ do it "can use and filter" $ do
Right () <- Ldap.with forumsys 389 $ \l -> do res <- locally $ \l -> do
res <- Ldap.bindEither l (Dn "cn=read-only-admin,dc=example,dc=com") res <- search l (And [ Attr "type" := "fire"
(Password "drowssap") , Attr "evolution" := "1"
res `shouldBe` Left (BindErrorCode InvalidCredentials) ])
return () dns res `shouldBe` [Dn "cn=charmeleon,o=localhost"]
res `shouldBe` Right ()
it "can login as another user" $ do it "can use or filter" $ do
Right () <- Ldap.with forumsys 389 $ \l -> do res <- locally $ \l -> do
Ldap.bind l (Dn "cn=read-only-admin,dc=example,dc=com") res <- search l (Or [ Attr "type" := "fire"
(Password "password") , Attr "evolution" := "1"
Ldap.SearchEntry udn _ : _ ])
<- Ldap.search l (Dn "dc=example,dc=com") dns res `shouldMatchList`
(Ldap.scope WholeSubtree <> Ldap.typesOnly True) [ Dn "cn=charizard,o=localhost"
(Attr "uid" := "euler") , Dn "cn=charmeleon,o=localhost"
[] , Dn "cn=charmander,o=localhost"
Ldap.bind l udn (Password "password") , Dn "cn=metapod,o=localhost"
return () , Dn "cn=wartortle,o=localhost"
, Dn "cn=ivysaur,o=localhost"
]
res `shouldBe` Right ()
context "search" $ do it "can use or filter" $ do
let search l f = Ldap.search l (Dn "dc=example,dc=com") (Ldap.scope WholeSubtree <> Ldap.typesOnly True) f [] res <- locally $ \l -> do
res <- search l (Not (Or [ Attr "type" := "fire"
, Attr "evolution" :>= "1"
]))
dns res `shouldMatchList`
[ Dn "cn=bulbasaur,o=localhost"
, Dn "cn=squirtle,o=localhost"
, Dn "cn=caterpie,o=localhost"
, Dn "cn=pikachu,o=localhost"
]
res `shouldBe` Right ()
it "can use present filter" $ do localhost :: Ldap.Host
Right () <- Ldap.with forumsys 389 $ \l -> do localhost = Ldap.Plain "localhost"
res <- search l (Present (Attr "initials"))
dns res `shouldMatchList` [Dn "uid=test,dc=example,dc=com"]
return ()
it "can use equality match filter" $ do
Right () <- Ldap.with forumsys 389 $ \l -> do
res <- search l (Attr "sn" := "Tesla")
dns res `shouldMatchList` [Dn "uid=tesla,dc=example,dc=com"]
return ()
it "can use or filter" $ do
Right () <- Ldap.with forumsys 389 $ \l -> do
res <- search l (Or [ Attr "sn" := "Tesla"
, Attr "cn" := "Issac Newton" -- why the fuck "Issac"?
])
dns res `shouldMatchList` [Dn "uid=tesla,dc=example,dc=com", Dn "uid=newton,dc=example,dc=com"]
return ()
it "can use and and not filters" $ do
Right () <- Ldap.with forumsys 389 $ \l -> do
res <- search l (And [ Attr "uniqueMember" := "uid=tesla,dc=example,dc=com"
, Not (Attr "uniqueMember" := "uid=einstein,dc=example,dc=com")
])
dns res `shouldMatchList` [Dn "ou=italians,ou=scientists,dc=example,dc=com"]
return ()
mit :: Host
mit = Plain "scripts.mit.edu"
forumsys :: Host
forumsys = Plain "ldap.forumsys.com"
dns :: [Ldap.SearchEntry] -> [Dn] dns :: [Ldap.SearchEntry] -> [Dn]
dns (Ldap.SearchEntry dn _ : es) = dn : dns es dns (Ldap.SearchEntry dn _ : es) = dn : dns es

20
test/Main.hs Normal file
View File

@ -0,0 +1,20 @@
module Main (main) where
import Control.Concurrent (threadDelay)
import Control.Exception (bracket)
import System.IO (hGetLine)
import System.Process (runInteractiveProcess, terminateProcess, waitForProcess)
import Test.Hspec
import qualified Spec
import SpecHelper (port)
main :: IO ()
main =
bracket (do (_, out, _, h) <- runInteractiveProcess "./test/ldap.js" [] Nothing (Just [("PORT", show port)])
hGetLine out
return h)
(\h -> do terminateProcess h
waitForProcess h)
(\_ -> hspec Spec.spec)

View File

@ -1 +1 @@
{-# OPTIONS_GHC -F -pgmF hspec-discover #-} {-# OPTIONS_GHC -F -pgmF hspec-discover -optF --module-name=Spec #-}

4
test/SpecHelper.hs Normal file
View File

@ -0,0 +1,4 @@
module SpecHelper (port) where
port :: Num a => a
port = 24620

90
test/ldap.js Executable file
View File

@ -0,0 +1,90 @@
#!/usr/bin/env nodejs
var ldapjs = require('ldapjs');
var server = ldapjs.createServer();
var port = process.env.PORT
// <http://bulbapedia.bulbagarden.net/wiki/List_of_Pok%C3%A9mon_by_National_Pok%C3%A9dex_number>
var pokemon = [
{ dn: 'cn=bulbasaur,o=localhost',
attributes: { cn: 'bulbasaur', evolution: "0", type: ["grass", "poison"], }
},
{ dn: 'cn=ivysaur,o=localhost',
attributes: { cn: 'ivysaur', evolution: "1", type: ["grass", "poison"], }
},
{ dn: 'cn=venusaur,o=localhost',
attributes: { cn: 'venusaur', evolution: "2", type: ["grass", "poison"], }
},
{ dn: 'cn=charmander,o=localhost',
attributes: { cn: 'charmander', evolution: "0", type: ["fire"], }
},
{ dn: 'cn=charmeleon,o=localhost',
attributes: { cn: 'charmeleon', evolution: "1", type: ["fire"], }
},
{ dn: 'cn=charizard,o=localhost',
attributes: { cn: 'charizard', evolution: "2", type: ["fire", "flying"], }
},
{ dn: 'cn=squirtle,o=localhost',
attributes: { cn: 'squirtle', evolution: "0", type: ["water"], }
},
{ dn: 'cn=wartortle,o=localhost',
attributes: { cn: 'wartortle', evolution: "1", type: ["water"], }
},
{ dn: 'cn=blastoise,o=localhost',
attributes: { cn: 'blastoise', evolution: "2", type: ["water"], }
},
{ dn: 'cn=caterpie,o=localhost',
attributes: { cn: 'caterpie', evolution: "0", type: ["bug"], }
},
{ dn: 'cn=metapod,o=localhost',
attributes: { cn: 'metapod', evolution: "1", type: ["bug"], }
},
{ dn: 'cn=butterfree,o=localhost',
attributes: { cn: 'butterfree', evolution: "2", type: ["bug", "flying"], }
},
{ dn: 'cn=pikachu,o=localhost',
attributes: { cn: 'pikachu', evolution: "0", type: ["electic"], password: "i-choose-you" }
},
];
server.bind('cn=admin', function(req, res, next) {
if ((req.dn.toString() === 'cn=admin') && (req.credentials === 'secret')) {
res.end();
return next();
} else {
return next(new ldapjs.InvalidCredentialsError());
}
});
server.bind('cn=pikachu,o=localhost', function(req, res, next) {
if ((req.dn.toString() === 'cn=pikachu,o=localhost') && (req.credentials === 'i-choose-you')) {
res.end();
return next();
} else {
return next(new ldapjs.InvalidCredentialsError());
}
});
function authorize(req, res, next) {
var bindDN = req.connection.ldap.bindDN;
if ((bindDN.equals('cn=admin')) ||
(bindDN.equals('cn=anonymous'))) {
return next();
} else {
return next(new ldapjs.InsufficientAccessRightsError());
}
}
server.search('o=localhost', [authorize], function(req, res, next) {
for (var i = 0; i < pokemon.length; i++) {
if (req.filter.matches(pokemon[i].attributes))
res.send(pokemon[i]);
};
res.end();
return next();
});
server.listen(port, function() {
console.log("ldap://localhost:%d", port);
});