1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31 """Module interacting with PAM performing authorization and authentication
32
33 This module authenticates and authorizes RAPI users based on their credintials.
34 Both actions are performed by interaction with PAM as a 'ganeti-rapi' service.
35
36 """
37
38 import logging
39 try:
40 import ctypes as c
41 import ctypes.util as util
42 except ImportError:
43 c = None
44
45 from ganeti import constants
46 from ganeti.errors import PamRapiAuthError
47 import ganeti.http as http
48 from ganeti.http.auth import HttpServerRequestAuthentication
49 from ganeti.rapi import auth
50
51
52 __all__ = ['PamAuthenticator']
53
54 DEFAULT_SERVICE_NAME = 'ganeti-rapi'
55 MAX_STR_LENGTH = 100000
56 MAX_MSG_COUNT = 100
57 PAM_ENV_URI = 'GANETI_RAPI_URI'
58 PAM_ENV_BODY = 'GANETI_REQUEST_BODY'
59 PAM_ENV_METHOD = 'GANETI_REQUEST_METHOD'
60 PAM_ENV_ACCESS = 'GANETI_RESOURCE_ACCESS'
61
62 PAM_ABORT = 26
63 PAM_BUF_ERR = 5
64 PAM_CONV_ERR = 19
65 PAM_SILENT = 32768
66 PAM_SUCCESS = 0
67
68 PAM_PROMPT_ECHO_OFF = 1
69
70 PAM_AUTHTOK = 6
71 PAM_USER = 2
72
73 if c:
75 """Wrapper for PamHandleT
76
77 """
78 _fields_ = [("hidden", c.c_void_p)]
79
81 c.Structure.__init__(self)
82 self.handle = 0
83
85 """Wrapper for PamMessage
86
87 """
88 _fields_ = [
89 ("msg_style", c.c_int),
90 ("msg", c.c_char_p),
91 ]
92
94 """Wrapper for PamResponse
95
96 """
97 _fields_ = [
98 ("resp", c.c_char_p),
99 ("resp_retcode", c.c_int),
100 ]
101
102 CONV_FUNC = c.CFUNCTYPE(c.c_int, c.c_int, c.POINTER(c.POINTER(PamMessage)),
103 c.POINTER(c.POINTER(PamResponse)), c.c_void_p)
104
106 """Wrapper for PamConv
107
108 """
109 _fields_ = [
110 ("conv", CONV_FUNC),
111 ("appdata_ptr", c.c_void_p),
112 ]
113
114
117 if not c:
118 raise PamRapiAuthError("ctypes Python package is not found;"
119 " remote API PAM authentication is not available")
120 self.libpam = c.CDLL(util.find_library("pam"))
121 if not self.libpam:
122 raise PamRapiAuthError("libpam C library is not found;"
123 " remote API PAM authentication is not available")
124 self.libc = c.CDLL(util.find_library("c"))
125 if not self.libc:
126 raise PamRapiAuthError("libc C library is not found;"
127 " remote API PAM authentication is not available")
128
129 self.pam_acct_mgmt = self.libpam.pam_acct_mgmt
130 self.pam_acct_mgmt.argtypes = [PamHandleT, c.c_int]
131 self.pam_acct_mgmt.restype = c.c_int
132
133 self.pam_authenticate = self.libpam.pam_authenticate
134 self.pam_authenticate.argtypes = [PamHandleT, c.c_int]
135 self.pam_authenticate.restype = c.c_int
136
137 self.pam_end = self.libpam.pam_end
138 self.pam_end.argtypes = [PamHandleT, c.c_int]
139 self.pam_end.restype = c.c_int
140
141 self.pam_get_item = self.libpam.pam_get_item
142 self.pam_get_item.argtypes = [PamHandleT, c.c_int, c.POINTER(c.c_void_p)]
143 self.pam_get_item.restype = c.c_int
144
145 self.pam_putenv = self.libpam.pam_putenv
146 self.pam_putenv.argtypes = [PamHandleT, c.c_char_p]
147 self.pam_putenv.restype = c.c_int
148
149 self.pam_set_item = self.libpam.pam_set_item
150 self.pam_set_item.argtypes = [PamHandleT, c.c_int, c.c_void_p]
151 self.pam_set_item.restype = c.c_int
152
153 self.pam_start = self.libpam.pam_start
154 self.pam_start.argtypes = [
155 c.c_char_p,
156 c.c_char_p,
157 c.POINTER(PamConv),
158 c.POINTER(PamHandleT),
159 ]
160 self.pam_start.restype = c.c_int
161
162 self.calloc = self.libc.calloc
163 self.calloc.argtypes = [c.c_uint, c.c_uint]
164 self.calloc.restype = c.c_void_p
165
166 self.free = self.libc.free
167 self.free.argstypes = [c.c_void_p]
168 self.free.restype = None
169
170 self.strndup = self.libc.strndup
171 self.strndup.argstypes = [c.c_char_p, c.c_uint]
172 self.strndup.restype = c.c_char_p
173
174
176 """Performs authentication via PAM.
177
178 Perfroms two steps:
179 - if authtok is provided then set it with pam_set_item
180 - call pam_authenticate
181
182 """
183 try:
184 authtok_copy = None
185 if authtok:
186 authtok_copy = cf.strndup(authtok, len(authtok))
187 if not authtok_copy:
188 raise http.HttpInternalServerError("Not enough memory for PAM")
189 ret = cf.pam_set_item(c.pointer(pam_handle), PAM_AUTHTOK, authtok_copy)
190 if ret != PAM_SUCCESS:
191 raise http.HttpInternalServerError("pam_set_item failed [%d]" % ret)
192
193 ret = cf.pam_authenticate(pam_handle, 0)
194 if ret == PAM_ABORT:
195 raise http.HttpInternalServerError("pam_authenticate requested abort")
196 if ret != PAM_SUCCESS:
197 raise http.HttpUnauthorized("Authentication failed")
198 except:
199 cf.pam_end(pam_handle, ret)
200 raise
201 finally:
202 if authtok_copy:
203 cf.free(authtok_copy)
204
205
207 """Wrapper over pam_setenv.
208
209 """
210 setenv = "%s=" % name
211 if value:
212 setenv += value
213 ret = cf.pam_putenv(pam_handle, setenv)
214 if ret != PAM_SUCCESS:
215 raise http.HttpInternalServerError("pam_putenv call failed [%d]" % ret)
216
217
218 -def Authorize(cf, pam_handle, uri_access_rights, uri=None, method=None,
219 body=None):
220 """Performs authorization via PAM.
221
222 Performs two steps:
223 - initialize environmental variables
224 - call pam_acct_mgmt
225
226 """
227 try:
228 PutPamEnvVariable(cf, pam_handle, PAM_ENV_ACCESS, uri_access_rights)
229 PutPamEnvVariable(cf, pam_handle, PAM_ENV_URI, uri)
230 PutPamEnvVariable(cf, pam_handle, PAM_ENV_METHOD, method)
231 PutPamEnvVariable(cf, pam_handle, PAM_ENV_BODY, body)
232
233 ret = cf.pam_acct_mgmt(pam_handle, PAM_SILENT)
234 if ret != PAM_SUCCESS:
235 raise http.HttpUnauthorized("Authorization failed")
236 except:
237 cf.pam_end(pam_handle, ret)
238 raise
239
240
241 -def ValidateParams(username, _uri_access_rights, password, service, authtok,
242 _uri, _method, _body):
243 """Checks whether ValidateRequest has been called with a correct params.
244
245 These checks includes:
246 - username is an obligatory parameter
247 - either password or authtok is an obligatory parameter
248
249 """
250 if not username:
251 raise http.HttpUnauthorized("Username should be provided")
252 if not service:
253 raise http.HttpBadRequest("Service should be proivded")
254 if not password and not authtok:
255 raise http.HttpUnauthorized("Password or authtok should be provided")
256
257
261 """Checks whether it's permitted to execute an rapi request.
262
263 Calls pam_authenticate and then pam_acct_mgmt in order to check whether a
264 request should be executed.
265
266 @param cf: An instance of CFunctions class containing necessary imports
267 @param username: username
268 @param uri_access_rights: handler access rights
269 @param password: password
270 @param service: a service name that will be used for the interaction with PAM
271 @param authtok: user's authentication token (e.g. some kind of signature)
272 @param uri: an uri of a target resource obtained from an http header
273 @param method: http method trying to access the uri
274 @param body: a body of an RAPI request
275 @return: On success - authenticated user name. Throws an exception otherwise.
276
277 """
278 ValidateParams(username, uri_access_rights, password, service, authtok, uri,
279 method, body)
280
281 def ConversationFunction(num_msg, msg, resp, _app_data_ptr):
282 """Conversation function that will be provided to PAM modules.
283
284 The function replies with a password for each message with
285 PAM_PROMPT_ECHO_OFF style and just ignores the others.
286
287 """
288 if num_msg > MAX_MSG_COUNT:
289 logging.warning("Too many messages passed to conv function: [%d]",
290 num_msg)
291 return PAM_BUF_ERR
292 response = cf.calloc(num_msg, c.sizeof(PamResponse))
293 if not response:
294 logging.warning("calloc failed in conv function")
295 return PAM_BUF_ERR
296 resp[0] = c.cast(response, c.POINTER(PamResponse))
297 for i in range(num_msg):
298 if msg[i].contents.msg_style != PAM_PROMPT_ECHO_OFF:
299 continue
300 resp.contents[i].resp = cf.strndup(password, len(password))
301 if not resp.contents[i].resp:
302 logging.warning("strndup failed in conv function")
303 for j in range(i):
304 cf.free(c.cast(resp.contents[j].resp, c.c_void_p))
305 cf.free(response)
306 return PAM_BUF_ERR
307 resp.contents[i].resp_retcode = 0
308 return PAM_SUCCESS
309
310 pam_handle = PamHandleT()
311 conv = PamConv(CONV_FUNC(ConversationFunction), 0)
312 ret = cf.pam_start(service, username, c.pointer(conv), c.pointer(pam_handle))
313 if ret != PAM_SUCCESS:
314 cf.pam_end(pam_handle, ret)
315 raise http.HttpInternalServerError("pam_start call failed [%d]" % ret)
316
317 Authenticate(cf, pam_handle, authtok)
318 Authorize(cf, pam_handle, uri_access_rights, uri, method, body)
319
320
321 puser = c.c_void_p()
322 ret = cf.pam_get_item(pam_handle, PAM_USER, c.pointer(puser))
323 if ret != PAM_SUCCESS or not puser:
324 cf.pam_end(pam_handle, ret)
325 raise http.HttpInternalServerError("pam_get_item call failed [%d]" % ret)
326 user_c_string = c.cast(puser, c.c_char_p)
327
328 cf.pam_end(pam_handle, PAM_SUCCESS)
329 return user_c_string.value
330
331
333 """Converts a string to a valid C string.
334
335 As a C side treats non-unicode strings, encode unicode string with 'ascii'.
336 Also ensure that C string will not be longer than MAX_STR_LENGTH in order to
337 prevent attacs based on too long buffers.
338
339 """
340 if string is None:
341 return None
342 if isinstance(string, unicode):
343 string = string.encode("ascii")
344 if not isinstance(string, str):
345 return None
346 if len(string) <= MAX_STR_LENGTH:
347 return string
348 return string[:MAX_STR_LENGTH]
349
350
352 """Class providing an Authenticate method based on interaction with PAM.
353
354 """
355
357 """Checks whether ctypes has been imported.
358
359 """
360 self.cf = CFunctions()
361
363 """Checks whether a user can access a resource.
364
365 This function retuns authenticated user name on success.
366
367 """
368 username, password = HttpServerRequestAuthentication \
369 .ExtractUserPassword(req)
370 authtok = req.request_headers.get(constants.HTTP_RAPI_PAM_CREDENTIAL, None)
371 if handler_access is not None:
372 handler_access_ = ','.join(handler_access)
373 return ValidateRequest(self.cf, MakeStringC(username),
374 MakeStringC(handler_access_),
375 MakeStringC(password),
376 MakeStringC(DEFAULT_SERVICE_NAME),
377 MakeStringC(authtok), MakeStringC(req.request_path),
378 MakeStringC(req.request_method),
379 MakeStringC(req.request_body))
380