-
Notifications
You must be signed in to change notification settings - Fork 127
Attach response controls to exceptions #251
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
57ed6ae
c76f087
cc7ef3b
0144bdb
a0ba399
2b5d1ee
a616cae
e8648f2
9ed502d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -27,7 +27,9 @@ | |
|
||
# a template string for generating simple slapd.conf file | ||
SLAPD_CONF_TEMPLATE = r""" | ||
|
||
serverID %(serverid)s | ||
%(moduleload_directives)s | ||
moduleload back_%(database)s | ||
%(include_directives)s | ||
loglevel %(loglevel)s | ||
|
@@ -43,6 +45,8 @@ | |
rootdn "%(rootdn)s" | ||
rootpw "%(rootpw)s" | ||
|
||
%(overlay_configurations)s | ||
|
||
TLSCACertificateFile "%(cafile)s" | ||
TLSCertificateFile "%(servercert)s" | ||
TLSCertificateKeyFile "%(serverkey)s" | ||
|
@@ -187,6 +191,9 @@ class SlapdObject(object): | |
'core.schema', | ||
) | ||
|
||
modules = () | ||
overlays = () | ||
|
||
TMPDIR = os.environ.get('TMP', os.getcwd()) | ||
if 'SCHEMA' in os.environ: | ||
SCHEMADIR = os.environ['SCHEMA'] | ||
|
@@ -331,9 +338,22 @@ def gen_config(self): | |
) | ||
for schema_file in self.openldap_schema_files | ||
) | ||
|
||
moduleload_directives = '\n'.join( | ||
"moduleload {module}".format(module=module) | ||
for module in self.modules | ||
) | ||
|
||
overlay_configurations = '\n'.join( | ||
"overlay {name}\n{configuration}".format(**overlay) | ||
for overlay in self.overlays | ||
) | ||
|
||
config_dict = { | ||
'serverid': hex(self.server_id), | ||
'schema_prefix':self._schema_prefix, | ||
'moduleload_directives': moduleload_directives, | ||
'overlay_configurations': overlay_configurations, | ||
'include_directives': include_directives, | ||
'loglevel': self.slapd_loglevel, | ||
'database': self.database, | ||
|
@@ -582,10 +602,17 @@ def _open_ldap_conn(self, who=None, cred=None, **kwargs): | |
""" | ||
return a LDAPObject instance after simple bind | ||
""" | ||
ldap_conn = self._make_ldap_object(**kwargs) | ||
ldap_conn.simple_bind_s(who or self.server.root_dn, cred or self.server.root_pw) | ||
return ldap_conn | ||
|
||
def _make_ldap_object(self, **kwargs): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If subclasses are supposed to call this, it should be documented. |
||
""" | ||
return an unbound LDAPObject instance with common ldap options. | ||
""" | ||
ldap_conn = self.ldap_object_class(self.server.ldap_uri, **kwargs) | ||
ldap_conn.protocol_version = 3 | ||
#ldap_conn.set_option(ldap.OPT_REFERRALS, 0) | ||
ldap_conn.simple_bind_s(who or self.server.root_dn, cred or self.server.root_pw) | ||
# ldap_conn.set_option(ldap.OPT_REFERRALS, 0) | ||
return ldap_conn | ||
|
||
@classmethod | ||
|
@@ -596,3 +623,43 @@ def setUpClass(cls): | |
@classmethod | ||
def tearDownClass(cls): | ||
cls.server.stop() | ||
|
||
|
||
class PPolicyEnabledSlapdObject(SlapdObject): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you think this class will be needed for more than just one test? If not, would it be better to define the class just in the file for that test? |
||
""" | ||
A subclass of :py:class:`SlapdObject` with password policy enabled. | ||
Note that this class has no actual password policy configuration entries. | ||
It is the job of the users of this class to define | ||
the default password policies on their own. | ||
The dn of the default is :attr:`.default_ppolicy_dn` of this class. | ||
""" | ||
|
||
openldap_schema_files = ( | ||
'core.schema', 'ppolicy.schema' | ||
) | ||
modules = ( | ||
'ppolicy', | ||
) | ||
|
||
default_ppolicy_dn = "cn=default-ppolicy,%(suffix)s" % { | ||
'suffix': SlapdObject.suffix | ||
} | ||
|
||
overlays = ( | ||
{ | ||
'name': 'ppolicy', | ||
'configuration': "\n".join([ | ||
'ppolicy_default "{}"'.format(default_ppolicy_dn), | ||
# let slapd tell the clients that they are locked out | ||
'ppolicy_use_lockout']) | ||
}, | ||
) | ||
|
||
|
||
class PPolicyEnabledSlapdTestCase(SlapdTestCase): | ||
""" | ||
A subclass of :py:class:`SlapdTestCase`, which uses | ||
:py:class:`PPolicyEnabledSlapdObject` as the slapd controller. | ||
""" | ||
|
||
server_class = PPolicyEnabledSlapdObject | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this necessary? |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -48,7 +48,7 @@ LDAPerr(int errnum) | |
|
||
/* Convert an LDAP error into an informative python exception */ | ||
PyObject * | ||
LDAPerror(LDAP *l, char *msg) | ||
LDAPraise_exception(LDAP *l, char *msg, PyObject *pyctrls) | ||
{ | ||
if (l == NULL) { | ||
PyErr_SetFromErrno(LDAPexception_class); | ||
|
@@ -104,6 +104,11 @@ LDAPerror(LDAP *l, char *msg) | |
ldap_memfree(matched); | ||
} | ||
|
||
if (pyctrls != NULL) { | ||
PyDict_SetItemString(info, "ctrls", pyctrls); | ||
/* Py_XDECREF(pyctrls) must be called on caller side */ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Needing to call |
||
} | ||
|
||
if (errnum == LDAP_REFERRAL) { | ||
str = PyUnicode_FromString(msg); | ||
if (str) | ||
|
@@ -125,6 +130,17 @@ LDAPerror(LDAP *l, char *msg) | |
} | ||
} | ||
|
||
|
||
/* Convert an LDAP error into an informative python exception. | ||
This is the convenient function for the case where the exception | ||
doesn't have to include any response controls. */ | ||
PyObject * | ||
LDAPerror(LDAP *l, char *msg) | ||
{ | ||
return LDAPraise_exception(l, msg, NULL); | ||
} | ||
|
||
|
||
/* initialise the module constants */ | ||
|
||
int | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -28,9 +28,10 @@ | |
os.environ['LDAPNOINIT'] = '1' | ||
|
||
import ldap | ||
import ldap.controls | ||
import ldap.controls.ppolicy | ||
from ldap.ldapobject import SimpleLDAPObject, ReconnectLDAPObject | ||
|
||
from slapdtest import SlapdTestCase | ||
from slapdtest import SlapdTestCase, PPolicyEnabledSlapdTestCase | ||
from slapdtest import requires_ldapi, requires_sasl, requires_tls | ||
|
||
|
||
|
@@ -75,6 +76,72 @@ | |
""" | ||
|
||
|
||
class Test02_ResponseControl(PPolicyEnabledSlapdTestCase): | ||
""" | ||
tests abount response controls sent by the server | ||
""" | ||
|
||
ldap_object_class = SimpleLDAPObject | ||
|
||
@classmethod | ||
def setUpClass(cls): | ||
super(Test02_ResponseControl, cls).setUpClass() | ||
# insert some Foo* objects via ldapadd | ||
cls.server.ldapadd( | ||
LDIF_TEMPLATE % { | ||
'suffix': cls.server.suffix, | ||
'rootdn': cls.server.root_dn, | ||
'rootcn': cls.server.root_cn, | ||
'rootpw': cls.server.root_pw, | ||
'dc': cls.server.suffix.split(',')[0][3:], | ||
} | ||
) | ||
|
||
# Very strict pwdMaxFailure in order to easily test the cases where | ||
# bind failure with response controls is needed | ||
cls.server.ldapadd( | ||
'''dn: {dn} | ||
objectClass: organizationalRole | ||
objectClass: pwdPolicy | ||
cn: default-ppolicy | ||
pwdAttribute: userPassword | ||
pwdLockout: TRUE | ||
pwdMaxFailure: 1 | ||
pwdLockoutDuration: 60 | ||
pwdFailureCountInterval: 3600'''.format(dn=cls.server.default_ppolicy_dn) | ||
) | ||
|
||
def test_response_controls_are_attached_to_exceptions(self): | ||
base = self.server.suffix | ||
cn = "test_response_controls_are_attached_to_exceptions" | ||
user_dn = "cn={},{}".format(cn, base) | ||
password = "user5_pw" | ||
|
||
self.server.ldapadd( | ||
'''dn: {dn} | ||
objectClass: applicationProcess | ||
objectClass: simpleSecurityObject | ||
cn: {cn} | ||
userPassword: {password}'''.format(cn=cn, dn=user_dn, password=password) | ||
) | ||
|
||
ldap_conn = self._make_ldap_object(bytes_mode=False) | ||
|
||
# Firstly cause a bind failure to lock out the account | ||
with self.assertRaises(ldap.INVALID_CREDENTIALS): | ||
wrong_password = 'wrong' + password | ||
ldap_conn.simple_bind_s(user_dn, wrong_password) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you also check here that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. checked. |
||
|
||
with self.assertRaises(ldap.INVALID_CREDENTIALS) as cm: | ||
ldap_conn.simple_bind_s( | ||
user_dn, password, | ||
serverctrls=[ldap.controls.ppolicy.PasswordPolicyControl()]) | ||
|
||
controls = cm.exception.args[0]['ctrls'] | ||
pp = ldap.controls.DecodeControlTuples(controls)[0] | ||
self.assertEqual(pp.error, 1) # error == 1 means AccountLockout | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we have a constant for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you also check |
||
|
||
|
||
class Test00_SimpleLDAPObject(SlapdTestCase): | ||
""" | ||
test LDAP search operations | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since these attributes can be overridden in subclasses, they should be documented.