Mock LDAP server for testing
This commit is contained in:
parent
1a5aa30733
commit
aa4f17b354
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,3 +1,4 @@
|
|||||||
dist/
|
dist/
|
||||||
.cabal-sandbox/
|
.cabal-sandbox/
|
||||||
cabal.sandbox.config
|
cabal.sandbox.config
|
||||||
|
node_modules
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -21,6 +21,7 @@ module Ldap.Client
|
|||||||
, Type.Scope(..)
|
, Type.Scope(..)
|
||||||
, Attr(..)
|
, Attr(..)
|
||||||
, SearchEntry(..)
|
, SearchEntry(..)
|
||||||
|
, SearchError(..)
|
||||||
, search
|
, search
|
||||||
, searchEither
|
, searchEither
|
||||||
, searchAsync
|
, searchAsync
|
||||||
|
|||||||
@ -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
20
test/Main.hs
Normal 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)
|
||||||
@ -1 +1 @@
|
|||||||
{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
|
{-# OPTIONS_GHC -F -pgmF hspec-discover -optF --module-name=Spec #-}
|
||||||
|
|||||||
4
test/SpecHelper.hs
Normal file
4
test/SpecHelper.hs
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
module SpecHelper (port) where
|
||||||
|
|
||||||
|
port :: Num a => a
|
||||||
|
port = 24620
|
||||||
90
test/ldap.js
Executable file
90
test/ldap.js
Executable 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);
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue
Block a user