Implement Extended Operation support

This commit is contained in:
Matvey Aksenov 2015-04-03 16:18:11 +00:00
parent 1c9bd11626
commit ab79efd94c
9 changed files with 190 additions and 48 deletions

View File

@ -10,19 +10,22 @@ This library implements (the parts of) [RFC 4511][rfc4511]
Bind Operation | 4.2 | ✔ Bind Operation | 4.2 | ✔
Unbind Operation | 4.3 | ✔ Unbind Operation | 4.3 | ✔
Notice of Disconnection | 4.4.1 | ✘ Notice of Disconnection | 4.4.1 | ✘
Search Operation | 4.5 | ✔ Search Operation | 4.5 | ✔\*
Modify Operation | 4.6 | ✔ Modify Operation | 4.6 | ✔
Add Operation | 4.7 | ✔ Add Operation | 4.7 | ✔
Delete Operation | 4.8 | ✔ Delete Operation | 4.8 | ✔
Modify DN Operation | 4.9 | ✘ Modify DN Operation | 4.9 | ✘
Compare Operation | 4.10 | ✔ Compare Operation | 4.10 | ✔
Abandon Operation | 4.11 | ✘ Abandon Operation | 4.11 | ✘
Extended Operation | 4.12 | Extended Operation | 4.12 |
IntermediateResponse Message | 4.13 | ✘ IntermediateResponse Message | 4.13 | ✘
StartTLS Operation | 4.14 | StartTLS Operation | 4.14 | ✔†
LDAP over TLS | - | ✔ LDAP over TLS | - | ✔
†: approximate and extensible matches are untested, so probably do not work \*: approximate and extensible matches are untested, so probably do not work
†: only serves as an example of Extended Operation, meaning that it does not change
connection's state on success, so it's useless for all practical purposes.
In other words, use LDAP over TLS instead.
``` ```
% git grep '\bString\b' | wc -l % git grep '\bString\b' | wc -l

View File

@ -62,6 +62,14 @@ instance FromAsn1 LdapString where
Right t -> return (LdapString t) Right t -> return (LdapString t)
Left _ -> empty Left _ -> empty
{- |
LDAPOID ::= OCTET STRING -- Constrained to <numericoid>
-}
instance FromAsn1 LdapOid where
fromAsn1 = do
Asn1.OctetString s <- next
return (LdapOid s)
{- | {- |
LDAPDN ::= LDAPString LDAPDN ::= LDAPString
-} -}
@ -250,6 +258,17 @@ instance FromAsn1 ProtocolServerOp where
, fmap AddResponse (app 9) , fmap AddResponse (app 9)
, fmap DeleteResponse (app 11) , fmap DeleteResponse (app 11)
, fmap CompareResponse (app 15) , fmap CompareResponse (app 15)
, do
Asn1.Start (Asn1.Container Asn1.Application 24) <- next
res <- fromAsn1
name <- optional $ do
Asn1.Other Asn1.Context 0 s <- next
return s
value <- optional $ do
Asn1.Other Asn1.Context 1 s <- next
return s
Asn1.End (Asn1.Container Asn1.Application 24) <- next
return (ExtendedResponse res (fmap LdapOid name) value)
] ]
where where
app l = do app l = do

View File

