Skip to content

Commit 167e358

Browse files
author
Joe Stubbs
committed
several basic ldap utilities should now be working, including creating OUs in the local ldap (create_tapis_ldap_tenant_ou), creating test users in the dev OU (add_test_user), and checking passwords (check_username_password)
1 parent 2c920f1 commit 167e358

File tree

6 files changed

+275
-26
lines changed

6 files changed

+275
-26
lines changed

Dockerfile-tests

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Image: tapis/authenticator-tests
2+
from tapis/authenticator
3+
4+
USER root
5+
ADD tests-requirements.txt /home/tapis/tests-requirements.txt
6+
RUN pip install -r /home/tapis/tests-requirements.txt
7+
ADD service/tests /home/tapis/service/tests
8+
RUN chown -R tapis:tapis /home/tapis
9+
10+
USER tapis
11+
ENTRYPOINT ["pytest"]

service/ldap.py

Lines changed: 172 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,163 @@
11
from ldap3 import Server, Connection
22
from ldap3.core.exceptions import LDAPBindError
3+
import json
34

45
from service import get_tenant_config
56
from service.errors import InvalidPasswordError
7+
from service.models import LdapOU, LdapUser
8+
9+
from common.config import conf
10+
from common.errors import DAOError
611

712
# get the logger instance -
813
from common.logs import get_logger
914
logger = get_logger(__name__)
1015

1116

