1 2# pam.py - functions authentication, authorisation and session handling 3# 4# Copyright (C) 2010-2019 Arthur de Jong 5# 6# This library is free software; you can redistribute it and/or 7# modify it under the terms of the GNU Lesser General Public 8# License as published by the Free Software Foundation; either 9# version 2.1 of the License, or (at your option) any later version. 10# 11# This library is distributed in the hope that it will be useful, 12# but WITHOUT ANY WARRANTY; without even the implied warranty of 13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14# Lesser General Public License for more details. 15# 16# You should have received a copy of the GNU Lesser General Public 17# License along with this library; if not, write to the Free Software 18# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 19# 02110-1301 USA 20 21import logging 22import random 23import socket 24import time 25 26import ldap 27from ldap.controls.ppolicy import PasswordPolicyControl, PasswordPolicyError 28from ldap.filter import escape_filter_chars 29 30import cfg 31import common 32import constants 33import passwd 34import search 35import shadow 36 37 38random = random.SystemRandom() 39 40 41def authenticate(binddn, password): 42 # open a new connection 43 conn = search.Connection() 44 # bind using the specified credentials 45 pwctrl = PasswordPolicyControl() 46 res, data, msgid, ctrls = conn.simple_bind_s(binddn, password, serverctrls=[pwctrl]) 47 # go over bind result server controls 48 for ctrl in ctrls: 49 if ctrl.controlType == PasswordPolicyControl.controlType: 50 # found a password policy control 51 logging.debug( 52 'PasswordPolicyControl found: error=%s (%s), ' 53 'timeBeforeExpiration=%s, graceAuthNsRemaining=%s', 54 'None' if ctrl.error is None else PasswordPolicyError(ctrl.error).prettyPrint(), 55 ctrl.error, ctrl.timeBeforeExpiration, ctrl.graceAuthNsRemaining) 56 if ctrl.error == 0: # passwordExpired 57 return ( 58 conn, constants.NSLCD_PAM_AUTHTOK_EXPIRED, 59 PasswordPolicyError(ctrl.error).prettyPrint()) 60 elif ctrl.error == 1: # accountLocked 61 return ( 62 conn, constants.NSLCD_PAM_ACCT_EXPIRED, 63 PasswordPolicyError(ctrl.error).prettyPrint()) 64 elif ctrl.error == 2: # changeAfterReset 65 return ( 66 conn, constants.NSLCD_PAM_NEW_AUTHTOK_REQD, 67 'Password change is needed after reset') 68 elif ctrl.error: 69 return ( 70 conn, constants.NSLCD_PAM_PERM_DENIED, 71 PasswordPolicyError(ctrl.error).prettyPrint()) 72 elif ctrl.timeBeforeExpiration is not None: 73 return ( 74 conn, constants.NSLCD_PAM_NEW_AUTHTOK_REQD, 75 'Password will expire in %d seconds' % ctrl.timeBeforeExpiration) 76 elif ctrl.graceAuthNsRemaining is not None: 77 return ( 78 conn, constants.NSLCD_PAM_NEW_AUTHTOK_REQD, 79 'Password expired, %d grace logins left' % ctrl.graceAuthNsRemaining) 80 # perform search for own object (just to do any kind of search) 81 results = search.LDAPSearch( 82 conn, base=binddn, scope=ldap.SCOPE_BASE, 83 filter='(objectClass=*)', attributes=['dn']) 84 for entry in results: 85 if entry[0] == binddn: 86 return conn, constants.NSLCD_PAM_SUCCESS, '' 87 # if our DN wasn't found raise an error to signal bind failure 88 raise ldap.NO_SUCH_OBJECT() 89 90 91def pwmod(conn, userdn, oldpassword, newpassword): 92 # perform request without old password 93 try: 94 conn.passwd_s(userdn, None, newpassword) 95 except ldap.LDAPError: 96 # retry with old password 97 if oldpassword: 98 conn.passwd_s(userdn, oldpassword, newpassword) 99 else: 100 raise 101 102 103def update_lastchange(conns, userdn): 104 """Try to update the shadowLastChange attribute of the entry.""" 105 attribute = shadow.attmap['shadowLastChange'] 106 if str(attribute) == '"${shadowLastChange:--1}"': 107 attribute = 'shadowLastChange' 108 if not attribute or '$' in str(attribute): 109 raise ValueError('shadowLastChange has unsupported mapping') 110 # build the value for the new attribute 111 if attribute.lower() == 'pwdlastset': 112 # for AD we use another timestamp */ 113 value = '%d000000000' % (int(time.time()) // 100 + (134774 * 864)) 114 else: 115 # time in days since Jan 1, 1970 116 value = '%d' % (int(time.time()) // (60 * 60 * 24)) 117 # perform the modification, return at first success 118 for conn in conns: 119 try: 120 conn.modify_s(userdn, [(ldap.MOD_REPLACE, attribute, [value.encode('utf-8')])]) 121 return 122 except ldap.LDAPError: 123 pass # ignore error and try next connection 124 125 126class PAMRequest(common.Request): 127 128 def validate(self, parameters): 129 """Check the username for validity and fill in the DN if needed.""" 130 # check username for validity 131 common.validate_name(parameters['username']) 132 # look up user DN 133 entry = passwd.uid2entry(self.conn, parameters['username']) 134 if not entry: 135 # FIXME: we should close the stream with an empty response here 136 raise ValueError('%r: user not found' % parameters['username']) 137 # save the DN 138 parameters['userdn'] = entry[0] 139 # get the "real" username 140 value = passwd.attmap.get_rdn_value(entry[0], 'uid') 141 if not value: 142 # get the username from the uid attribute 143 values = entry[1]['uid'] 144 if not values or not values[0]: 145 logging.warning('%s: is missing a %s attribute', entry[0], passwd.attmap['uid']) 146 value = values[0] 147 # check the username 148 if value and not common.is_valid_name(value): 149 raise ValueError('%s: has invalid %s attribute', entry[0], passwd.attmap['uid']) 150 # check if the username is different and update it if needed 151 if value != parameters['username']: 152 logging.info('username changed from %r to %r', parameters['username'], value) 153 parameters['username'] = value 154 155 156class PAMAuthenticationRequest(PAMRequest): 157 158 action = constants.NSLCD_ACTION_PAM_AUTHC 159 160 def read_parameters(self, fp): 161 return dict(username=fp.read_string(), 162 service=fp.read_string(), 163 ruser=fp.read_string(), 164 rhost=fp.read_string(), 165 tty=fp.read_string(), 166 password=fp.read_string()) 167 # TODO: log call with parameters 168 169 def write(self, username, authc=constants.NSLCD_PAM_SUCCESS, 170 authz=constants.NSLCD_PAM_SUCCESS, msg=''): 171 self.fp.write_int32(constants.NSLCD_RESULT_BEGIN) 172 self.fp.write_int32(authc) 173 self.fp.write_string(username) 174 self.fp.write_int32(authz) 175 self.fp.write_string(msg) 176 self.fp.write_int32(constants.NSLCD_RESULT_END) 177 178 def handle_request(self, parameters): 179 # if the username is blank and rootpwmoddn is configured, try to 180 # authenticate as administrator, otherwise validate request as usual 181 if not parameters['username'] and cfg.rootpwmoddn: 182 # authenticate as rootpwmoddn 183 binddn = cfg.rootpwmoddn 184 # if the caller is root we will allow the use of rootpwmodpw 185 if not parameters['password'] and self.calleruid == 0 and cfg.rootpwmodpw: 186 password = cfg.rootpwmodpw 187 elif parameters['password']: 188 password = parameters['password'] 189 else: 190 raise ValueError('password missing') 191 else: 192 self.validate(parameters) 193 binddn = parameters['userdn'] 194 password = parameters['password'] 195 # try authentication 196 try: 197 conn, authz, msg = authenticate(binddn, password) 198 except ldap.INVALID_CREDENTIALS as e: 199 try: 200 msg = e[0]['desc'] 201 except Exception: 202 msg = str(e) 203 logging.debug('bind failed: %s', msg) 204 self.write(parameters['username'], authc=constants.NSLCD_PAM_AUTH_ERR, msg=msg) 205 return 206 if authz != constants.NSLCD_PAM_SUCCESS: 207 logging.warning('%s: %s: %s', binddn, parameters['username'], msg) 208 else: 209 logging.debug('bind successful') 210 # FIXME: perform shadow attribute checks with check_shadow() 211 self.write(parameters['username'], authz=authz, msg=msg) 212 213 214class PAMAuthorisationRequest(PAMRequest): 215 216 action = constants.NSLCD_ACTION_PAM_AUTHZ 217 218 def read_parameters(self, fp): 219 return dict(username=fp.read_string(), 220 service=fp.read_string(), 221 ruser=fp.read_string(), 222 rhost=fp.read_string(), 223 tty=fp.read_string()) 224 # TODO: log call with parameters 225 226 def write(self, authz=constants.NSLCD_PAM_SUCCESS, msg=''): 227 self.fp.write_int32(constants.NSLCD_RESULT_BEGIN) 228 self.fp.write_int32(authz) 229 self.fp.write_string(msg) 230 self.fp.write_int32(constants.NSLCD_RESULT_END) 231 232 def check_authz_search(self, parameters): 233 if not cfg.pam_authz_searches: 234 return 235 # escape all parameters 236 variables = dict((k, escape_filter_chars(v)) for k, v in parameters.items()) 237 variables.update( 238 hostname=escape_filter_chars(socket.gethostname()), 239 fqdn=escape_filter_chars(socket.getfqdn()), 240 dn=variables['userdn'], 241 uid=variables['username']) 242 # go over all authz searches 243 for x in cfg.pam_authz_searches: 244 filter = x.value(variables) 245 logging.debug('trying pam_authz_search "%s"', filter) 246 srch = search.LDAPSearch(self.conn, filter=filter, attributes=('dn', )) 247 try: 248 dn, values = srch.items().next() 249 except StopIteration: 250 logging.error('pam_authz_search "%s" found no matches', filter) 251 raise 252 logging.debug('pam_authz_search found "%s"', dn) 253 254 def handle_request(self, parameters): 255 # fill in any missing userdn, etc. 256 self.validate(parameters) 257 # check authorisation search 258 try: 259 self.check_authz_search(parameters) 260 except StopIteration: 261 self.write(constants.NSLCD_PAM_PERM_DENIED, 262 'LDAP authorisation check failed') 263 return 264 # all tests passed, return OK response 265 self.write() 266 267 268class PAMPasswordModificationRequest(PAMRequest): 269 270 action = constants.NSLCD_ACTION_PAM_PWMOD 271 272 def read_parameters(self, fp): 273 return dict(username=fp.read_string(), 274 service=fp.read_string(), 275 ruser=fp.read_string(), 276 rhost=fp.read_string(), 277 tty=fp.read_string(), 278 asroot=fp.read_int32(), 279 oldpassword=fp.read_string(), 280 newpassword=fp.read_string()) 281 # TODO: log call with parameters 282 283 def write(self, rc=constants.NSLCD_PAM_SUCCESS, msg=''): 284 self.fp.write_int32(constants.NSLCD_RESULT_BEGIN) 285 self.fp.write_int32(rc) 286 self.fp.write_string(msg) 287 self.fp.write_int32(constants.NSLCD_RESULT_END) 288 289 def handle_request(self, parameters): 290 # fill in any missing userdn, etc. 291 self.validate(parameters) 292 # check if pam_password_prohibit_message is set 293 if cfg.pam_password_prohibit_message: 294 self.write(constants.NSLCD_PAM_PERM_DENIED, 295 cfg.pam_password_prohibit_message) 296 return 297 # check if the the user passed the rootpwmoddn 298 if parameters['asroot']: 299 binddn = cfg.rootpwmoddn 300 # check if rootpwmodpw should be used 301 if not parameters['oldpassword'] and self.calleruid == 0 and cfg.rootpwmodpw: 302 password = cfg.rootpwmodpw 303 elif parameters['oldpassword']: 304 password = parameters['oldpassword'] 305 else: 306 raise ValueError('password missing') 307 else: 308 binddn = parameters['userdn'] 309 password = parameters['oldpassword'] 310 # TODO: check if shadow properties allow password change 311 # perform password modification 312 try: 313 conn, authz, msg = authenticate(binddn, password) 314 pwmod(conn, parameters['userdn'], parameters['oldpassword'], parameters['newpassword']) 315 # try to update lastchange with normal or user connection 316 update_lastchange((self.conn, conn), parameters['userdn']) 317 except ldap.INVALID_CREDENTIALS as e: 318 try: 319 msg = e[0]['desc'] 320 except Exception: 321 msg = str(e) 322 logging.debug('pwmod failed: %s', msg) 323 self.write(constants.NSLCD_PAM_PERM_DENIED, msg) 324 return 325 logging.debug('pwmod successful') 326 self.write() 327 328 329SESSION_ID_LENGTH = 25 330SESSION_ID_ALPHABET = ( 331 "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + 332 "abcdefghijklmnopqrstuvwxyz" + 333 "01234567890" 334) 335 336 337def generate_session_id(): 338 return ''.join( 339 random.choice(SESSION_ID_ALPHABET) 340 for i in range(SESSION_ID_LENGTH) 341 ) 342 343 344class PAMSessionOpenRequest(PAMRequest): 345 346 action = constants.NSLCD_ACTION_PAM_SESS_O 347 348 def read_parameters(self, fp): 349 return dict(username=fp.read_string(), 350 service=fp.read_string(), 351 ruser=fp.read_string(), 352 rhost=fp.read_string(), 353 tty=fp.read_string()) 354 # TODO: log call with parameters 355 356 def write(self, sessionid): 357 self.fp.write_int32(constants.NSLCD_RESULT_BEGIN) 358 self.fp.write_string(sessionid) 359 self.fp.write_int32(constants.NSLCD_RESULT_END) 360 361 def handle_request(self, parameters): 362 # generate a session id 363 session_id = generate_session_id() 364 self.write(session_id) 365 366 367class PAMSessionCloseRequest(PAMRequest): 368 369 action = constants.NSLCD_ACTION_PAM_SESS_C 370 371 def read_parameters(self, fp): 372 return dict(username=fp.read_string(), 373 service=fp.read_string(), 374 ruser=fp.read_string(), 375 rhost=fp.read_string(), 376 tty=fp.read_string(), 377 session_id=fp.read_string()) 378 # TODO: log call with parameters 379 380 def write(self): 381 self.fp.write_int32(constants.NSLCD_RESULT_BEGIN) 382 self.fp.write_int32(constants.NSLCD_RESULT_END) 383 384 def handle_request(self, parameters): 385 self.write() 386