Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: add decorator to log audit #885

Merged
merged 20 commits into from
May 7, 2021
Merged

Conversation

jamesgetx
Copy link
Collaborator

变更点(Changes)
提供装饰器的方案,以支持操作审计日志记录

self.audit_context = audit_context

def log_succeed(self):
activity_status = 'succeed'
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit:感觉可以不定义这个变量,直接使用 'succeed'

Copy link
Collaborator

@narasux narasux Apr 27, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

有一个 ActivityStatus 是常量但是使用的dict,是否考虑下改成枚举类?也可以在这里使用

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

有一个 ActivityStatus 是常量但是使用的dict,是否考虑下改成枚举类?也可以在这里使用

ActivityStatus中有4中状态,感觉其中的succeed和failed两种状态就可以。考虑先保持常量,不复用ActivityStatus

if v:
setattr(self, f.name, v)

def update_fields(self, **kwargs):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: 这俩的区别是不是一个合并对象,一个合并字典?感觉名字改成 merge / merge_dict 似乎更精确一点。

@@ -0,0 +1,102 @@
# -*- coding: utf-8 -*-
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit:感觉复数的 decorators 更好一点。

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已调整


def log_audit_on_view(auditor_cls: Type[Auditor], activity_type: str):
"""
用于 view 的操作审计装饰器
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

建议更详细的描述下这装饰器的功能、使用场景。

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已做调整

return resp
finally:
# 生成默认的audit_ctx
extra = dict(**kwargs)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

下面这段逻辑,我感觉抽象出另一个函数似乎好一点,这样装饰器不会显得太复杂。

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

做了代码调整


auditor = auditor_cls(audit_ctx)
if err_msg:
auditor.log_failed(err_msg)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

直接记录所有异常信息的做法,总觉得不太妥当,很容易把敏感的错误信息暴露给用户。如果只是把一些特定异常转成字符串存储(比如 ValidatonError 之流),其他就不记录或者说未知错误,感觉更妥当。

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里的err_msg 捕获的是view中raise 出来的异常信息,可以在view中做这个约定

Copy link
Collaborator

@piglei piglei May 6, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

但是 view 也没法确定自己会抛出什么吧?异常的问题主要就是不可预知,假如网络出问题了,某个底层模块抛了一个没捕获的 RequestError,里面带了内网地址,不就是会被记录上吗?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

通过以下方式解决

 # 记录原始错误信息的异常列表
    err_msg_exceptions = (APIError, ValidationError)

audit_ctx.activity_type = activity_type

auditor = auditor_cls(audit_ctx)
if err_msg:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

看到这个判断重复出现后,我在想,是不是目前来看,只提供一个方法就够了?有 err_msg 就是失败,没有就是成功

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

重新做了优化

assert activity_log.activity_status == 'succeed'
assert activity_log.description == 'list templatesets succeed'

request_post = factory.post('/', data={'version': '1.6.0'})
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

考虑拆分为两个独立的测试方法。

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

# 生成默认的audit_ctx
extra = dict(**kwargs)
if hasattr(request, 'data'):
extra.update(request.data)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

几个问题:

  • requests.data可能不是json,用update可能异常(直接加一个request_data key?), data body可能比较大,应该要限制下长度
  • 资源类型,名称,ID,操作类型这些是如何写入的
  • 如果操作db异常,是否会影响到API返回

Copy link
Collaborator Author

@jamesgetx jamesgetx Apr 27, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

    1. 这里其实是提供了默认的extra的生成。我考虑优化下
    1. 资源类型用装饰器时可以写入。如果有名称和ID的需求,需要在view中设置,如
      request.audit_ctx.update_fields(resource_id=XX, resource='nginx')
    1. 如果操作db异常,考虑到db是关键服务。如果忽略,可能要通过日志的方式记录,或者发出告警。如果是这样,那主动报错,会不会更好些?

UserActivityLog.objects.create(**asdict(self.audit_context))


class TemplatesetsAuditor(Auditor):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里是审计类型吗? 如果只是一个名称,直接在装饰器使用choices的变量是否会更好

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

是每种资源类型,有一个初始化?这样除了resource_type, 其它应该一样

Copy link
Collaborator Author

@jamesgetx jamesgetx Apr 28, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

目前用于指定resource_type,也考虑子类可能有自己的逻辑。
如果不定义子类,现阶段可以直接传Auditor,再主动设置audit_ctx.resource_type

from .context import AuditContext


def log_audit_on_view(auditor_cls: Type[Auditor], activity_type: str):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

还有个场景需要支持下

  • slz.is_valid(raise_exception=True) 这个在很多view之前会检测,这类应该需要忽略,所以加个参数,忽略掉特定的异常(默认ValidateError应该要忽略的)?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

加上忽略异常的逻辑,会不会太复杂了。比如参数异常,但是用户确实点了资源下发,只是没有到集群,这种是否也可能要审计下来,一个是方便他回溯错误,另一个是他有操作行为。不过可以考虑下看

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

可以忽略掉某些异常

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

@@ -38,6 +39,11 @@ class UserActivityLog(models.Model):
description = models.TextField(help_text='描述', null=True, blank=True)
extra = models.TextField(help_text='扩展')
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extra 如果都是json格式的数据,是否考虑用JsonField,可以不用每次手动转换?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这次pr先不调整这个,因为涉及到其他地方有json.loads(extra)的逻辑

@@ -85,6 +87,8 @@ def has_permission(self, request, view):
if project:
request.project = project
self._set_ctx_project_cluster(request, project.project_id, view.kwargs.get('cluster_id', ''))
# 设置操作审计 context
request.audit_ctx = AuditContext(user=request.user.username, project_id=project.project_id)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

在装饰器处理这个逻辑

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

装饰器中增加了_pre_audit_ctx方法实现

activity_status = 'failed'
self._log(activity_status, err_msg)

def _gen_default_description(self, activity_status: str, err_msg):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

err_msg 添加类型

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

UserActivityLog.objects.create(**asdict(self.audit_context))


class TemplatesetsAuditor(Auditor):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

是每种资源类型,有一个初始化?这样除了resource_type, 其它应该一样

from .context import AuditContext


def log_audit_on_view(auditor_cls: Type[Auditor], activity_type: str):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

可以忽略掉某些异常

if isinstance(request.data, dict):
extra.update(request.data)
elif isinstance(request.data, str):
extra['body'] = request.data
Copy link
Collaborator

@narasux narasux Apr 29, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

如果是特殊字段用 __body__ 会不会好些,否则比如 request.data = {'body': 'xxxxx'} ,request.data = 'xxxxx' 结果是一样的

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

你的意思是,如果请求数据本身就有body key,会与这里的混淆对吧。body=>request_body

auditor_cls: Type[Auditor] = type(Auditor),
activity_type: str = '',
auto_audit: bool = True,
ignore_exception_classes: Optional[List[Type[Exception]]] = None,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

直接ignore_exception_classes -> ignore_exceptions 感觉还顺畅点

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok,这里有类型注解,可以简写下

return ret
except Exception as e:
# 如果是 ignore_exception_classes 中的异常,不做审计记录
if self.ignore_exception_classes and type(e) in self.ignore_exception_classes:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这个会有BUG,exception是可以继承的,可能要抽象一个函数,用for循环后,使用isinstance(e, exc)这样判断下

def is_ignore_exception(self, e) -> bool:
    if not self.ignore_exceptions:
        return False
    for exc in self.ignore_exceptions:
        if isinstance(e, exc):
            return True
    return False

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里的逻辑其实是精确忽略,不考虑一并忽略子类的情况

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

如果考虑指定父类后一并忽略子类,可能会这样写

    def _ignore_exception(self, e: Exception) -> bool:
        if not self.ignore_exceptions:
            return False

        try:
            raise e
        except self.ignore_exceptions:
            return True
        except:
            return False

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

直接调整为

            try:
                ret = func(*args, **kwargs)
                return ret
            except self.ignore_exceptions:
                # 如果是 ignore_exceptions 中的异常,不做审计记录
                self.auto_audit = False
                raise
            except Exception as e:
                err_msg = str(e)
                raise
            finally:
                if self.auto_audit:
                    ...

@@ -0,0 +1,58 @@
# -*- coding: utf-8 -*-
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

要不我们直接按照新模块结构,把这个放在 bcs_web 里去?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

backend.bcs_web.activity_log.audit

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

本pr先不调整,等到模块重组时调整

self.auditor_cls = auditor_cls
self.activity_type = activity_type
self.auto_audit = auto_audit
self.ignore_exception_classes = ignore_exception_classes
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit:参数名可以短点,ignored_exceptions 感觉就够了

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已调整

- auditor_cls: 执行审计记录的类, 默认为 Auditor
- activity_type: 操作类型,默认为''。可在 audit_ctx 中覆盖
- auto_audit: 是否记录审计,默认为记录
- ignore_exception_classes: 忽略审计的异常类列表
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

建议使用 Sphinx 格式的参数说明

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已调整

activity_log = UserActivityLog.objects.get(project_id=project_id, user=bk_user.username, resource_type='helm')
assert activity_log.activity_type == 'install'
assert activity_log.description == 'test install helm'
assert json.loads(activity_log.extra)['chart'] == 'http://example.chart.com/nginx/nginx1.12.tgz'
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

我认为可以直接调整一部分现有审计代码,用来测试新工具的能力,直接把这部分改动和工具一起提交。

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

本pr先不调整,等到模块重组时调整

self.audit_context = audit_context

def log_succeed(self):
self._log('succeed')
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

成功时,是否也允许记录一些额外的description信息;
比如,页面展示的时候,可以直接展示出来,比如操作了增加操作的集群或者其它信息

Copy link
Collaborator Author

@jamesgetx jamesgetx May 6, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

    def _log(self, activity_status: str, err_msg: str = ''):
        if not self.audit_context.description:
            self.audit_context.description = self._gen_default_description(activity_status, err_msg)
        self.audit_context.activity_status = activity_status
        UserActivityLog.objects.create(**asdict(self.audit_context))

可以主动设置description,这样会直接使用。另外,集群和其他信息,是不是在resource, 和resource_id中设置会更合适

class TemplatesetsAuditor(Auditor):
def __init__(self, audit_context: AuditContext):
super().__init__(audit_context)
self.audit_context.resource_type = 'templatesets'
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: 这里templatesets是不是单数即可

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

@dellkeji dellkeji merged commit 3711c21 into Tencent:master May 7, 2021
@jamesgetx jamesgetx linked an issue May 17, 2021 that may be closed by this pull request
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

重构操作审计模块
5 participants