12-
def get_ldap_connection(tenant_id, bind_dn=None, bind_credential=None):
17+
def get_ldap_connection(ldap_server, ldap_port, bind_dn, bind_password, use_ssl=True):
1318
"""
14-
Get an ldap connection to the ldap server corresponding to the tenant_id.
15-
:param tenant_id:
19+
Get a connection to an LDAP server.
20+
:param ldap_server: The URI of the ldap server.
21+
:param ldap_port: The port of the ldap server.
22+
:param bind_dn: The DN to use to bind.
23+
:param bind_password: The password associated with the bind DN.
24+
:param use_ssl: Whether to use SSL when connecting to the LDAP server.
25+
:return:
26+
"""
27+
server = Server(ldap_server, port=ldap_port, use_ssl=use_ssl)
28+
conn = Connection(server, bind_dn, bind_password, auto_bind=True)
29+
return conn
30+
31+
def get_tapis_ldap_server_info():
32+
"""
33+
Returns dictionary of Tapis LDAP server connection information.
34+
:return: (dict)
35+
"""
36+
return {
37+
"server": conf.dev_ldap_url,
38+
"port": conf.dev_ldap_port,
39+
"bind_dn": conf.dev_ldap_bind_dn,
40+
"bind_password": conf.dev_ldap_bind_credential,
41+
"base_dn": conf.dev_ldap_tenants_base_dn,
42+
"use_ssl": conf.dev_ldap_use_ssl
43+
}
44+
45+
tapis_ldap = get_tapis_ldap_server_info()
46+
47+
def get_tapis_ldap_connection():
48+
"""
49+
Convenience wrapper function to get an ldap connection to the Tapis dev ldap server.
50+
:return:
51+
"""
52+
try:
53+
return get_ldap_connection(ldap_server = tapis_ldap['server'],
54+
ldap_port = tapis_ldap['port'],
55+
bind_dn = tapis_ldap['bind_dn'],
56+
bind_password = tapis_ldap['bind_password'],
57+
use_ssl = tapis_ldap['use_ssl'])
58+
except LDAPBindError as e:
59+
logger.debug(f'Invalid Tapis bind credential: {e}')
60+
raise InvalidPasswordError("Invalid username/password combination.")
61+
except Exception as e:
62+
msg = f"Got exception trying to create connection object to Tapis LDAP. e: {e}"
63+
logger.error(msg)
64+
raise DAOError(msg)
65+
66+
67+
def add_tapis_ou(ou):
68+
"""
69+
Add an LDAP record representing an Organizational Unit (ou) to the Tapis LDAP.
70+
:param ou: (LdapOU) The OU object to add.
71+
:return:
72+
"""
73+
conn = get_tapis_ldap_connection()
74+
try:
75+
result = conn.add(ou.dn, ou.object_class)
76+
except Exception as e:
77+
msg = f'got an error trying to add an ou. Exception: {e}; ou.dn: {ou.dn}; ou.object_class: {ou.object_class}'
78+
logger.error(msg)
79+
if not result:
80+
msg = f'Got False result trying to add OU to LDAP; error data: {conn.result}'
81+
logger.error(msg)
82+
raise DAOError("Unable to add OU to LDAP database; "
83+
"Required fields could be missing or improperly formatted.")
84+
return True
85+
86+
87+
def list_tapis_ous():
88+
"""
89+
List the OUs associated with the Tapis LDAP server.
90+
:return:
91+
"""
92+
conn = get_tapis_ldap_connection()
93+
try:
94+
# search for all cn's under the tapis tenants base_dn and pull back all attributes
95+
result = conn.search(conf.dev_ldap_tenants_base_dn, '(ou=*)', attributes=['*'])
96+
except Exception as e:
97+
msg = f'Got an exception trying to list Tapis OUs. Exception: {e}'
98+
logger.error(msg)
99+
raise DAOError(msg)
100+
if not result:
101+
msg = f'Got an error trying to list Tapis OUs. message: {conn.result}'
102+
logger.error(msg)
103+
# return the results -
104+
result = []
105+
for ent in conn.entries:
106+
result.append(ent.entry_attributes_as_dict)
107+
return result
108+
109+
110+
def create_tapis_ldap_tenant_ou(tenant_id):
111+
"""
112+
Create an OU in the Tapis LDAP for a tenant id.
113+
:param tenant_id:
114+
:return:
115+
"""
116+
base_dn = tapis_ldap['base_dn']
117+
ou = LdapOU(dn=f'ou=tenants.{tenant_id},{base_dn}')
118+
return add_tapis_ou(ou)
119+
120+
121+
def get_tenant_ldap_connection(tenant_id, bind_dn=None, bind_password=None):
122+
"""
123+
Convenience wrapper function to get an ldap connection to the ldap server corresponding to the tenant_id.
124+
:param tenant_id: (str) The id of the tenant.
125+
:param bind_dn: (str) Optional dn to use to bind. Pass this to check validity of a username/password.
126+
:param bind_password (str) Optional password to use to bind. Pass this to check validity of a username/password.
16127
:return:
17128
"""
18129
tenant = get_tenant_config(tenant_id)
19-
server = Server(tenant['ldap_url'], port=tenant['ldap_port'], use_ssl=tenant['ldap_use_ssl'])
20-
if bind_dn and bind_credential:
21-
conn = Connection(server, bind_dn, bind_credential, auto_bind=True)
22-
else:
23-
conn = Connection(server, tenant['ldap_bind_dn'], tenant['ldap_bind_credential'], auto_bind=True)
24-
return conn
130+
# if we are passed specific bind credentials, use those:
131+
if not bind_dn is None:
132+
return get_ldap_connection(ldap_server=tenant['ldap_url'],
133+
ldap_port=tenant['ldap_port'],
134+
bind_dn=bind_dn,
135+
bind_password=bind_password,
136+
use_ssl=tenant['ldap_use_ssl'])
137+
# otherwise, return the connection associated with the tenant's bind credentials -
138+
return get_ldap_connection(ldap_server=tenant['ldap_url'],
139+
ldap_port=tenant['ldap_port'],
140+
bind_dn=tenant['ldap_bind_dn'],
141+
bind_password=tenant['ldap_bind_credential'],
142+
use_ssl=tenant['ldap_use_ssl'])
143+
144+
def list_tenant_users(tenant_id):
145+
"""
146+
List all users in a tenant
147+
:param tenant_id: (str) the tenant id to use.
148+
:return:
149+
"""
150+
tenant = get_tenant_config(tenant_id)
151+
conn = get_tenant_ldap_connection(tenant_id)
152+
result = conn.search(tenant['ldap_user_dn'], '(cn=*)', attributes=['*'])
153+
if not result:
154+
msg = f'Error retrieving users; debug information: {conn.result}'
155+
logger.error(msg)
156+
raise DAOError(msg)
157+
result = []
158+
for ent in conn.entries:
159+
result.append(ent.entry_attributes_as_dict)
160+
return result
25161

