distcloud/distributedcloud/dcmanager/common/context.py
Karla Felix b056a514c8 Ignore JSON decode error in request log.
For DELETE requests with no request body or "Content-Length" field, the
expected return value for "hasattr(rest_state.request, 'json')" would be
False and for "rest_state.request.json" would be an empty string.
Instead, what occurs is that both "hasattr(rest_state.request, 'json')"
and "rest_state.request.json" are throwing JSON decode exceptions in
this case. This change adds tolerance for this exception. This is done
because it is not known all the possible request conditions that may
cause this exception to be thrown.

Test Plan:

PASS:  Send a delete request through the CLI to subcloud-group and
       verify if any exception shows up dcmanager-api logs.

Closes-Bug: 1999901
Change-Id: I58af3f4711d6caf840e188fb4690f2bde219d79a
Signed-off-by: Karla Felix <karla.karolinenogueirafelix@windriver.com>
2022-12-19 14:07:41 +00:00

265 lines
9.3 KiB
Python

# Copyright (c) 2017-2022 Wind River Systems, Inc.
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
import pecan
from pecan import hooks
import re
from six.moves.urllib.parse import urlparse
import time
from oslo_context import context as base_context
from oslo_log import log
from oslo_utils import encodeutils
from oslo_utils import uuidutils
from dcmanager.api.policies import base as base_policy
from dcmanager.api import policy
from dcmanager.db import api as db_api
ALLOWED_WITHOUT_AUTH = '/'
audit_log_name = "{}.{}".format(__name__, "auditor")
auditLOG = log.getLogger(audit_log_name)
def generate_request_id():
return 'req-%s' % uuidutils.generate_uuid()
class RequestContext(base_context.RequestContext):
"""Stores information about the security context.
The context encapsulates information related to the user accessing the
the system, as well as additional request information.
"""
def __init__(self, auth_token=None, user=None, project=None,
domain=None, user_domain=None, project_domain=None,
is_admin=None, read_only=False, show_deleted=False,
request_id=None, auth_url=None, trusts=None,
user_name=None, project_name=None, domain_name=None,
user_domain_name=None, project_domain_name=None,
auth_token_info=None, region_name=None, roles=None,
password=None, **kwargs):
"""Initializer of request context."""
# We still have 'tenant' param because oslo_context still use it.
# pylint: disable=E1123
super(RequestContext, self).__init__(
auth_token=auth_token, user=user, tenant=project,
domain=domain, user_domain=user_domain,
project_domain=project_domain, roles=roles,
read_only=read_only, show_deleted=show_deleted,
request_id=request_id)
# request_id might be a byte array
self.request_id = encodeutils.safe_decode(self.request_id)
# we save an additional 'project' internally for use
self.project = project
# Session for DB access
self._session = None
self.auth_url = auth_url
self.trusts = trusts
self.user_name = user_name
self.project_name = project_name
self.domain_name = domain_name
self.user_domain_name = user_domain_name
self.project_domain_name = project_domain_name
self.auth_token_info = auth_token_info
self.region_name = region_name
self.roles = roles or []
self.password = password
# Check user is admin or not
if is_admin is None:
self.is_admin = policy.authorize(
base_policy.ADMIN_IN_SYSTEM_PROJECTS, {}, self.to_dict(),
do_raise=False)
else:
self.is_admin = is_admin
@property
def session(self):
if self._session is None:
self._session = db_api.get_session()
return self._session
def to_dict(self):
return {
'auth_url': self.auth_url,
'auth_token': self.auth_token,
'auth_token_info': self.auth_token_info,
'user': self.user,
'user_name': self.user_name,
'user_domain': self.user_domain,
'user_domain_name': self.user_domain_name,
'project': self.project,
'project_name': self.project_name,
'project_domain': self.project_domain,
'project_domain_name': self.project_domain_name,
'domain': self.domain,
'domain_name': self.domain_name,
'trusts': self.trusts,
'region_name': self.region_name,
'roles': self.roles,
'show_deleted': self.show_deleted,
'is_admin': self.is_admin,
'request_id': self.request_id,
'password': self.password,
}
@classmethod
def from_dict(cls, values):
return cls(**values)
def get_admin_context(show_deleted=False):
return RequestContext(is_admin=True, show_deleted=show_deleted)
def get_service_context(**args):
"""An abstraction layer for getting service context.
There could be multiple cloud backends for dcmanager to use. This
abstraction layer provides an indirection for dcmanager to get the
credentials of 'dcmanager' user on the specific cloud. By default,
this credential refers to the credentials built for dcmanager middleware
in an OpenStack cloud.
"""
pass
class AuthHook(hooks.PecanHook):
def before(self, state):
if state.request.path == ALLOWED_WITHOUT_AUTH:
return
req = state.request
identity_status = req.headers.get('X-Identity-Status')
service_identity_status = req.headers.get('X-Service-Identity-Status')
if (identity_status == 'Confirmed' or
service_identity_status == 'Confirmed'):
return
if req.headers.get('X-Auth-Token'):
msg = 'Auth token is invalid: %s' % req.headers['X-Auth-Token']
else:
msg = 'Authentication required'
msg = "Failed to validate access token: %s" % str(msg)
pecan.abort(status_code=401, detail=msg)
class AuditLoggingHook(hooks.PecanHook):
"""Request data logging.
Performs audit logging of all Distributed Cloud Manager
["POST", "PUT", "PATCH", "DELETE"] REST requests.
"""
def __init__(self):
self.log_methods = ["POST", "PUT", "PATCH", "DELETE"]
def before(self, state):
state.request.start_time = time.time()
def __after(self, state):
method = state.request.method
if method not in self.log_methods:
return
now = time.time()
try:
elapsed = now - state.request.start_time
except AttributeError:
auditLOG.info("Start time is not in request, setting it to 0.")
elapsed = 0
environ = state.request.environ
server_protocol = environ["SERVER_PROTOCOL"]
response_content_length = state.response.content_length
user_id = state.request.headers.get('X-User-Id')
user_name = state.request.headers.get('X-User', user_id)
tenant_id = state.request.headers.get('X-Tenant-Id')
tenant = state.request.headers.get('X-Tenant', tenant_id)
domain_name = state.request.headers.get('X-User-Domain-Name')
try:
request_id = state.request.context.request_id
except AttributeError:
auditLOG.info("Request id is not in request, setting it to an "
"auto generated id.")
request_id = generate_request_id()
url_path = urlparse(state.request.path_qs).path
def json_post_data(rest_state):
if 'form-data' in rest_state.request.headers.get('Content-Type'):
return " POST: {}".format(rest_state.request.params)
try:
if not hasattr(rest_state.request, 'json'):
return ""
return " POST: {}".format(rest_state.request.json)
except Exception:
return ""
# Filter password from log
filtered_json = re.sub(r'{[^{}]*(passwd_hash|community|password)[^{}]*},*',
'',
json_post_data(state))
log_data = \
"{} \"{} {} {}\" status: {} len: {} time: {}{} host:{}" \
" agent:{} user: {} tenant: {} domain: {}".format(
state.request.remote_addr,
state.request.method,
url_path,
server_protocol,
state.response.status_int,
response_content_length,
elapsed,
filtered_json,
state.request.host,
state.request.user_agent,
user_name,
tenant,
domain_name)
# The following ctx object will be output in the logger as
# something like this:
# [req-088ed3b6-a2c9-483e-b2ad-f1b2d03e06e6
# 3d76d3c1376744e8ad9916a6c3be3e5f
# ca53e70c76d847fd860693f8eb301546]
# When the ctx is defined, the formatter (defined in common/log.py) requires that keys
# request_id, user, tenant be defined within the ctx
ctx = {'request_id': request_id,
'user': user_id,
'tenant': tenant_id}
auditLOG.info("{}".format(log_data), context=ctx)
def after(self, state):
try:
self.__after(state)
except Exception:
# Logging and then swallowing exception to ensure
# rest service does not fail even if audit logging fails
auditLOG.exception("Exception in AuditLoggingHook on event 'after'")
def on_error(self, state, e):
auditLOG.exception("Exception in AuditLoggingHook passed to event 'on_error': " + str(e))