The entry will have the following format: ("gnt:library:rlib2", "RAPI-Auth:user", *time*).
This entry in the reason trail might be useful in order to detect malicious rapi users or restrict access for some operations by filters. Note, that in the last case it's necessary to restrict access to filters modification first. Signed-off-by: Oleg Ponomarev <oponoma...@google.com> --- lib/rapi/auth/__init__.py | 5 +++-- lib/rapi/auth/basic_auth.py | 4 ++-- lib/rapi/auth/pam.py | 30 +++++++++++++++++++++++------- lib/rapi/baserlib.py | 12 ++++++++++++ lib/rapi/rlib2.py | 4 ++++ lib/server/rapi.py | 9 ++++++++- src/Ganeti/Constants.hs | 4 ++++ 7 files changed, 56 insertions(+), 12 deletions(-) diff --git a/lib/rapi/auth/__init__.py b/lib/rapi/auth/__init__.py index bb43f2e..a146436 100644 --- a/lib/rapi/auth/__init__.py +++ b/lib/rapi/auth/__init__.py @@ -45,8 +45,9 @@ class RapiAuthenticator(object): @param req: HTTP request context @type handler_access: set of strings @param handler_access: access rights required by the requested resourse - @rtype: bool - @return: Whether request execution is permitted + @rtype: str + @return: the authenticated user name if request execution is permitted and + None otherwise """ raise NotImplementedError() diff --git a/lib/rapi/auth/basic_auth.py b/lib/rapi/auth/basic_auth.py index 66400b8..f6bb0e3 100644 --- a/lib/rapi/auth/basic_auth.py +++ b/lib/rapi/auth/basic_auth.py @@ -137,12 +137,12 @@ class BasicAuthenticator(auth.RapiAuthenticator): .ExtractSchemePassword(password) if not (user and expected_password == user.password): # Unknown user or password wrong - return False + return None if (not handler_access or set(user.options).intersection(handler_access)): # Allow access - return True + return username # Access forbidden raise http.HttpForbidden() diff --git a/lib/rapi/auth/pam.py b/lib/rapi/auth/pam.py index 84f6acf..1de78d9 100644 --- a/lib/rapi/auth/pam.py +++ b/lib/rapi/auth/pam.py @@ -69,6 +69,7 @@ PAM_SUCCESS = 0 PAM_PROMPT_ECHO_OFF = 1 PAM_AUTHTOK = 6 +PAM_USER = 2 class PamHandleT(c.Structure): @@ -128,6 +129,10 @@ PAM_END = LIBPAM.pam_end PAM_END.argtypes = [PamHandleT, c.c_int] PAM_END.restype = c.c_int +PAM_GET_ITEM = LIBPAM.pam_get_item +PAM_GET_ITEM.argtypes = [PamHandleT, c.c_int, c.POINTER(c.c_void_p)] +PAM_GET_ITEM.restype = c.c_int + PAM_PUTENV = LIBPAM.pam_putenv PAM_PUTENV.argtypes = [PamHandleT, c.c_char_p] PAM_PUTENV.restype = c.c_int @@ -257,6 +262,7 @@ def ValidateRequest(username, uri_access_rights, password=None, @param uri: an uri of a target resource obtained from an http header @param method: http method trying to access the uri @param body: a body of an RAPI request + @return: On success - authenticated user name. Throws an exception otherwise. """ ValidateParams(username, uri_access_rights, password, service, authtok, uri, @@ -300,7 +306,16 @@ def ValidateRequest(username, uri_access_rights, password=None, Authenticate(pam_handle, authtok) Authorize(pam_handle, uri_access_rights, uri, method, body) + # retrieve the authorized user name + puser = c.c_void_p() + ret = PAM_GET_ITEM(pam_handle, PAM_USER, c.pointer(puser)) + if ret != PAM_SUCCESS or not puser: + PAM_END(pam_handle, ret) + raise HttpInternalServerError("pam_get_item call failed [%d]" % ret) + user_c_string = c.cast(puser, c.c_char_p) + PAM_END(pam_handle, PAM_SUCCESS) + return user_c_string.value def MakeStringC(string): @@ -336,16 +351,17 @@ class PamAuthenticator(auth.RapiAuthenticator): def ValidateRequest(self, req, handler_access): """Checks whether a user can access a resource. + This function retuns authenticated user name on success. + """ username, password = HttpServerRequestAuthentication \ .ExtractUserPassword(req) authtok = req.request_headers.get(constants.HTTP_RAPI_PAM_CREDENTIAL, None) if handler_access is not None: handler_access_ = ','.join(handler_access) - ValidateRequest(MakeStringC(username), MakeStringC(handler_access_), - MakeStringC(password), - MakeStringC(DEFAULT_SERVICE_NAME), - MakeStringC(authtok), MakeStringC(req.request_path), - MakeStringC(req.request_method), - MakeStringC(req.request_body)) - return True + return ValidateRequest(MakeStringC(username), MakeStringC(handler_access_), + MakeStringC(password), + MakeStringC(DEFAULT_SERVICE_NAME), + MakeStringC(authtok), MakeStringC(req.request_path), + MakeStringC(req.request_method), + MakeStringC(req.request_body)) diff --git a/lib/rapi/baserlib.py b/lib/rapi/baserlib.py index 6a4014a..25e4781 100644 --- a/lib/rapi/baserlib.py +++ b/lib/rapi/baserlib.py @@ -323,6 +323,8 @@ class ResourceBase(object): self._client_cls = _client_cls + self.auth_user = "" + def _GetRequestBody(self): """Returns the body data. @@ -411,6 +413,11 @@ class ResourceBase(object): raise http.HttpInternalServerError("Internal error: no permission to" " connect to the master daemon") + def GetAuthReason(self): + return (constants.OPCODE_REASON_SRC_RLIB2, + constants.OPCODE_REASON_AUTH_USER + self.auth_user, + utils.EpochNano()) + def SubmitJob(self, op, cl=None): """Generic wrapper for submit job, for better http compatibility. @@ -425,6 +432,11 @@ class ResourceBase(object): if cl is None: cl = self.GetClient() try: + for opcode in op: + # Add an authorized user name to the reason trail + trail = getattr(opcode, constants.OPCODE_REASON, []) + trail.append(self.GetAuthReason()) + setattr(opcode, constants.OPCODE_REASON, trail) return cl.SubmitJob(op) except errors.JobQueueFull: raise http.HttpServiceUnavailable("Job queue is full, needs archiving") diff --git a/lib/rapi/rlib2.py b/lib/rapi/rlib2.py index 70c6a5d..a6d8b42 100644 --- a/lib/rapi/rlib2.py +++ b/lib/rapi/rlib2.py @@ -372,6 +372,8 @@ class R_2_filters(baserlib.ResourceBase): priority, predicates, action, reason = \ checkFilterParameters(self.request_body) + reason.append(self.GetAuthReason()) + # ReplaceFilter(None, ...) inserts a new filter. return self.GetClient().ReplaceFilter(None, priority, predicates, action, reason) @@ -417,6 +419,8 @@ class R_2_filters_uuid(baserlib.ResourceBase): priority, predicates, action, reason = \ checkFilterParameters(self.request_body) + reason.append(self.GetAuthReason()) + return self.GetClient().ReplaceFilter(uuid, priority, predicates, action, reason) diff --git a/lib/server/rapi.py b/lib/server/rapi.py index 24b939f..45628f6 100644 --- a/lib/server/rapi.py +++ b/lib/server/rapi.py @@ -87,6 +87,7 @@ class RemoteApiHandler(http.auth.HttpServerRequestAuthentication, # it seems pylint doesn't see the second parent class there http.server.HttpServerHandler.__init__(self) http.auth.HttpServerRequestAuthentication.__init__(self) + self._client_cls = _client_cls self._resmap = connector.Mapper() self._authenticator = authenticator @@ -151,9 +152,15 @@ class RemoteApiHandler(http.auth.HttpServerRequestAuthentication, def Authenticate(self, req): """Checks whether a user can access a resource. + @return: username of an authenticated user or None otherwise """ ctx = self._GetRequestContext(req) - return self._authenticator.ValidateRequest(req, ctx.handler_access) + auth_user = self._authenticator.ValidateRequest(req, ctx.handler_access) + if auth_user is None: + return False + + ctx.handler.auth_user = auth_user + return True def HandleRequest(self, req): """Handles a request. diff --git a/src/Ganeti/Constants.hs b/src/Ganeti/Constants.hs index 0220e66..0417e4e 100644 --- a/src/Ganeti/Constants.hs +++ b/src/Ganeti/Constants.hs @@ -4900,6 +4900,10 @@ opcodeReasonSources = opcodeReasonSrcRlib2, opcodeReasonSrcUser] +-- | A reason content prefix for RAPI auth user +opcodeReasonAuthUser :: String +opcodeReasonAuthUser = "RAPI-Auth:" + -- | Path generating random UUID randomUuidFile :: String randomUuidFile = ConstantUtils.randomUuidFile -- 2.6.0.rc2.230.g3dd15c0