26162
def get_dn(tenant_id, username):
27163
"""
@@ -32,7 +168,7 @@ def get_dn(tenant_id, username):
32168
"""
33169
tenant = get_tenant_config(tenant_id)
34170
ldap_user_dn = tenant['ldap_user_dn']
35-
return f'uid={username},{ldap_user_dn}'
171+
return f'cn={username},{ldap_user_dn}'
36172

37173
def check_username_password(tenant_id, username, password):
38174
"""
@@ -44,18 +180,36 @@ def check_username_password(tenant_id, username, password):
44180
"""
45181
bind_dn = get_dn(tenant_id, username)
46182
try:
47-
get_ldap_connection(tenant_id, bind_dn, password)
183+
conn = get_tenant_ldap_connection(tenant_id, bind_dn=bind_dn, bind_password=password)
48184
except LDAPBindError as e:
49-
logger.debg(f'got exception checking password: {e}')
185+
logger.debug(f'got exception checking password: {e}')
50186
raise InvalidPasswordError("Invalid username/password combination.")
51187

52-
def add_user(tenant_id, username, password):
188+
189+
def add_user(tenant_id, user):
53190
"""
54-
Add an LDAP record
55-
:param tenant_id:
56-
:param username:
57-
:param password:
191+
Add an LDAP record representing a user in a specific tenant.
192+
:param tenant_id: (str) The tenant id of the tenant where the user should be added.
193+
:param user: (LdapUser) An LdapUser object containing the details of the user to add.
58194
:return:
59195
"""
196+
conn = get_tenant_ldap_connection(tenant_id)
197+
user.save(conn)
60198

61-
199+
def add_test_user(tenant_id, username):
200+
"""
201+
Add a testuser to the Tapis LDAP for tenant id, tenant_id. The username is required and from it, all inetorgperson
202+
attributes are derived.
203+
:param tenant_id: (str) the tenant id.
204+
:param username: (str) the username of the test account.
205+
:return:
206+
"""
207+
# first, create an LdapUser object with the appropriate attributes.
208+
base_dn = tapis_ldap['base_dn']
209+
user = LdapUser(dn=f'cn={username},ou=tenants.{tenant_id},{base_dn}',
210+
givenName=username,
211+
sn=username,
212+
mail=f'{username}@test.tapis.io',
213+
userPassword=username)
214+
# now call the generic add user for the tenant id:
215+
add_user(tenant_id, user)

service/models.py

Lines changed: 73 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88

99
from common.config import conf
1010
from common.errors import DAOError
11+
from common.logs import get_logger
12+
logger = get_logger(__name__)
13+
1114

1215
app = Flask(__name__)
1316
app.config['SQLALCHEMY_DATABASE_URI'] = conf.sql_db_url
@@ -116,7 +119,7 @@ class LdapUser(object):
116119
# dn for the record;
117120
dn = None
118121
# we use inetOrgPerson for all LDAP user object
119-
object_classes = [u'inetOrgPerson']
122+
object_class = 'inetOrgPerson'
120123

121124
# attributes
122125
# inetOrgPerson -----
@@ -133,14 +136,22 @@ class LdapUser(object):
133136
username = None
134137
password = None
135138

