Package ganeti :: Package rapi :: Package auth :: Module pam
[hide private]
[frames] | no frames]

Source Code for Module ganeti.rapi.auth.pam

  1  # 
  2  # 
  3   
  4  # Copyright (C) 2015, 2016 Google Inc. 
  5  # All rights reserved. 
  6  # 
  7  # Redistribution and use in source and binary forms, with or without 
  8  # modification, are permitted provided that the following conditions are 
  9  # met: 
 10  # 
 11  # 1. Redistributions of source code must retain the above copyright notice, 
 12  # this list of conditions and the following disclaimer. 
 13  # 
 14  # 2. Redistributions in binary form must reproduce the above copyright 
 15  # notice, this list of conditions and the following disclaimer in the 
 16  # documentation and/or other materials provided with the distribution. 
 17  # 
 18  # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 
 19  # IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 
 20  # TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 
 21  # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR 
 22  # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 
 23  # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 
 24  # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
 25  # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 
 26  # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 
 27  # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 
 28  # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
 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 # pylint: disable=F0401 
 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: 
74 - class PamHandleT(c.Structure):
75 """Wrapper for PamHandleT 76 77 """ 78 _fields_ = [("hidden", c.c_void_p)] 79
80 - def __init__(self):
81 c.Structure.__init__(self) 82 self.handle = 0
83
84 - class PamMessage(c.Structure):
85 """Wrapper for PamMessage 86 87 """ 88 _fields_ = [ 89 ("msg_style", c.c_int), 90 ("msg", c.c_char_p), 91 ]
92
93 - class PamResponse(c.Structure):
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
105 - class PamConv(c.Structure):
106 """Wrapper for PamConv 107 108 """ 109 _fields_ = [ 110 ("conv", CONV_FUNC), 111 ("appdata_ptr", c.c_void_p), 112 ]
113 114
115 -class CFunctions(object):
116 - def __init__(self):
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
175 -def Authenticate(cf, pam_handle, authtok=None):
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
206 -def PutPamEnvVariable(cf, pam_handle, name, value):
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
258 -def ValidateRequest(cf, username, uri_access_rights, password=None, 259 service=DEFAULT_SERVICE_NAME, authtok=None, uri=None, 260 method=None, body=None):
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 # retrieve the authorized user name 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
332 -def MakeStringC(string):
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
351 -class PamAuthenticator(auth.RapiAuthenticator):
352 """Class providing an Authenticate method based on interaction with PAM. 353 354 """ 355
356 - def __init__(self):
357 """Checks whether ctypes has been imported. 358 359 """ 360 self.cf = CFunctions()
361
362 - def ValidateRequest(self, req, handler_access, _):
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