1
# -*- coding: iso-8859-1 -*-
3
MoinMoin - modular authentication code
5
Here are some methods moin can use in cfg.auth authentication method list.
6
The methods from that list get called (from request.py) in that sequence.
7
They get request as first argument and also some more kw arguments:
8
name: the value we did get from a POST of the UserPreferences page
9
in the "name" form field (or None)
10
password: the value of the password form field (or None)
11
login: True if user has clicked on Login button
12
logout: True if user has clicked on Logout button
13
user_obj: the user_obj we have until now (user_obj returned from
14
previous auth method or None for first auth method)
15
(we maybe add some more here)
17
Use code like this to get them:
18
name = kw.get('name') or ''
19
password = kw.get('password') or ''
20
login = kw.get('login')
21
logout = kw.get('logout')
22
request.log("got name=%s len(password)=%d login=%r logout=%r" % (name, len(password), login, logout))
24
The called auth method then must return a tuple (user_obj, continue_flag).
25
user_obj can be one of:
26
* a (newly created) User object
27
* None if we want to inhibit log in from previous auth methods
28
* what we got as kw argument user_obj (meaning: no change).
29
continue_flag is a boolean indication whether the auth loop shall continue
30
trying other auth methods (or not).
32
The methods give a kw arg "auth_attribs" to User.__init__ that tells
33
which user attribute names are DETERMINED and set by this auth method and
34
must not get changed by the user using the UserPreferences form.
35
It also gives a kw arg "auth_method" that tells the name of the auth
36
method that authentified the user.
38
@copyright: 2005-2006 Bastian Blank, Florian Festi, Thomas Waldmann
39
@copyright: 2005-2006 MoinMoin:AlexanderSchremmer
40
@license: GNU GPL, see COPYING for details.
44
from MoinMoin import config, user
46
def log(request, **kw):
47
""" just log the call, do nothing else """
48
username = kw.get('name')
49
password = kw.get('password')
50
login = kw.get('login')
51
logout = kw.get('logout')
52
user_obj = kw.get('user_obj')
53
request.log("auth.log: name=%s login=%r logout=%r user_obj=%r" % (username, login, logout, user_obj))
56
# some cookie functions used by moin_cookie auth
57
def makeCookie(request, moin_id, maxage, expires):
58
""" calculate a MOIN_ID cookie """
59
c = Cookie.SimpleCookie()
61
c['MOIN_ID'] = moin_id
62
c['MOIN_ID']['max-age'] = maxage
64
c['MOIN_ID']['domain'] = cfg.cookie_domain
66
c['MOIN_ID']['path'] = cfg.cookie_path
68
path = request.getScriptname()
71
c['MOIN_ID']['path'] = path
72
# Set expires for older clients
73
c['MOIN_ID']['expires'] = request.httpDate(when=expires, rfc='850')
76
def setCookie(request, u):
77
""" Set cookie for the user obj u
79
cfg.cookie_lifetime and the user 'remember_me' setting set the
80
lifetime of the cookie. lifetime in int hours, see table:
83
----------------------------------------------------------------
84
= 0 forever, ignoring user 'remember_me' setting
85
> 0 n hours, or forever if user checked 'remember_me'
86
< 0 -n hours, ignoring user 'remember_me' setting
88
# Calculate cookie maxage and expires
89
lifetime = int(request.cfg.cookie_lifetime) * 3600
90
forever = 10*365*24*3600 # 10 years
101
expires = now + maxage
103
cookie = makeCookie(request, u.id, maxage, expires)
105
request.setHttpHeader(cookie)
106
# IMPORTANT: Prevent caching of current page and cookie
107
request.disableHttpCaching()
109
def deleteCookie(request):
110
""" Delete the user cookie by sending expired cookie with null value
112
According to http://www.cse.ohio-state.edu/cgi-bin/rfc/rfc2109.html#sec-4.2.2
113
Deleted cookie should have Max-Age=0. We also have expires
114
attribute, which is probably needed for older browsers.
116
Finally, delete the saved cookie and create a new user based on the new settings.
120
# Set expires to one year ago for older clients
121
expires = time.time() - (3600 * 24 * 365) # 1 year ago
122
cookie = makeCookie(request, moin_id, maxage, expires)
124
request.setHttpHeader(cookie)
125
# IMPORTANT: Prevent caching of current page and cookie
126
request.disableHttpCaching()
128
def moin_cookie(request, **kw):
129
""" authenticate via the MOIN_ID cookie """
130
username = kw.get('name')
131
password = kw.get('password')
132
login = kw.get('login')
133
logout = kw.get('logout')
134
user_obj = kw.get('user_obj')
135
#request.log("auth.moin_cookie: name=%s login=%r logout=%r user_obj=%r" % (username, login, logout, user_obj))
137
u = user.User(request, name=username, password=password,
138
auth_method='login_userpassword')
140
setCookie(request, u)
141
return u, True # we make continuing possible, e.g. for smbmount
142
return user_obj, True
145
cookie = Cookie.SimpleCookie(request.saved_cookie)
146
except Cookie.CookieError:
147
# ignore invalid cookies, else user can't relogin
149
if cookie and cookie.has_key('MOIN_ID'):
150
u = user.User(request, id=cookie['MOIN_ID'].value,
151
auth_method='moin_cookie', auth_attribs=())
154
u.valid = 0 # just make user invalid, but remember him
157
setCookie(request, u) # refreshes cookie lifetime
158
return u, True # use True to get other methods called, too
159
else: # logout or invalid user
160
deleteCookie(request)
161
return u, True # we return a invalidated user object, so that
162
# following auth methods can get the name of
163
# the user who logged out
164
return user_obj, True
167
def http(request, **kw):
168
""" authenticate via http basic/digest/ntlm auth """
169
from MoinMoin.request import RequestTwisted, RequestCLI
170
user_obj = kw.get('user_obj')
172
# check if we are running Twisted
173
if isinstance(request, RequestTwisted):
174
username = request.twistd.getUser().decode(config.charset)
175
password = request.twistd.getPassword().decode(config.charset)
176
# when using Twisted http auth, we use username and password from
177
# the moin user profile, so both can be changed by user.
178
u = user.User(request, auth_username=username, password=password,
179
auth_method='http', auth_attribs=())
181
elif not isinstance(request, RequestCLI):
183
auth_type = env.get('AUTH_TYPE','')
184
if auth_type in ['Basic', 'Digest', 'NTLM', 'Negotiate',]:
185
username = env.get('REMOTE_USER', '').decode(config.charset)
186
if auth_type in ('NTLM', 'Negotiate',):
187
# converting to standard case so the user can even enter wrong case
188
# (added since windows does not distinguish between e.g.
190
username = username.split('\\')[-1] # split off domain e.g.
192
# this "normalizes" the login name from {meier, Meier, MEIER} to Meier
193
# put a comment sign in front of next line if you don't want that:
194
username = username.title()
195
# when using http auth, we have external user name and password,
196
# we don't use the moin user profile for those attributes.
197
u = user.User(request, auth_username=username,
198
auth_method='http', auth_attribs=('name', 'password'))
203
return u, True # True to get other methods called, too
205
return user_obj, True
207
def sslclientcert(request, **kw):
208
""" authenticate via SSL client certificate """
209
from MoinMoin.request import RequestTwisted
210
user_obj = kw.get('user_obj')
213
# check if we are running Twisted
214
if isinstance(request, RequestTwisted):
215
return user_obj, True # not supported if we run twisted
216
# Addendum: this seems to need quite some twisted insight and coding.
217
# A pointer i got on #twisted: divmod's vertex.sslverify
218
# If you really need this, feel free to implement and test it and
219
# submit a patch if it works.
222
if env.get('SSL_CLIENT_VERIFY', 'FAILURE') == 'SUCCESS':
223
# if we only want to accept some specific CA, do a check like:
224
# if env.get('SSL_CLIENT_I_DN_OU') == "http://www.cacert.org"
225
email = env.get('SSL_CLIENT_S_DN_Email', '').decode(config.charset)
226
email_lower = email.lower()
227
commonname = env.get('SSL_CLIENT_S_DN_CN', '').decode(config.charset)
228
commonname_lower = commonname.lower()
229
if email_lower or commonname_lower:
230
for uid in user.getUserList(request):
231
u = user.User(request, uid,
232
auth_method='sslclientcert', auth_attribs=())
233
if email_lower and u.email.lower() == email_lower:
234
u.auth_attribs = ('email', 'password')
235
#this is only useful if same name should be used, as
236
#commonname is likely no CamelCase WikiName
237
#if commonname_lower != u.name.lower():
238
# u.name = commonname
240
#u.auth_attribs = ('email', 'name', 'password')
242
if commonname_lower and u.name.lower() == commonname_lower:
243
u.auth_attribs = ('name', 'password')
244
#this is only useful if same email should be used as
245
#specified in certificate.
246
#if email_lower != u.email.lower():
249
#u.auth_attribs = ('name', 'email', 'password')
254
# user wasn't found, so let's create a new user object
255
u = user.User(request, name=commonname_lower, auth_username=commonname_lower)
258
u.create_or_update(changed)
262
return user_obj, True
265
def smb_mount(request, **kw):
266
""" (u)mount a SMB server's share for username (using username/password for
267
authentication at the SMB server). This can be used if you need access
268
to files on some share via the wiki, but needs more code to be useful.
269
If you don't need it, don't use it.
271
username = kw.get('name')
272
password = kw.get('password')
273
login = kw.get('login')
274
logout = kw.get('logout')
275
user_obj = kw.get('user_obj')
277
verbose = cfg.smb_verbose
278
if verbose: request.log("got name=%s login=%r logout=%r" % (username, login, logout))
280
# we just intercept login to mount and logout to umount the smb share
282
import os, pwd, subprocess
283
web_username = cfg.smb_dir_user
284
web_uid = pwd.getpwnam(web_username)[2] # XXX better just use current uid?
285
if logout and user_obj: # logout -> we don't have username in form
286
username = user_obj.name # so we take it from previous auth method (moin_cookie e.g.)
287
mountpoint = cfg.smb_mountpoint % {
288
'username': username,
291
cmd = u"sudo mount -t cifs -o user=%(user)s,domain=%(domain)s,uid=%(uid)d,dir_mode=%(dir_mode)s,file_mode=%(file_mode)s,iocharset=%(iocharset)s //%(server)s/%(share)s %(mountpoint)s >>%(log)s 2>&1"
293
cmd = u"sudo umount %(mountpoint)s >>%(log)s 2>&1"
298
'domain': cfg.smb_domain,
299
'server': cfg.smb_server,
300
'share': cfg.smb_share,
301
'mountpoint': mountpoint,
302
'dir_mode': cfg.smb_dir_mode,
303
'file_mode': cfg.smb_file_mode,
304
'iocharset': cfg.smb_iocharset,
307
env = os.environ.copy()
310
os.makedirs(mountpoint) # the dir containing the mountpoint must be writeable for us!
313
env['PASSWD'] = password.encode(cfg.smb_coding)
314
subprocess.call(cmd.encode(cfg.smb_coding), env=env, shell=True)
315
return user_obj, True
318
def ldap_login(request, **kw):
319
""" get authentication data from form, authenticate against LDAP (or Active Directory),
320
fetch some user infos from LDAP and create a user profile for that user that must
321
be used by subsequent auth plugins (like moin_cookie) as we never return a user
322
object from ldap_login.
323
python-ldap needs to be at least 2.0.0pre06 (available since mid 2002) for ldaps support -
324
some older debian installations (woody and older?) require libldap2-tls and python2.x-ldap-tls,
325
otherwise you get ldap.SERVER_DOWN: "Can't contact LDAP server" -
326
more recent debian installations have tls support in libldap2 (see dependency on gnutls)
327
and also in python-ldap.
328
use ldaps://server:636 url for ldaps, ldap://server for ldap with tls (plus some tls code, todo)
330
username = kw.get('name')
331
password = kw.get('password')
332
login = kw.get('login')
333
logout = kw.get('logout')
334
user_obj = kw.get('user_obj')
337
verbose = cfg.ldap_verbose
339
if verbose: request.log("got name=%s login=%r logout=%r" % (username, login, logout))
341
# we just intercept login for ldap, other requests have to be
342
# handled by another auth handler
344
return user_obj, True
346
# we require non-empty password as ldap bind does a anon (not password
347
# protected) bind if the password is empty and SUCCEEDS!
358
coding = cfg.ldap_coding
359
if verbose: request.log("LDAP: Setting misc. options...")
360
ldap.set_option(ldap.OPT_PROTOCOL_VERSION, ldap.VERSION3) # ldap v2 is outdated
361
ldap.set_option(ldap.OPT_REFERRALS, 0) # needed for Active Directory:
362
ldap.set_option(ldap.OPT_NETWORK_TIMEOUT, cfg.ldap_timeout)
364
starttls = cfg.ldap_start_tls
366
for option, value in (
367
(ldap.OPT_X_TLS_CACERTDIR, cfg.ldap_tls_cacertdir),
368
(ldap.OPT_X_TLS_CACERTFILE, cfg.ldap_tls_cacertfile),
369
(ldap.OPT_X_TLS_CERTFILE, cfg.ldap_tls_certfile),
370
(ldap.OPT_X_TLS_KEYFILE, cfg.ldap_tls_keyfile),
371
(ldap.OPT_X_TLS_REQUIRE_CERT, cfg.ldap_tls_require_cert), # ldap.OPT_X_TLS_NEVER - this is needed for self-signed ssl certs
372
(ldap.OPT_X_TLS, starttls),
373
#(ldap.OPT_X_TLS_ALLOW, 1),
376
ldap.set_option(option, value)
378
server = cfg.ldap_uri
379
if verbose: request.log("LDAP: Trying to initialize %s." % server)
380
l = ldap.initialize(server)
381
if verbose: request.log("LDAP: Connected to LDAP server %s." % server)
383
if starttls and server.startswith('ldap:'):
384
if verbose: request.log("LDAP: Trying to start TLS to %s." % server)
387
if verbose: request.log("LDAP: Using TLS to %s." % server)
388
except (ldap.SERVER_DOWN, ldap.CONNECT_ERROR), err:
389
if verbose: request.log("LDAP: Couldn't establish TLS to %s (err: %s)." % (server, str(err)))
392
# you can use %(username)s and %(password)s here to get the stuff entered in the form:
393
ldap_binddn = cfg.ldap_binddn % locals()
394
ldap_bindpw = cfg.ldap_bindpw % locals()
395
l.simple_bind_s(ldap_binddn.encode(coding), ldap_bindpw.encode(coding))
396
if verbose: request.log("LDAP: Bound with binddn %s" % ldap_binddn)
398
# you can use %(username)s here to get the stuff entered in the form:
399
filterstr = cfg.ldap_filter % locals()
400
if verbose: request.log("LDAP: Searching %s" % filterstr)
401
lusers = l.search_st(cfg.ldap_base, cfg.ldap_scope, filterstr.encode(coding),
402
attrlist=[cfg.ldap_email_attribute,
403
cfg.ldap_aliasname_attribute,
404
cfg.ldap_surname_attribute,
405
cfg.ldap_givenname_attribute,
406
], timeout=cfg.ldap_timeout)
407
# we remove entries with dn == None to get the real result list:
408
lusers = [(dn, ldap_dict) for dn, ldap_dict in lusers if dn is not None]
410
for dn, ldap_dict in lusers:
411
request.log("LDAP: dn:%s" % dn)
412
for key, val in ldap_dict.items():
413
request.log(" %s: %s" % (key, val))
415
result_length = len(lusers)
416
if result_length != 1:
417
if result_length > 1:
418
request.log("LDAP: Search found more than one (%d) matches for %s." % (result_length, filterstr))
419
if result_length == 0:
420
if verbose: request.log("LDAP: Search found no matches for %s." % (filterstr, ))
421
return None, False # if ldap returns unusable results, we veto the user and don't let him in
423
dn, ldap_dict = lusers[0]
424
if verbose: request.log("LDAP: DN found is %s, trying to bind with pw" % dn)
425
l.simple_bind_s(dn, password.encode(coding))
426
if verbose: request.log("LDAP: Bound with dn %s (username: %s)" % (dn, username))
428
email = ldap_dict.get(cfg.ldap_email_attribute, [''])[0]
429
email = email.decode(coding)
433
aliasname = ldap_dict[cfg.ldap_aliasname_attribute][0]
434
except (KeyError, IndexError):
437
sn = ldap_dict.get(cfg.ldap_surname_attribute, [''])[0]
438
gn = ldap_dict.get(cfg.ldap_givenname_attribute, [''])[0]
440
aliasname = "%s, %s" % (sn, gn)
443
aliasname = aliasname.decode(coding)
445
u = user.User(request, auth_username=username, password=password, auth_method='ldap', auth_attribs=('name', 'password', 'email', 'mailto_author',))
447
u.aliasname = aliasname
449
u.remember_me = 0 # 0 enforces cookie_lifetime config param
450
if verbose: request.log("LDAP: creating userprefs with name %s email %s alias %s" % (username, email, aliasname))
452
except ldap.INVALID_CREDENTIALS, err:
453
request.log("LDAP: invalid credentials (wrong password?) for dn %s (username: %s)" % (dn, username))
454
return None, False # if ldap says no, we veto the user and don't let him in
457
u.create_or_update(True)
458
return user_obj, True # == nop, moin_cookie has to set the cookie and return the user obj
462
info = sys.exc_info()
463
request.log("LDAP: caught an exception, traceback follows...")
464
request.log(''.join(traceback.format_exception(*info)))
465
return None, False # something went completely wrong, in doubt we veto the login
468
def interwiki(request, **kw):
469
# TODO use auth_method and auth_attribs for User object
470
username = kw.get('name')
471
password = kw.get('password')
472
login = kw.get('login')
473
logout = kw.get('logout')
474
user_obj = kw.get('user_obj')
477
wikitag, wikiurl, wikitail, err = wikiutil.resolve_wiki(username)
479
if err or wikitag not in request.cfg.trusted_wikis:
480
return user_obj, True
484
homewiki = xmlrpclib.Server(wikiurl + "?action=xmlrpc2")
485
account_data = homewiki.getUser(wikitail, password)
486
if isinstance(account_data, str):
488
return user_obj, True
490
u = user.User(request, name=username)
491
for key, value in account_data.iteritems():
492
if key not in ["may", "id", "valid", "trusted"
496
setattr(u, key, value)
498
setCookie(request, u)
502
# XXX redirect to homewiki
504
return user_obj, True
508
""" Authentication module for PHP based frameworks
509
Authenticates via PHP session cookie. Currently supported systems:
511
* eGroupware 1.2 ("egw")
512
* You need to configure eGroupware in the "header setup" to use
513
"php sessions plus restore"
515
@copyright: 2005 by MoinMoin:AlexanderSchremmer
516
- Thanks to Spreadshirt
519
def __init__(self, apps=['egw'], s_path="/tmp", s_prefix="sess_"):
520
""" @param apps: A list of the enabled applications. See above for
522
@param s_path: The path where the PHP sessions are stored.
523
@param s_prefix: The prefix of the session files.
527
self.s_prefix = s_prefix
530
def __call__(self, request, **kw):
531
def handle_egroupware(session):
532
""" Extracts name, fullname and email from the session. """
533
username = session['egw_session']['session_lid'].split("@", 1)[0]
534
known_accounts = session['egw_info_cache']['accounts']['cache']['account_data']
536
# if the next line breaks, then the cache was not filled with the current
538
user_info = [value for key, value in known_accounts.items()
539
if value['account_lid'] == username][0]
540
name = user_info.get('fullname', '')
541
email = user_info.get('email', '')
543
dec = lambda x: x and x.decode("iso-8859-1")
545
return dec(username), dec(email), dec(name)
547
import Cookie, urllib
548
from MoinMoin.user import User
549
from MoinMoin.util import sessionParser
551
user_obj = kw.get('user_obj')
553
cookie = Cookie.SimpleCookie(request.saved_cookie)
554
except Cookie.CookieError: # ignore invalid cookies
557
for cookiename in cookie.keys():
558
cookievalue = urllib.unquote(cookie[cookiename].value).decode('iso-8859-1')
559
session = sessionParser.loadSession(cookievalue, path=self.s_path, prefix=self.s_prefix)
561
if "egw" in self.apps and session.get('egw_session', None):
562
username, email, name = handle_egroupware(session)
565
return user_obj, True
567
user = User(request, name=username, auth_username=username)
570
if name != user.aliasname:
571
user.aliasname = name
573
if email != user.email:
578
user.create_or_update(changed)
579
if user and user.valid:
580
return user, True # True to get other methods called, too
581
return user_obj, True # continue with next method in auth list