ldap: add 'exact' search_op, allow filter-only search (#76595)
gitea/passerelle/pipeline/head This commit looks good
Details
gitea/passerelle/pipeline/head This commit looks good
Details
This commit is contained in:
parent
32710aaa46
commit
8a9e0ff5ea
|
@ -39,6 +39,7 @@ LDAP_HAS_OPT_X_TLS_REQUIRE_SAN = hasattr(ldap, 'OPT_X_TLS_REQUIRE_SAN') # only
|
||||||
SEARCH_OP_SUBSTRING = 'substring'
|
SEARCH_OP_SUBSTRING = 'substring'
|
||||||
SEARCH_OP_PREFIX = 'prefix'
|
SEARCH_OP_PREFIX = 'prefix'
|
||||||
SEARCH_OP_APPROX = 'approx'
|
SEARCH_OP_APPROX = 'approx'
|
||||||
|
SEARCH_OP_EXACT = 'exact'
|
||||||
|
|
||||||
|
|
||||||
class Resource(BaseResource):
|
class Resource(BaseResource):
|
||||||
|
@ -295,7 +296,7 @@ class Resource(BaseResource):
|
||||||
},
|
},
|
||||||
'search_op': {
|
'search_op': {
|
||||||
'description': _(
|
'description': _(
|
||||||
'Search operator, can be "substring" (the default value), "prefix" or "approx"'
|
'Search operator, can be "substring" (the default value), "prefix", "approx" or "exact"'
|
||||||
),
|
),
|
||||||
'example_value': SEARCH_OP_SUBSTRING,
|
'example_value': SEARCH_OP_SUBSTRING,
|
||||||
},
|
},
|
||||||
|
@ -305,8 +306,8 @@ class Resource(BaseResource):
|
||||||
self,
|
self,
|
||||||
request,
|
request,
|
||||||
ldap_base_dn,
|
ldap_base_dn,
|
||||||
search_attribute,
|
|
||||||
id_attribute,
|
id_attribute,
|
||||||
|
search_attribute=None,
|
||||||
text_template=None,
|
text_template=None,
|
||||||
ldap_attributes=None,
|
ldap_attributes=None,
|
||||||
id=None,
|
id=None,
|
||||||
|
@ -316,23 +317,31 @@ class Resource(BaseResource):
|
||||||
filter=None,
|
filter=None,
|
||||||
search_op=SEARCH_OP_SUBSTRING,
|
search_op=SEARCH_OP_SUBSTRING,
|
||||||
):
|
):
|
||||||
search_attribute = search_attribute.lower()
|
if not q and not id and not filter:
|
||||||
|
raise APIError('filter or q or id are mandatory parameters', http_status=400)
|
||||||
|
if q and not search_attribute:
|
||||||
|
raise APIError('search_attribute is mandatory with q parameter', http_status=400)
|
||||||
|
if not search_attribute and not text_template:
|
||||||
|
raise APIError('search_attribute or text_template are mandatory parameters', http_status=400)
|
||||||
|
if search_attribute:
|
||||||
|
search_attribute = search_attribute.lower()
|
||||||
|
if not search_attribute.isascii():
|
||||||
|
raise APIError('search_attribute contains non ASCII characters', http_status=400)
|
||||||
id_attribute = id_attribute.lower()
|
id_attribute = id_attribute.lower()
|
||||||
if not search_attribute.isascii():
|
|
||||||
raise APIError('search_attribute contains non ASCII characters')
|
|
||||||
if not id_attribute.isascii():
|
if not id_attribute.isascii():
|
||||||
raise APIError('id_attribute contains non ASCII characters')
|
raise APIError('id_attribute contains non ASCII characters', http_status=400)
|
||||||
ldap_attributes = set(ldap_attributes.split()) if ldap_attributes else set()
|
ldap_attributes = set(ldap_attributes.split()) if ldap_attributes else set()
|
||||||
ldap_attributes.update([search_attribute, id_attribute])
|
ldap_attributes.add(id_attribute)
|
||||||
|
if search_attribute:
|
||||||
|
ldap_attributes.add(search_attribute)
|
||||||
if not all(attribute.isascii() for attribute in ldap_attributes):
|
if not all(attribute.isascii() for attribute in ldap_attributes):
|
||||||
raise APIError('ldap_attributes contains non ASCII characters')
|
raise APIError('ldap_attributes contains non ASCII characters', http_status=400)
|
||||||
try:
|
try:
|
||||||
sizelimit = int(sizelimit)
|
sizelimit = int(sizelimit)
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
pass
|
pass
|
||||||
sizelimit = max(1, min(sizelimit or 30, 200))
|
sizelimit = max(1, min(sizelimit or 30, 200))
|
||||||
if not q and not id:
|
ldap_filter = None
|
||||||
raise APIError('q or id are mandatory parameters', http_status=400)
|
|
||||||
if id:
|
if id:
|
||||||
ldap_filter = '(%s=%s)' % (id_attribute, ldap.filter.escape_filter_chars(id))
|
ldap_filter = '(%s=%s)' % (id_attribute, ldap.filter.escape_filter_chars(id))
|
||||||
elif q:
|
elif q:
|
||||||
|
@ -342,12 +351,17 @@ class Resource(BaseResource):
|
||||||
ldap_filter = '(%s=%s*)' % (search_attribute, ldap.filter.escape_filter_chars(q))
|
ldap_filter = '(%s=%s*)' % (search_attribute, ldap.filter.escape_filter_chars(q))
|
||||||
elif search_op == SEARCH_OP_APPROX:
|
elif search_op == SEARCH_OP_APPROX:
|
||||||
ldap_filter = '(%s~=%s)' % (search_attribute, ldap.filter.escape_filter_chars(q))
|
ldap_filter = '(%s~=%s)' % (search_attribute, ldap.filter.escape_filter_chars(q))
|
||||||
|
elif search_op == SEARCH_OP_EXACT:
|
||||||
|
ldap_filter = '(%s=%s)' % (search_attribute, ldap.filter.escape_filter_chars(q))
|
||||||
else:
|
else:
|
||||||
raise APIError('unknown search_op %r' % search_op)
|
raise APIError('unknown search_op %r' % search_op, http_status=400)
|
||||||
if filter:
|
if filter:
|
||||||
if not filter.startswith('('):
|
if not filter.startswith('('):
|
||||||
filter = '(%s)' % filter
|
filter = '(%s)' % filter
|
||||||
ldap_filter = '(&%s%s)' % (ldap_filter, filter)
|
if ldap_filter:
|
||||||
|
ldap_filter = '(&%s%s)' % (ldap_filter, filter)
|
||||||
|
else:
|
||||||
|
ldap_filter = filter
|
||||||
scopes = {
|
scopes = {
|
||||||
'subtree': ldap.SCOPE_SUBTREE,
|
'subtree': ldap.SCOPE_SUBTREE,
|
||||||
'onelevel': ldap.SCOPE_ONELEVEL,
|
'onelevel': ldap.SCOPE_ONELEVEL,
|
||||||
|
|
|
@ -179,13 +179,69 @@ def test_q_approx(app, resource, ldap_server):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_q_exact(app, resource, ldap_server):
|
||||||
|
response = app.get(
|
||||||
|
'/ldap/resource/search',
|
||||||
|
params={
|
||||||
|
'q': 'Jane Doe',
|
||||||
|
'ldap_base_dn': 'o=orga',
|
||||||
|
'search_attribute': 'cn',
|
||||||
|
'id_attribute': 'uid',
|
||||||
|
'search_op': 'exact',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert response.json == {
|
||||||
|
'err': 0,
|
||||||
|
'data': [
|
||||||
|
{
|
||||||
|
'attributes': {'cn': 'Jane Doe', 'uid': 'janedoe'},
|
||||||
|
'dn': 'uid=janedoe,o=orga',
|
||||||
|
'id': 'janedoe',
|
||||||
|
'text': 'Jane Doe',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
response = app.get(
|
||||||
|
'/ldap/resource/search',
|
||||||
|
params={
|
||||||
|
'q': 'Foo Bar',
|
||||||
|
'ldap_base_dn': 'o=orga',
|
||||||
|
'search_attribute': 'cn',
|
||||||
|
'id_attribute': 'uid',
|
||||||
|
'search_op': 'exact',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert response.json == {
|
||||||
|
'err': 0,
|
||||||
|
'data': [],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_bad_search_op(app, resource, ldap_server):
|
||||||
|
response = app.get(
|
||||||
|
'/ldap/resource/search',
|
||||||
|
params={
|
||||||
|
'q': 'Jane Doe',
|
||||||
|
'ldap_base_dn': 'o=orga',
|
||||||
|
'search_attribute': 'cn',
|
||||||
|
'id_attribute': 'uid',
|
||||||
|
'search_op': 'bad search op',
|
||||||
|
},
|
||||||
|
status=400,
|
||||||
|
)
|
||||||
|
assert response.json['err'] == 1
|
||||||
|
assert response.json['err_desc'] == "unknown search_op 'bad search op'"
|
||||||
|
assert response.json['data'] is None
|
||||||
|
|
||||||
|
|
||||||
def test_id(app, resource, ldap_server):
|
def test_id(app, resource, ldap_server):
|
||||||
response = app.get(
|
response = app.get(
|
||||||
'/ldap/resource/search',
|
'/ldap/resource/search',
|
||||||
params={
|
params={
|
||||||
'id': 'janedoe',
|
'id': 'janedoe',
|
||||||
'ldap_base_dn': 'o=orga',
|
'ldap_base_dn': 'o=orga',
|
||||||
'search_attribute': 'cn',
|
'ldap_attributes': 'cn',
|
||||||
|
'text_template': '{{ cn }}',
|
||||||
'id_attribute': 'uid',
|
'id_attribute': 'uid',
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -205,6 +261,60 @@ def test_id(app, resource, ldap_server):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_filter(app, resource, ldap_server):
|
||||||
|
for filter in ('(cn~=Jane Doe)', 'cn~=Jane Doe'):
|
||||||
|
response = app.get(
|
||||||
|
'/ldap/resource/search',
|
||||||
|
params={
|
||||||
|
'filter': filter,
|
||||||
|
'ldap_base_dn': 'o=orga',
|
||||||
|
'search_attribute': 'cn',
|
||||||
|
'id_attribute': 'uid',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert response.json == {
|
||||||
|
'err': 0,
|
||||||
|
'data': [
|
||||||
|
{
|
||||||
|
'attributes': {'cn': 'Jane Doe', 'uid': 'janedoe'},
|
||||||
|
'dn': 'uid=janedoe,o=orga',
|
||||||
|
'id': 'janedoe',
|
||||||
|
'text': 'Jane Doe',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'attributes': {'cn': 'John Doe', 'uid': 'johndoe'},
|
||||||
|
'dn': 'uid=johndoe,o=orga',
|
||||||
|
'id': 'johndoe',
|
||||||
|
'text': 'John Doe',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
response = app.get(
|
||||||
|
'/ldap/resource/search',
|
||||||
|
params={
|
||||||
|
'filter': 'cn~=Jane Doe',
|
||||||
|
'id': 'nobody',
|
||||||
|
'ldap_base_dn': 'o=orga',
|
||||||
|
'search_attribute': 'cn',
|
||||||
|
'id_attribute': 'uid',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert response.json == {'err': 0, 'data': []}
|
||||||
|
|
||||||
|
response = app.get(
|
||||||
|
'/ldap/resource/search',
|
||||||
|
params={
|
||||||
|
'filter': 'cn~=Jane Doe',
|
||||||
|
'q': 'nobody',
|
||||||
|
'ldap_base_dn': 'o=orga',
|
||||||
|
'search_attribute': 'cn',
|
||||||
|
'id_attribute': 'uid',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert response.json == {'err': 0, 'data': []}
|
||||||
|
|
||||||
|
|
||||||
def test_sizelimit(app, resource, ldap_server):
|
def test_sizelimit(app, resource, ldap_server):
|
||||||
response = app.get(
|
response = app.get(
|
||||||
'/ldap/resource/search',
|
'/ldap/resource/search',
|
||||||
|
@ -261,3 +371,89 @@ def test_scope(app, resource, ldap_server):
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
assert len(response.json['data']) == 1
|
assert len(response.json['data']) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_missing_q_id_filter(app, resource, ldap_server):
|
||||||
|
response = app.get(
|
||||||
|
'/ldap/resource/search',
|
||||||
|
params={
|
||||||
|
'ldap_base_dn': 'o=orga',
|
||||||
|
'search_attribute': 'cn',
|
||||||
|
'id_attribute': 'uid',
|
||||||
|
},
|
||||||
|
status=400,
|
||||||
|
)
|
||||||
|
assert response.json['err'] == 1
|
||||||
|
assert response.json['err_desc'] == 'filter or q or id are mandatory parameters'
|
||||||
|
assert response.json['data'] is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_bad_requests(app, resource, ldap_server):
|
||||||
|
response = app.get(
|
||||||
|
'/ldap/resource/search',
|
||||||
|
params={
|
||||||
|
'q': 'Jane Doe',
|
||||||
|
'ldap_base_dn': 'o=orga',
|
||||||
|
'id_attribute': 'uid',
|
||||||
|
},
|
||||||
|
status=400,
|
||||||
|
)
|
||||||
|
assert response.json['err'] == 1
|
||||||
|
assert response.json['err_desc'] == 'search_attribute is mandatory with q parameter'
|
||||||
|
assert response.json['data'] is None
|
||||||
|
|
||||||
|
response = app.get(
|
||||||
|
'/ldap/resource/search',
|
||||||
|
params={
|
||||||
|
'filter': "(cn~=Jane Doe)",
|
||||||
|
'ldap_base_dn': 'o=orga',
|
||||||
|
'id_attribute': 'uid',
|
||||||
|
},
|
||||||
|
status=400,
|
||||||
|
)
|
||||||
|
assert response.json['err'] == 1
|
||||||
|
assert response.json['err_desc'] == 'search_attribute or text_template are mandatory parameters'
|
||||||
|
assert response.json['data'] is None
|
||||||
|
|
||||||
|
response = app.get(
|
||||||
|
'/ldap/resource/search',
|
||||||
|
params={
|
||||||
|
'filter': "(cn~=Jane Doe)",
|
||||||
|
'ldap_base_dn': 'o=orga',
|
||||||
|
'id_attribute': 'uid',
|
||||||
|
'search_attribute': 'bloqué',
|
||||||
|
},
|
||||||
|
status=400,
|
||||||
|
)
|
||||||
|
assert response.json['err'] == 1
|
||||||
|
assert response.json['err_desc'] == 'search_attribute contains non ASCII characters'
|
||||||
|
assert response.json['data'] is None
|
||||||
|
|
||||||
|
response = app.get(
|
||||||
|
'/ldap/resource/search',
|
||||||
|
params={
|
||||||
|
'filter': "(cn~=Jane Doe)",
|
||||||
|
'ldap_base_dn': 'o=orga',
|
||||||
|
'id_attribute': 'bloqué',
|
||||||
|
'search_attribute': 'cn',
|
||||||
|
},
|
||||||
|
status=400,
|
||||||
|
)
|
||||||
|
assert response.json['err'] == 1
|
||||||
|
assert response.json['err_desc'] == 'id_attribute contains non ASCII characters'
|
||||||
|
assert response.json['data'] is None
|
||||||
|
|
||||||
|
response = app.get(
|
||||||
|
'/ldap/resource/search',
|
||||||
|
params={
|
||||||
|
'filter': "(cn~=Jane Doe)",
|
||||||
|
'ldap_base_dn': 'o=orga',
|
||||||
|
'id_attribute': 'uid',
|
||||||
|
'search_attribute': 'cn',
|
||||||
|
'ldap_attributes': 'bloqué',
|
||||||
|
},
|
||||||
|
status=400,
|
||||||
|
)
|
||||||
|
assert response.json['err'] == 1
|
||||||
|
assert response.json['err_desc'] == 'ldap_attributes contains non ASCII characters'
|
||||||
|
assert response.json['data'] is None
|
||||||
|
|
Loading…
Reference in New Issue