Skip to content

Commit cbd3899

Browse files
committed
+ pam authentication for non-root use
rewrote check_credentials to be exception-based will always pass base_dir to cherrypy.quickstart
1 parent b621e15 commit cbd3899

File tree

3 files changed

+160
-25
lines changed

3 files changed

+160
-25
lines changed

auth.py

Lines changed: 27 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,19 @@ def check_credentials(username, password):
1717
try:
1818
enc_pwd = getspnam(username)[1]
1919
except KeyError:
20-
return "user '%s' not found" % username
20+
raise OSError("user '%s' not found" % username)
2121
else:
22-
if enc_pwd in ["NP", "!", "", None]:
23-
return "user '%s' has no password set" % username
24-
elif enc_pwd in ["LK", "*"]:
25-
return 'account is locked'
22+
if enc_pwd in ['NP', '!', '', None]:
23+
raise OSError("user '%s' has no password set" % username)
24+
elif enc_pwd in ['LK', '*']:
25+
raise OSError('account is locked')
2626
elif enc_pwd == "!!":
27-
return 'password is expired'
27+
raise OSError('password is expired')
2828

2929
if crypt(password, enc_pwd) == enc_pwd:
30-
return None
30+
return True
3131
else:
32-
return "incorrect password"
32+
raise OSError('incorrect password')
3333

