Skip to content

Commit aa70caf

Browse files
committed
ldap auth improvements
1 parent 8e75691 commit aa70caf

File tree

3 files changed

+71
-17
lines changed

3 files changed

+71
-17
lines changed

apps/_scaffold/settings.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,15 @@
104104
"mode": "ad", # Microsoft Active Directory
105105
"server": "mydc.domain.com", # FQDN or IP of one Domain Controller
106106
"base_dn": "cn=Users,dc=domain,dc=com", # base dn, i.e. where the users are located
107+
108+
# the following are only needed if you want to use groups
109+
"group_member_attrib": "member", # for AD, attribute that contains the user DN in the group
110+
"bind_dn": "CN=LdapBindUser,CN=users,DC=test,DC=local", # bind user DN
111+
"bind_pw": "P@ssw0rd", # bind user password
112+
"group_dn": "DC=test,DC=local", # group DN, where the groups are located, default = base_dn
113+
"allowed_groups": ["allowed_login_group"], # list of groups that are allowed to log in, default = everyone
114+
"denied_login_popup": True, # show an error at login if not in allowed groups
115+
# default = False
107116
}
108117

109118
# i18n settings

py4web/utils/auth.py

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -702,7 +702,16 @@ def login(self, email, password):
702702
user_info["sso_id"] = plugin.name + ":" + email
703703
if self.use_username or "@" not in email:
704704
user_info["username"] = email
705-
if "@" in email:
705+
# --- LDAP/AD: use real email if available ---
706+
if (
707+
plugin.name == "ldap"
708+
and getattr(plugin, "mode", None) == "ad"
709+
and getattr(plugin, "last_user_mail", None)
710+
):
711+
self.logger.debug(f"Using AD email from LDAP plugin: {plugin.last_user_mail}")
712+
user_info["email"] = plugin.last_user_mail
713+
elif "@" in email:
714+
self.logger.debug(f"Using email from login: {email}")
706715
user_info["email"] = email
707716
else:
708717
self.logger.debug(
@@ -1303,11 +1312,11 @@ def login(auth):
13031312
if "pam" in auth.plugins or "ldap" in auth.plugins:
13041313
plugin_name = "pam" if "pam" in auth.plugins else "ldap"
13051314
plugin = auth.plugins[plugin_name]
1306-
self.logger.debug(
1315+
auth.logger.debug(
13071316
f"AuthAPI.login: Trying plugin {plugin_name} for user {username}"
13081317
)
13091318
check = plugin.check_credentials(username, password)
1310-
self.logger.debug(
1319+
auth.logger.debug(
13111320
f"AuthAPI.login: plugin.check_credentials returned {check}"
13121321
)
13131322
if check:
@@ -1316,19 +1325,29 @@ def login(auth):
13161325
# "email": username + "@localhost",
13171326
"sso_id": plugin_name + ":" + username,
13181327
}
1319-
# and register the user if we have one, just in case
1328+
# For AD, use the real email if available
1329+
if (
1330+
plugin_name == "ldap"
1331+
and getattr(plugin, "mode", None) == "ad"
1332+
and getattr(plugin, "last_user_mail", None)
1333+
):
1334+
auth.logger.debug(f"AuthAPI.login: Using AD email from LDAP plugin: {plugin.last_user_mail}")
1335+
# save the real email from AD to database
1336+
data["email"] = plugin.last_user_mail
1337+
else:
1338+
auth.logger.debug(f"AuthAPI.login: Not using AD email, plugin_name={plugin_name}, mode={getattr(plugin, 'mode', None)}, last_user_mail={getattr(plugin, 'last_user_mail', None)}")
13201339
if auth.db:
1321-
self.logger.debug(
1340+
auth.logger.debug(
13221341
f"AuthAPI.login: Calling get_or_register_user with data={data}"
13231342
)
13241343
user = auth.get_or_register_user(data)
1325-
self.logger.debug(
1344+
auth.logger.debug(
13261345
f"AuthAPI.login: User after get_or_register_user: {user}"
13271346
)
13281347
auth.store_user_in_session(user["id"])
13291348
# else: if we're here - check is OK, but user is not in the session - is it right?
13301349
else:
1331-
self.logger.debug(
1350+
auth.logger.debug(
13321351
f"AuthAPI.login: plugin.check_credentials failed for {username}"
13331352
)
13341353
data = auth._error(

py4web/utils/auth_plugins/ldap_plugin.py

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66
import logging
77
import sys
88

9-
import ldap # python-ldap
10-
import ldap.filter
9+
import ldap # python-ldap # type: ignore # pylance: ignore undefined
10+
import ldap.filter # type: ignore # pylance: ignore undefined
11+
from py4web import HTTP
1112

1213
from . import UsernamePassword
1314

@@ -90,7 +91,7 @@ class LDAPPlugin(UsernamePassword):
9091
9192
Where:
9293
manage_user: bool
93-
If True py4web will fetch and update user profile
94+
If True or AD is used, py4web will fetch and update user profile
9495
fields (first name, last name, email) from LDAP/AD on each login and
9596
keep them in sync with its db.
9697
If False, only authentication is performed and user profile fields
@@ -126,7 +127,6 @@ class LDAPPlugin(UsernamePassword):
126127
group_member_attrib - the attribute containing the group members name
127128
group_filterstr - as the filterstr but for group select
128129
129-
**allowed_groups still to be implemented in py4web**
130130
You can restrict login access to specific groups if you specify:
131131
132132
auth.register_plugin(LDAPPlugin(
@@ -169,6 +169,7 @@ def __init__(
169169
username_attrib="uid",
170170
custom_scope="subtree",
171171
allowed_groups=None,
172+
denied_login_popup=False,
172173
manage_user=False,
173174
user_firstname_attrib="cn:1",
174175
user_lastname_attrib="cn:2",
@@ -202,6 +203,7 @@ def __init__(
202203
self.username_attrib = username_attrib
203204
self.custom_scope = custom_scope
204205
self.allowed_groups = allowed_groups
206+
self.denied_login_popup = denied_login_popup
205207
self.manage_user = manage_user
206208
self.user_firstname_attrib = user_firstname_attrib
207209
self.user_lastname_attrib = user_lastname_attrib
@@ -219,6 +221,7 @@ def __init__(
219221
# rfc4515 syntax
220222
self.filterstr = self.filterstr.lstrip("(").rstrip(")")
221223
self.groups = groups
224+
self.last_user_mail = None
222225

223226
def check_credentials(self, username, password):
224227
base_dn = self.base_dn
@@ -266,11 +269,14 @@ def check_credentials(self, username, password):
266269
user_lastname_attrib = ldap.filter.escape_filter_chars(user_lastname_attrib)
267270
user_mail_attrib = ldap.filter.escape_filter_chars(user_mail_attrib)
268271
try:
269-
# if allowed_groups:
270-
# if not self.is_user_in_allowed_groups(
271-
# username, password, allowed_groups
272-
# ):
273-
# return False
272+
allowed_groups = self.allowed_groups
273+
if allowed_groups and mode == "ad":
274+
if not self.is_user_in_allowed_groups(
275+
username
276+
):
277+
logger.warning(f"[{username}] refused login because not in allowed groups!")
278+
279+
return False
274280
con = self._init_ldap()
275281
if mode == "ad":
276282
# Microsoft Active Directory
@@ -295,7 +301,7 @@ def check_credentials(self, username, password):
295301
# this will throw an index error if the account is not found
296302
# in the base_dn
297303
requested_attrs = ["sAMAccountName"]
298-
if manage_user:
304+
if manage_user or mode == "ad":
299305
requested_attrs.extend(
300306
[user_firstname_attrib, user_lastname_attrib, user_mail_attrib]
301307
)
@@ -306,7 +312,14 @@ def check_credentials(self, username, password):
306312
f"(&(sAMAccountName={ldap.filter.escape_filter_chars(username_bare)})({filterstr}))",
307313
requested_attrs,
308314
)[0][1]
315+
316+
# set last_user_mail from ldap result for further use
317+
if 'mail' in result and result['mail']:
318+
self.last_user_mail = str(result['mail'][0].decode() if isinstance(result['mail'][0], bytes) else result['mail'][0])
319+
else:
320+
self.last_user_mail = None
309321
logger.info(f"Login result: {result}")
322+
logger.debug(f"LDAPPlugin: Set last_user_mail to {self.last_user_mail} for {username}")
310323
if not isinstance(result, dict):
311324
# result should be a dict in the form
312325
# {'sAMAccountName': [username_bare]}
@@ -533,6 +546,8 @@ def check_credentials(self, username, password):
533546

534547
def is_user_in_allowed_groups(self, username, password=None):
535548
allowed_groups = self.allowed_groups
549+
logger = self.logger
550+
logger.debug(f"[{str(username)}] Check if user is in allowed groups")
536551

537552
"""
538553
Figure out if the username is a member of an allowed group
@@ -549,8 +564,12 @@ def is_user_in_allowed_groups(self, username, password=None):
549564
for group in allowed_groups:
550565
if group in self.groups:
551566
# Match
567+
logger.debug(f"[{str(username)}] allowed login because in allowed groups")
552568
return True
553569
# No match
570+
logger.warning(f"[{str(username)}] denied login because not in allowed groups")
571+
if self.denied_login_popup:
572+
raise HTTP(401, body=f"{str(username)} : you're not authorized to login!")
554573
return False
555574

556575
def do_manage_groups(self, con, username, group_mapping={}):
@@ -729,6 +748,8 @@ def get_user_groups_from_ldap(self, username=None, password=None):
729748
if "DC=" in x.upper():
730749
domain.append(x.split("=")[-1])
731750
username = f"{username}@{'.'.join(domain)}"
751+
if not group_dn:
752+
group_dn = base_dn
732753
username_bare = username.split("@")[0]
733754
con = self._init_ldap()
734755
con.set_option(ldap.OPT_PROTOCOL_VERSION, 3)
@@ -758,6 +779,11 @@ def get_user_groups_from_ldap(self, username=None, password=None):
758779
if username is None:
759780
return []
760781
# search for groups where user is in
782+
filter_full = (
783+
ldap.filter.escape_filter_chars(group_member_attrib),
784+
ldap.filter.escape_filter_chars(username),
785+
group_filterstr,
786+
)
761787
filter = f"(&({ldap.filter.escape_filter_chars(group_member_attrib)}=\
762788
{ldap.filter.escape_filter_chars(username)})({group_filterstr}))"
763789

0 commit comments

Comments
 (0)