136-
def __init__(self, dn, givenName, sn, cn, mail, telephoneNumber, mobile,
137-
createTimestamp, uidNumber, uid, userPassword):
139+
def __init__(self,
140+
dn,
141+
givenName=None,
142+
sn=None,
143+
mail=None,
144+
telephoneNumber=None,
145+
mobile=None,
146+
createTimestamp=None,
147+
uidNumber=None,
148+
uid=None,
149+
userPassword=None):
138150
"""
139-
Create a LdapUser object from an LDAP row.
151+
Create an LdapUser object corresponding to an entry in an LDAP server.
140152
:param dn:
141153
:param givenName:
142-
:param sn:
143-
:param cn:
154+
:param sn:
144155
:param mail:
145156
:param telephoneNumber:
146157
:param mobile:
@@ -152,7 +163,6 @@ def __init__(self, dn, givenName, sn, cn, mail, telephoneNumber, mobile,
152163
self.dn = dn
153164
self.given_name = givenName
154165
self.last_name = sn
155-
self.full_name = cn
156166
self.email = mail
157167
self.phone = telephoneNumber
158168
self.mobile_phone = mobile
@@ -161,6 +171,58 @@ def __init__(self, dn, givenName, sn, cn, mail, telephoneNumber, mobile,
161171
self.username = uid
162172
self.password = userPassword
163173

174+
def save(self, conn):
175+
"""
176+
Save an LdapUser object in an LDAP server with connection, conn.
177+
:param conn (ldap3.core.connection.Connection) A connection to the ldap server.
178+
:return:
179+
"""
180+
# first, get the ldap representation of this object and remove any fields not allowed to be passed to
181+
# ldap on save:
182+
repr = self.serialize_to_ldap
183+
repr.pop('create_time', None)
184+
repr.pop('dn')
185+
try:
186+
result = conn.add(self.dn, self.object_class, repr)
187+
except Exception as e:
188+
msg = f'Got exception trying to add a user to LDAP; exception: {e}'
189+
logger.error(msg)
190+
raise DAOError("Unable to communicate with LDAP database when trying to save user account.")
191+
if not result:
192+
msg = f'Got False result trying to add a user to LDAP; error data: {conn.result}'
193+
logger.error(msg)
194+
raise DAOError("Unable to save user account in LDAP database; "
195+
"Required fields could be missing or improperly formatted.")
196+
# the object was saved successfully so we can now return it:
197+
return True
198+
199+
@property
200+
def serialize_to_ldap(self):
201+
"""
202+
Creates a Python dictionary using the LDAP inetorgperson attributes names.
203+
:return:
204+
"""
205+
result = {'dn': self.dn}
206+
if self.given_name:
207+
result['givenName'] = self.given_name
208+
if self.last_name:
209+
result['sn'] = self.last_name
210+
if self.email:
211+
result['mail'] = self.email
212+
if self.phone:
213+
result['telephoneNumber'] = self.phone
214+
if self.mobile_phone:
215+
result['mobile'] = self.mobile_phone
216+
if self.create_time:
217+
result['createTimestamp'] = self.create_time
218+
if self.uid:
219+
result['uidNumber'] = self.uid
220+
if self.username:
221+
result['uid'] = self.username
222+
if self.password:
223+
result['userPassword'] = self.password
224+
return result
225+
164226

165227
class LdapOU(object):
166228
"""
@@ -173,11 +235,14 @@ class LdapOU(object):
173235

174236
# LDAP meta-data -----
175237
dn = None
176-
object_classes = [u'organizationalUnit']
238+
object_class = 'organizationalUnit'
177239

178240
# attributes
179241
ou = None
180242

243+
def __init__(self, dn):
244+
self.dn = dn
245+
181246
def __str__(self):
182247
return self.ou
183248

service/tests/__init__.py

Whitespace-only changes.

service/tests/basic_test.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import pytest
2+
import json
3+
from unittest import TestCase
4+
from service.api import app
5+
from common import auth
6+
7+
# These tests are intended to be run locally.
8+
9+
@pytest.fixture
10+
def client():
11+
app.debug = True
12+
return app.test_client()
13+
14+
15+
def test_invalid_post(client):
16+
with client:
17+
response = client.post("http://localhost:5000/v3/oauth/clients")
18+
assert response.status_code == 400

tests-requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pytest==5.1.2

0 commit comments

Comments
 (0)