@ -175,6 +175,11 @@ DelRequest ::= [APPLICATION 10] LDAPDN
CompareRequest ::= [APPLICATION 14] SEQUENCE { CompareRequest ::= [APPLICATION 14] SEQUENCE {
entry LDAPDN, entry LDAPDN,
ava AttributeValueAssertion } ava AttributeValueAssertion }
ExtendedRequest ::= [APPLICATION 23] SEQUENCE {
requestName [0] LDAPOID,
requestValue [1] OCTET STRING OPTIONAL }
-} -}
instance ToAsn1 ProtocolClientOp where instance ToAsn1 ProtocolClientOp where
toAsn1 (BindRequest v n a) = toAsn1 (BindRequest v n a) =
@ -216,6 +221,11 @@ instance ToAsn1 ProtocolClientOp where
other Asn1.Application 10 (Text.encodeUtf8 dn) other Asn1.Application 10 (Text.encodeUtf8 dn)
toAsn1 (CompareRequest dn av) = toAsn1 (CompareRequest dn av) =
application 14 (toAsn1 dn <> sequence (toAsn1 av)) application 14 (toAsn1 dn <> sequence (toAsn1 av))
toAsn1 (ExtendedRequest (LdapOid oid) mv) =
application 23 (fold
[ other Asn1.Context 0 oid
, maybe mempty (other Asn1.Context 1) mv
])
{- | {- |
AuthenticationChoice ::= CHOICE { AuthenticationChoice ::= CHOICE {

View File

@ -23,6 +23,7 @@ data ProtocolClientOp =
| DeleteRequest LdapDn | DeleteRequest LdapDn
| CompareRequest LdapDn AttributeValueAssertion | CompareRequest LdapDn AttributeValueAssertion
| ModifyRequest LdapDn [(Operation, PartialAttribute)] | ModifyRequest LdapDn [(Operation, PartialAttribute)]
| ExtendedRequest LdapOid (Maybe ByteString)
deriving (Show, Eq, Ord) deriving (Show, Eq, Ord)
data ProtocolServerOp = data ProtocolServerOp =
@ -34,6 +35,7 @@ data ProtocolServerOp =
| AddResponse LdapResult | AddResponse LdapResult
| DeleteResponse LdapResult | DeleteResponse LdapResult
| CompareResponse LdapResult | CompareResponse LdapResult
| ExtendedResponse LdapResult (Maybe LdapOid) (Maybe ByteString)
deriving (Show, Eq, Ord) deriving (Show, Eq, Ord)
data AuthenticationChoice = Simple ByteString data AuthenticationChoice = Simple ByteString

View File

@ -36,6 +36,9 @@ module Ldap.Client
, delete , delete
-- * Compare Operation -- * Compare Operation
, compare , compare
-- * Extended Operation
, Oid(..)
, extended
-- * Waiting for Operation Completion -- * Waiting for Operation Completion
, wait , wait
, waitSTM , waitSTM
@ -82,6 +85,7 @@ import Ldap.Client.Modify (Operation(..), modify)
import Ldap.Client.Add (add) import Ldap.Client.Add (add)
import Ldap.Client.Delete (delete) import Ldap.Client.Delete (delete)
import Ldap.Client.Compare (compare) import Ldap.Client.Compare (compare)
import Ldap.Client.Extended (extended)
newLdap :: IO Ldap newLdap :: IO Ldap
@ -199,4 +203,7 @@ dispatch Ldap { client } inq outq =
Type.CompareResponse {} -> do Type.CompareResponse {} -> do
traverse_ (\var -> putTMVar var (op :| [])) (Map.lookup mid results) traverse_ (\var -> putTMVar var (op :| [])) (Map.lookup mid results)
return (Map.delete mid got, Map.delete mid results, counter) return (Map.delete mid got, Map.delete mid results, counter)
Type.ExtendedResponse {} -> do
traverse_ (\var -> putTMVar var (op :| [])) (Map.lookup mid results)
return (Map.delete mid got, Map.delete mid results, counter)
]) ])

View File

