diff --git a/docs/usage.md b/docs/usage.md index ac650a0..5978803 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -543,6 +543,37 @@ Resource API Framework 中有两个核心概念: - `page.limit(int)`:查询数量 - `page.offset(int)`:查询偏移 - 返回值:返回 `iam.resource.provider.ListResult` 的实例,其中 `results` 应满足 IAM search_instance 响应协议 +7. `fetch_instance_list(filter, page, **options)`:处理来自 IAM 的 fetch_instance_list 请求,在审计中心生成静态资源快照时,需要实现此方法 + - 参数: + - `filter`:过滤器对象 + - `filter.start_time(int)`:资源实例变更时间的开始时间(包含start_time) + - `filter.end_time(int)`:资源实例变更时间的结束时间(包含end_time) + - `page`:分页对象 + - `page.limit(int)`:查询数量 + - `page.offset(int)`:查询偏移 + - 返回值:返回 `iam.resource.provider.ListResult` 的实例,其中 `results` 应满足 IAM fetch_instance_list 响应协议 +8. `fetch_resource_type_schema(**options)`:处理来自 IAM 的 fetch_resource_type_schema 请求,在审计中心显示静态资源时,需要实现此方法 + - 返回值:返回 `iam.resource.provider.SchemaResult` 的实例,输出结果可以通过 [JSON Schema Validator](https://www.jsonschemavalidator.net/) 校验 + - `注意`:为满足`审计中心`需求,字段描述新增`description_en`、`code` 两个 Key,`description_en`指字段英文描述,`code(可选)`指该字段为`代码`内容,在`审计中心`将按代码格式显示 + - DEMO(Response.data) + ``` + { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "ID", + "description_en": "ID", + }, + "script": { + "type": "string", + "description": "脚本", + "description_en": "Script", + "code": "shell" + } + } + } + ``` 除此之外,如果 Provider 中定义了 `pre_{method}` 方法(`method` 可选值(`list_attr`, `list_attr_value`, `list_instance`, `fetch_instance_info`, `list_instance_by_policy`),Dispatcher 会在调用对应的 `{method}` 方法前调用其对应的 `pre` 方法进行预处理,下面的例子检测 `list_instance` 中传入的 page 对象,如果 limit 过大,则拒绝该请求: @@ -557,7 +588,7 @@ class TaskResourceProvider(ResourceProvider): 下面是一种资源类型的 Provider 定义示例: ```python -from iam.resource.provider import ResourceProvider, ListResult +from iam.resource.provider import ResourceProvider, ListResult, SchemaResult from task.models import Tasks @@ -614,6 +645,18 @@ class TaskResourceProvider(ResourceProvider): 注意, 有翻页; 需要返回count """ return ListResult(results=[], count=0) + + def fetch_instance_list(self, filter, page, **options): + """根据过滤条件搜索实例 + 注意, 有翻页; 需要返回count + """ + return ListResult(results=[], count=0) + + def fetch_resource_type_schema(self, **options): + """获取资源类型 schema 定义 + schema定义 + """ + return SchemaResult(properties={}) ``` ### 3.2 Dispatcher @@ -648,7 +691,7 @@ class TaskResourceProvider(ResourceProvider): from blueapps.account.decorators import login_exempt from iam import IAM from iam.contrib.django.dispatcher import DjangoBasicResourceApiDispatcher -from iam.resource.provider import ResourceProvider, ListResult +from iam.resource.provider import ResourceProvider, ListResult, SchemaResult from iam.contrib.converter.queryset import PathEqDjangoQuerySetConverter from django.conf.urls import url, include @@ -763,6 +806,42 @@ class FlowResourceProvider(ResourceProvider): # TODO return ListResult(results=[], count=0) + def fetch_instance_list(self, filter, page, **options): + """ + flow + """ + queryset = TaskTemplate.objects.filter(updated_at__gte=filter.start_time).filter(updated_at__lte=filter.end_time) + results = [] + for flow in queryset[page.slice_from : page.slice_to]: + results.append( + { + "id": str(flow.id), + "display_name": flow.name, + "creator": flow.creator, + "created_at": flow.created_at, + "updater": flow.updater, + "updated_at": flow.updated_at, + "data": flow.to_json() + } + ) + return ListResult(results=results, count=queryset.count()) + + def fetch_resource_type_schema(self, **options): + properties = { + "id": { + "type": "string", + "description": "ID", + "description_en": "ID", + }, + "script": { + "type": "string", + "description": "脚本", + "description_en": "Script", + "code": "shell" + } + } + return SchemaResult(properties=properties) + dispatcher = DjangoBasicResourceApiDispatcher(iam, "my_system") dispatcher.register("flow", FlowResourceProvider()) diff --git a/iam/__version__.py b/iam/__version__.py index 7a1faa9..6ebbc35 100644 --- a/iam/__version__.py +++ b/iam/__version__.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- -__version__ = "1.2.1" +__version__ = "1.2.2" diff --git a/iam/contrib/django/dispatcher/dispatchers.py b/iam/contrib/django/dispatcher/dispatchers.py index 4b5121a..02ef31f 100644 --- a/iam/contrib/django/dispatcher/dispatchers.py +++ b/iam/contrib/django/dispatcher/dispatchers.py @@ -218,3 +218,32 @@ def _dispatch_search_instance(self, request, data, request_id): result = provider.search_instance(filter_obj, page_obj, **options) return success_response(result.to_dict(), request_id) + + def _dispatch_fetch_instance_list(self, request, data, request_id): + options = self._get_options(request) + + filter_obj = get_filter_obj(data.get("filter"), ["start_time", "end_time"]) + page_obj = get_page_obj(data.get("page")) + + provider = self._provider[data["type"]] + + pre_process = getattr(provider, "pre_fetch_instance_list", None) + if pre_process and callable(pre_process): + pre_process(filter_obj, page_obj, **options) + + result = provider.fetch_instance_list(filter_obj, page_obj, **options) + + return success_response(result.to_dict(), request_id) + + def _dispatch_fetch_resource_type_schema(self, request, data, request_id): + options = self._get_options(request) + + provider = self._provider[data["type"]] + + pre_process = getattr(provider, "pre_fetch_resource_type_schema", None) + if pre_process and callable(pre_process): + pre_process(**options) + + result = provider.fetch_resource_type_schema(**options) + + return success_response(result.to_dict(), request_id) diff --git a/iam/resource/constants.py b/iam/resource/constants.py new file mode 100644 index 0000000..a0b153f --- /dev/null +++ b/iam/resource/constants.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +""" +TencentBlueKing is pleased to support the open source community by making +蓝鲸智云-权限中心Python SDK(iam-python-sdk) available. +Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved. +Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. +You may obtain a copy of the License at http://opensource.org/licenses/MIT +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. +""" + + +class SchemaSpecificType(object): + """ + NOTE: don't want to use Enum + """ + + ARRAY = "array" + BOOLEAN = "boolean" + INTEGER = "integer" + NULL = "null" + NUMBER = "number" + OBJECT = "object" + STRING = "string" diff --git a/iam/resource/provider.py b/iam/resource/provider.py index 9dc5b08..3d86995 100644 --- a/iam/resource/provider.py +++ b/iam/resource/provider.py @@ -14,6 +14,8 @@ import six +from iam.resource.constants import SchemaSpecificType + class ListResult(object): def __init__(self, results, count): @@ -31,6 +33,18 @@ def to_list(self): return self.results +class SchemaResult(object): + def __init__(self, properties): + """ + :param properties: 资源类型 schema 定义 + """ + self.type = SchemaSpecificType.OBJECT + self.properties = properties + + def to_dict(self): + return {"type": self.type, "properties": self.properties} + + @six.add_metaclass(abc.ABCMeta) class ResourceProvider(object): @abc.abstractmethod @@ -80,3 +94,19 @@ def search_instance(self, filter, page, **options): return: ListResult """ raise NotImplementedError() + + def fetch_instance_list(self, filter, page, **options): + """ + 处理来自 iam 的 fetch_instance_list 请求 + 在审计中心生成静态资源快照时,需要实现此方法 + return: ListResult + """ + raise NotImplementedError() + + def fetch_resource_type_schema(self, **options): + """ + 处理来自 iam 的 fetch_resource_type_schema 请求 + 在审计中心显示静态资源时,需要实现此方法 + return: SchemaResult + """ + raise NotImplementedError() diff --git a/iam/resource/utils.py b/iam/resource/utils.py index 9855597..3c237df 100644 --- a/iam/resource/utils.py +++ b/iam/resource/utils.py @@ -38,4 +38,4 @@ def slice_to(self): def get_page_obj(page_data): - return Page(limit=page_data.get("limit", 0), offset=page_data.get("offset", 0)) + return Page(limit=int(page_data.get("limit") or 0), offset=int(page_data.get("offset") or 0)) diff --git a/release.md b/release.md index d04a539..541197a 100644 --- a/release.md +++ b/release.md @@ -1,6 +1,10 @@ 版本日志 =============== +# v1.2.2 + +- add: fetch_instance_list/fetch_resource_type_schema in ResourceProvider + # v1.2.1 - add: operator `string_contains` #68 diff --git a/tests/contrib/django/dispatcher/test_dispatchers.py b/tests/contrib/django/dispatcher/test_dispatchers.py index 99d3dbc..d9af9f1 100644 --- a/tests/contrib/django/dispatcher/test_dispatchers.py +++ b/tests/contrib/django/dispatcher/test_dispatchers.py @@ -23,7 +23,7 @@ from iam.contrib.django.dispatcher import DjangoBasicResourceApiDispatcher, InvalidPageException from iam.exceptions import AuthInvalidOperation -from iam.resource.provider import ListResult, ResourceProvider +from iam.resource.provider import ListResult, SchemaResult, ResourceProvider def test_basic_resource_api_dispatcher_register(): @@ -53,6 +53,12 @@ def list_instance_by_policy(self, filter, page, **options): def search_instance(self, filter, page, **options): return ListResult(results=[filter, page], count=100) + def fetch_instance_list(self, filter, page, **options): + return ListResult(results=[filter, page], count=100) + + def fetch_resource_type_schema(self, **options): + return SchemaResult(properties={"id": {"type": "string"}}) + with pytest.raises(AuthInvalidOperation): dispatcher.register("type", "provider") @@ -257,12 +263,16 @@ def __init__(self): self.fetch_instance_info_spy = {} self.list_instance_by_policy_spy = {} self.search_instance_spy = {} + self.fetch_instance_list_spy = {} + self.fetch_resource_type_schema_spy = {} self.pre_list_attr = MagicMock() self.pre_list_attr_value = MagicMock() self.pre_list_instance = MagicMock() self.pre_fetch_instance_info = MagicMock() self.pre_list_instance_by_policy = MagicMock() self.pre_search_instance = MagicMock() + self.pre_fetch_instance_list = MagicMock() + self.pre_fetch_resource_type_schema = MagicMock() def list_attr(self, **options): self.list_attr_spy["options"] = options @@ -297,6 +307,16 @@ def search_instance(self, filter, page, **options): self.search_instance_spy["options"] = options return ListResult(results=["search_instance_token"], count=100) + def fetch_instance_list(self, filter, page, **options): + self.fetch_instance_list_spy["filter"] = filter + self.fetch_instance_list_spy["page"] = page + self.fetch_instance_list_spy["options"] = options + return ListResult(results=["fetch_instance_list_token"], count=100) + + def fetch_resource_type_schema(self, **options): + self.fetch_resource_type_schema_spy["options"] = options + return SchemaResult(properties={"fetch_resource_type_schema_token"}) + provider = SpyResourceProvider() dispatcher.register("spy", provider) @@ -450,3 +470,48 @@ def search_instance(self, filter, page, **options): "filter": {"expression": "expression"}, "page": {"limit": "limit", "offset": "offset"}, } + + # test fetch_instance_list + fetch_instance_list_req = MagicMock() + fetch_instance_list_req.body = json.dumps( + { + "method": "fetch_instance_list", + "type": "spy", + "filter": {"start_time": 1654012800, "end_time": 1654099199}, + "page": {"limit": "limit", "offset": "offset"}, + } + ) + fetch_instance_list_req.META = {"HTTP_X_REQUEST_ID": "rid", "HTTP_BLUEKING_LANGUAGE": "en"} + + resp = dispatcher._dispatch(fetch_instance_list_req) + + provider.pre_fetch_instance_list.assert_called_once_with( + {"start_time": 1654012800, "end_time": 1654099199}, + {"limit": "limit", "offset": "offset"}, + language="en", + ) + assert resp["code"] == 0 + assert resp["result"] is True + assert resp["data"] == {"count": 100, "results": ["fetch_instance_list_token"]} + assert resp["X-Request-Id"] == "rid" + assert "message" in resp + assert provider.fetch_instance_list_spy == { + "options": {"language": "en"}, + "filter": {"start_time": 1654012800, "end_time": 1654099199}, + "page": {"limit": "limit", "offset": "offset"}, + } + + # test fetch_resource_type_schema + fetch_resource_type_schema_req = MagicMock() + fetch_resource_type_schema_req.body = json.dumps({"method": "fetch_resource_type_schema", "type": "spy"}) + fetch_resource_type_schema_req.META = {"HTTP_X_REQUEST_ID": "rid", "HTTP_BLUEKING_LANGUAGE": "en"} + + resp = dispatcher._dispatch(fetch_resource_type_schema_req) + + provider.pre_fetch_resource_type_schema.assert_called_once_with(language="en") + assert resp["code"] == 0 + assert resp["result"] is True + assert resp["data"] == SchemaResult(properties={"fetch_resource_type_schema_token"}).to_dict() + assert resp["X-Request-Id"] == "rid" + assert "message" in resp + assert provider.fetch_resource_type_schema_spy == {"options": {"language": "en"}}