3434
def check_auth(*args, **kwargs):
3535
"""A tool that looks in config for 'auth.require'. If found and it
@@ -111,26 +111,32 @@ def on_login(self, username):
111111
def on_logout(self, username):
112112
"""Called on logout"""
113113

114-
def get_loginform(self, username, msg="Enter Username and Password", from_page="/"):
114+
def get_loginform(self):
115115
import os
116116
from cgi import escape
117117
from cherrypy.lib.static import serve_file
118118

119119
return serve_file(os.path.join(os.getcwd(),'login.html'))
120120

121121
@cherrypy.expose
122-
def login(self, username=None, password=None, from_page="/"):
123-
if username is None or password is None:
124-
return self.get_loginform("", from_page=from_page)
125-
126-
error_msg = check_credentials(username, password)
127-
if error_msg:
128-
return self.get_loginform(username, error_msg, from_page)
129-
else:
130-
cherrypy.session.regenerate()
131-
cherrypy.session[SESSION_KEY] = cherrypy.request.login = username
132-
self.on_login(username)
133-
raise cherrypy.HTTPRedirect("/")
122+
def login(self, username=None, password=None, from_page='/'):
123+
if not username or not password:
124+
return self.get_loginform()
125+
126+
validated = False
127+
try:
128+
validated = check_credentials(username, password)
129+
except OSError:
130+
import pam
131+
validated = pam.authenticate(username, password)
132+
finally:
133+
if validated:
134+
cherrypy.session.regenerate()
135+
cherrypy.session[SESSION_KEY] = cherrypy.request.login = username
136+
self.on_login(username)
137+
raise cherrypy.HTTPRedirect("/")
138+
else:
139+
return self.get_loginform()
134140

135141
@cherrypy.expose
136142
def logout(self):

pam.py

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
# (c) 2007 Chris AtLee <chris@atlee.ca>
2+
# Licensed under the MIT license:
3+
# http://www.opensource.org/licenses/mit-license.php
4+
"""
5+
PAM module for python
6+
7+
Provides an authenticate function that will allow the caller to authenticate
8+
a user against the Pluggable Authentication Modules (PAM) on the system.
9+
10+
Implemented using ctypes, so no compilation is necessary.
11+
"""
12+
__all__ = ['authenticate']
13+
14+
from ctypes import CDLL, POINTER, Structure, CFUNCTYPE, cast, pointer, sizeof
15+
from ctypes import c_void_p, c_uint, c_char_p, c_char, c_int
16+
from ctypes.util import find_library
17+
18+
LIBPAM = CDLL(find_library("pam"))
19+
LIBC = CDLL(find_library("c"))
20+
21+
CALLOC = LIBC.calloc
22+
CALLOC.restype = c_void_p
23+
CALLOC.argtypes = [c_uint, c_uint]
24+
25+
STRDUP = LIBC.strdup
26+
STRDUP.argstypes = [c_char_p]
27+
STRDUP.restype = POINTER(c_char) # NOT c_char_p !!!!
28+
29+
# Various constants
30+
PAM_PROMPT_ECHO_OFF = 1
31+
PAM_PROMPT_ECHO_ON = 2
32+
PAM_ERROR_MSG = 3
33+
PAM_TEXT_INFO = 4
34+
35+
class PamHandle(Structure):
36+
"""wrapper class for pam_handle_t"""
37+
_fields_ = [
38+
("handle", c_void_p)
39+
]
40+
41+
def __init__(self):
42+
Structure.__init__(self)
43+
self.handle = 0
44+
45+
class PamMessage(Structure):
46+
"""wrapper class for pam_message structure"""
47+
_fields_ = [
48+
("msg_style", c_int),
49+
("msg", POINTER(c_char)),
50+
]
51+
52+
def __repr__(self):
53+
return "<PamMessage %i '%s'>" % (self.msg_style, self.msg)
54+
55+
class PamResponse(Structure):
56+
"""wrapper class for pam_response structure"""
57+
_fields_ = [
58+
("resp", POINTER(c_char)),
59+
("resp_retcode", c_int),
60+
]
61+
62+
def __repr__(self):
63+
return "<PamResponse %i '%s'>" % (self.resp_retcode, self.resp)
64+
65+
CONV_FUNC = CFUNCTYPE(c_int,
66+
c_int, POINTER(POINTER(PamMessage)),
67+
POINTER(POINTER(PamResponse)), c_void_p)
68+
69+
class PamConv(Structure):
70+
"""wrapper class for pam_conv structure"""
71+
_fields_ = [
72+
("conv", CONV_FUNC),
73+
("appdata_ptr", c_void_p)
74+
]
75+
76+
PAM_START = LIBPAM.pam_start
77+
PAM_START.restype = c_int
78+
PAM_START.argtypes = [c_char_p, c_char_p, POINTER(PamConv),
79+
POINTER(PamHandle)]
80+
81+
PAM_END = LIBPAM.pam_end
82+
PAM_END.restpe = c_int
83+
PAM_END.argtypes = [PamHandle, c_int]
84+
85+
PAM_AUTHENTICATE = LIBPAM.pam_authenticate
86+
PAM_AUTHENTICATE.restype = c_int
87+
PAM_AUTHENTICATE.argtypes = [PamHandle, c_int]
88+
89+
def authenticate(username, password, service='login'):
90+
"""Returns True if the given username and password authenticate for the
91+
given service. Returns False otherwise
92+
93+
``username``: the username to authenticate
94+
95+
``password``: the password in plain text
96+
97+
``service``: the PAM service to authenticate against.
98+
Defaults to 'login'"""
99+
@CONV_FUNC
100+
def my_conv(n_messages, messages, p_response, app_data):
101+
"""Simple conversation function that responds to any
102+
prompt where the echo is off with the supplied password"""
103+
# Create an array of n_messages response objects
104+
addr = CALLOC(n_messages, sizeof(PamResponse))
105+
p_response[0] = cast(addr, POINTER(PamResponse))
106+
for i in range(n_messages):
107+
if messages[i].contents.msg_style == PAM_PROMPT_ECHO_OFF:
108+
pw_copy = STRDUP(str(password))
109+
p_response.contents[i].resp = pw_copy
110+
p_response.contents[i].resp_retcode = 0
111+
return 0
112+
113+
handle = PamHandle()
114+
conv = PamConv(my_conv, 0)
115+
retval = PAM_START(service, username, pointer(conv), pointer(handle))
116+
117+
if retval != 0:
118+
# TODO: This is not an authentication error, something
119+
# has gone wrong starting up PAM
120+
PAM_END(handle, retval)
121+
return False
122+
123+
retval = PAM_AUTHENTICATE(handle, 0)
124+
e = PAM_END(handle, retval)
125+
return retval == 0 and e == 0
126+
127+
if __name__ == "__main__":
128+
import getpass
129+
print authenticate(getpass.getuser(), getpass.getpass())

server.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -399,7 +399,7 @@ def to_jsonable_type(retval):
399399
if __name__ == "__main__":
400400
from argparse import ArgumentParser
401401

402-
parser = ArgumentParser(description='MineOS command line execution scripts',
402+
parser = ArgumentParser(description='MineOS web user interface service',
403403
version=__version__)
404404
parser.add_argument('-p',
405405
dest='port',
@@ -445,7 +445,7 @@ def to_jsonable_type(retval):
445445
}
446446
}
447447

448-
if args.base_directory:
449-
mc._make_skeleton(args.base_directory)
448+
base_dir = args.base_directory or mc.valid_user('will')[1]
449+
mc._make_skeleton(base_dir)
450450

451-
cherrypy.quickstart(mc_server(args.base_directory), config=conf)
451+
cherrypy.quickstart(mc_server(base_dir), config=conf)

0 commit comments

Comments
 (0)