@ -0,0 +1,64 @@
{-# LANGUAGE OverloadedStrings #-}
module Ldap.Client.Extended
( extended
, extendedEither
, extendedAsync
, extendedAsyncSTM
, startTls
, startTlsEither
, startTlsAsync
, startTlsAsyncSTM
) where
import Control.Monad ((<=<))
import Control.Monad.STM (STM, atomically)
import Data.ByteString (ByteString)
import Data.List.NonEmpty (NonEmpty((:|)))
import qualified Ldap.Asn1.Type as Type
import Ldap.Client.Internal
extended :: Ldap -> Oid -> Maybe ByteString -> IO ()
extended l oid mv =
raise =<< extendedEither l oid mv
extendedEither :: Ldap -> Oid -> Maybe ByteString -> IO (Either ResponseError ())
extendedEither l oid mv =
wait =<< extendedAsync l oid mv
extendedAsync :: Ldap -> Oid -> Maybe ByteString -> IO (Async ())
extendedAsync l oid mv =
atomically (extendedAsyncSTM l oid mv)
extendedAsyncSTM :: Ldap -> Oid -> Maybe ByteString -> STM (Async ())
extendedAsyncSTM l oid mv =
let req = extendedRequest oid mv in sendRequest l (extendedResult req) req
extendedRequest :: Oid -> Maybe ByteString -> Request
extendedRequest (Oid oid) mv =
Type.ExtendedRequest (Type.LdapOid oid) mv
extendedResult :: Request -> Response -> Either ResponseError ()
extendedResult req (Type.ExtendedResponse (Type.LdapResult code (Type.LdapDn (Type.LdapString dn))
(Type.LdapString msg) _) _ _ :| [])
| Type.Success <- code = Right ()
| otherwise = Left (ResponseErrorCode req code (Dn dn) msg)
extendedResult req res = Left (ResponseInvalid req res)
startTls :: Ldap -> IO ()
startTls =
raise <=< startTlsEither
startTlsEither :: Ldap -> IO (Either ResponseError ())
startTlsEither =
wait <=< startTlsAsync
startTlsAsync :: Ldap -> IO (Async ())
startTlsAsync =
atomically . startTlsAsyncSTM
startTlsAsyncSTM :: Ldap -> STM (Async ())
startTlsAsyncSTM l =
extendedAsyncSTM l (Oid "1.3.6.1.4.1.1466.20037") Nothing

View File

@ -8,6 +8,7 @@ module Ldap.Client.Internal
, ClientMessage(..) , ClientMessage(..)
, Type.ResultCode(..) , Type.ResultCode(..)
, Async , Async
, Oid(..)
, AttrList , AttrList
-- * Waiting for Request Completion -- * Waiting for Request Completion
, wait , wait
@ -59,6 +60,10 @@ instance Functor Async where
newtype Dn = Dn Text newtype Dn = Dn Text
deriving (Show, Eq) deriving (Show, Eq)
newtype Oid = Oid ByteString
deriving (Show, Eq)
newtype Password = Password ByteString newtype Password = Password ByteString
deriving (Show, Eq) deriving (Show, Eq)

View File

@ -0,0 +1,32 @@
{-# LANGUAGE OverloadedStrings #-}
module Ldap.Client.ExtendedSpec (spec) where
import Test.Hspec
import Ldap.Client as Ldap
import Ldap.Client.Extended as Ldap
import qualified Ldap.Asn1.Type as Ldap.Type
import SpecHelper (locally )
spec :: Spec
spec = do
it "sends an extended request" $ do
res <- locally $ \l ->
Ldap.extended l (Oid "0") Nothing
res `shouldBe` Left
(ResponseError (ResponseErrorCode (Ldap.Type.ExtendedRequest (Ldap.Type.LdapOid "0") Nothing)
ProtocolError
(Dn "")
"0 not supported"))
it "sends a startTLS request" $ do
res <- locally $ \l ->
Ldap.startTls l
res `shouldBe` Left
(ResponseError (ResponseErrorCode (Ldap.Type.ExtendedRequest (Ldap.Type.LdapOid "1.3.6.1.4.1.1466.20037")
Nothing)
ProtocolError
(Dn "")
"1.3.6.1.4.1.1466.20037 not supported"))

View File

@ -89,50 +89,6 @@ server.search('o=localhost', [authorize], function(req, res, next) {
return next(); return next();
}); });
server.add('o=localhost', [], function(req, res, next) {
var attributes = req.toObject().attributes;
pokemon.push(req.toObject())
res.end();
return next();
});
server.del('o=localhost', [], function(req, res, next) {
for (var i = 0; i < pokemon.length; i++) {
if (req.dn.toString() === pokemon[i].dn) {
pokemon.splice(i, 1);
res.end();
return next();
}
}
return next(new ldapjs.NoSuchObjectError(req.dn.toString()));
});
server.compare('o=localhost', [], function(req, res, next) {
for (var i = 0; i < pokemon.length; i++) {
if (req.dn.toString() === pokemon[i].dn) {
for (var attribute in pokemon[i].attributes) {
if (attribute === req.attribute) {
for (var j = 0; j < pokemon[i].attributes[attribute].length; j++) {
if (pokemon[i].attributes[attribute][j] === req.value) {
res.end(true);
return next();
}
}
res.end(false);
return next();
}
}
res.end(false);
return next();
}
}
return next(new ldapjs.NoSuchObjectError(req.dn.toString()));
});
// Javascript is helpless // Javascript is helpless
Array.prototype.diff = function(arr) { Array.prototype.diff = function(arr) {
return this.filter(function(idx) { return arr.indexOf(idx) < 0; }); return this.filter(function(idx) { return arr.indexOf(idx) < 0; });
@ -183,6 +139,50 @@ server.modify('o=localhost', [], function(req, res, next) {
return next(new ldapjs.NoSuchObjectError(dn)); return next(new ldapjs.NoSuchObjectError(dn));
}); });
server.add('o=localhost', [], function(req, res, next) {
var attributes = req.toObject().attributes;
pokemon.push(req.toObject())
res.end();
return next();
});
server.del('o=localhost', [], function(req, res, next) {
for (var i = 0; i < pokemon.length; i++) {
if (req.dn.toString() === pokemon[i].dn) {
pokemon.splice(i, 1);
res.end();
return next();
}
}
return next(new ldapjs.NoSuchObjectError(req.dn.toString()));
});
server.compare('o=localhost', [], function(req, res, next) {
for (var i = 0; i < pokemon.length; i++) {
if (req.dn.toString() === pokemon[i].dn) {
for (var attribute in pokemon[i].attributes) {
if (attribute === req.attribute) {
for (var j = 0; j < pokemon[i].attributes[attribute].length; j++) {
if (pokemon[i].attributes[attribute][j] === req.value) {
res.end(true);
return next();
}
}
res.end(false);
return next();
}
}
res.end(false);
return next();
}
}
return next(new ldapjs.NoSuchObjectError(req.dn.toString()));
});
server.listen(port, function() { server.listen(port, function() {
console.log("ldaps://localhost:%d", port); console.log("ldaps://localhost:%d", port);
}); });