From f435e27e14d3a81fa22d0fdd79db164011cc72e0 Mon Sep 17 00:00:00 2001 From: hito <1214105385@qq.com> Date: Wed, 20 Oct 2021 14:41:55 +0800 Subject: [PATCH] =?UTF-8?q?feature:=20=E6=96=B0=E7=89=88=E5=AF=BC=E8=88=AA?= =?UTF-8?q?=20(#1620)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: webconsole token invalid (#1564) * fix: webconsole token invalid * minor: use smart_str to convert bytes to str * fix: 集群信息界面 mem_usage -> memory_usage (#1566) * feat: update perm for access webconsole api (#1565) * feat: update perm for access webconsole api * feat: update perm for access webconsole api * refactor: 重构 components.cc 模块 (#1502) * refactor: components.cc * minor: resolve #1502 conversations * minor: get_application_name 调整为使用 admin 身份获取 * fix: 业务拓扑 补充 空闲机等 内部模块 * minor: APIError -> ComponentError * minor: use normal class rather than dataclass * fix: DEFAULT_SUPPLIER_ACCOUNT 改存 settings (#1567) * fix: DEFAULT_SUPPLIER_ACCOUNT 改存 settings * minor: BKCC_DEFAULT_SUPPLIER_ACCOUNT 支持从环境变量获取 * Iamv3 framework (#1580) * feat: project, cluster, namespace based and templateset on iam v3 * feat: add project & namespace iam api * minor: update license * chore: add requests_mock * minor: fix review * refactor: 删除K8S metriclist逻辑 * optimization: 节点选择 - 自定义输入不显示精确匹配勾选项 & refactor: 删除K8S metriclist逻辑 (#1587) * optimization: 节点选择 - 自定义输入不显示精确匹配勾选项 * refactor: 删除K8S metriclist逻辑 * feat (#1568) * fix: 0923节点选择器验收问题 * feat: Helm Release列表复选框去除, 删除release后列表不更新问题修复 * feat: Helm Release列表复选框去除, 删除release后列表不更新问题修复 * feat: Helm Release列表复选框去除, 删除release后列表不更新问题修复 * fix: 带有tooltip指令的button需要在外面包一层dom实现按钮禁用时的tooltip issue #1554 * feat: 自定义资源优化项 issue #1342 * fix: 创建集群弹框提示样式优化 issue #1552 * fix * feat: 申请主机功能优化|前端限制申请主机的数量 * fix * feat: 配额表单展示调整 | 命名空间侧边栏表单样式调整 | 节点详情网络图表展示问题 * feat: 资源视图优化项 #1574 * 样式调整 (#1589) * feat: MESOS应用页面展示调整 * feat: helm操作 命令行参数调整 * feat: helm操作 命令行参数调整 * feat: info组件添加defaultInfo配置项,值为true则使用组件库默认样式 * feat: info组件添加defaultInfo配置项,值为true则使用组件库默认样式 * fix: 节点管理页面搜索框自适应宽度调整 * feat: Helm Chart的readme内容渲染展示 * fix: 样式调整 * fix: ignore empty tasks in async_run (#1590) * docs: update helm push guide docs (#1592) * fix: AppQueryService.fetch_all add arg self.fields (#1593) * feat: update chart model default manager (#1598) * fix: 修复gamestatefulsets和gamedeployments批量删除功能缺失问题 (#1601) * Update cluster configure (#1553) * feat: update cluster configure * feat: add white list and update method for getting cluster cred * feat: update cluster configure * optimization: 优化代码批量删除弹窗宽度 (#1602) * optimization: 优化权限弹窗逻辑 * optimization: 集群首页重构 * refactor: 集群首页重构 (#1605) * refactor: 项目配置信息界面抽取 (#1608) * minor: set CORS_ALLOWED_ORIGINS (#1511) * feat: 标准化推送chart api的返回格式 (#1597) * feature: 新版导航 - 项目管理逻辑兼容多版本 * optimization: 同步修改helm md文档的数据返回格式 * optimization: 同步修改helm md文档的数据返回格式 (#1611) * optimization: 新版导航兼容监控中心跳转 * fix: 修复IP选择器表格模糊搜索分割符问题 & 自定义输入光标失效问题 * Fix: LoginSuccessView not render success (#1613) * fix: 修复IP选择器表格模糊搜索分割符问题 & 自定义输入光标失效问题 (#1614) * feature: 登录对接 * Fix: SESSION_COOKIE_DOMAIN not strip left dot (#1616) * optimization: 菜单配置优化 * fix: 修复集群指标watch集群时diff判断问题,导致指标重复请求 (#1617) * optimization: 菜单逻辑优化 * refator: 新导航 - 切换不同项目时重新刷新界面 * fix: 删除重复Key Co-authored-by: jamesge Co-authored-by: bellke Co-authored-by: schnee Co-authored-by: vhwweng <67886588+vhwweng@users.noreply.github.com> Co-authored-by: Joe Lei --- bcs-app/backend/api_urls.py | 2 +- bcs-app/backend/bcs_web/apis/permissions.py | 8 +- .../bcs_web/iam/bcs_iam_migration/__init__.py | 1 - .../bcs_iam_migration/migrations/__init__.py | 1 - .../backend/bcs_web/iam/open_apis/__init__.py | 1 - .../iam/open_apis/resources/__init__.py | 1 - .../iam/open_apis/resources/project.py | 49 - .../iam/open_apis/resources/provider.py | 64 - .../bcs_web/iam/open_apis/v1/__init__.py | 1 - bcs-app/backend/bcs_web/permissions.py | 2 +- bcs-app/backend/components/base.py | 14 +- bcs-app/backend/components/bcs/k8s_client.py | 55 +- bcs-app/backend/components/cc.py | 479 ------ bcs-app/backend/components/cc/__init__.py | 21 + bcs-app/backend/components/cc/business.py | 124 ++ bcs-app/backend/components/cc/client.py | 168 ++ .../bk_cc.py => components/cc/constants.py} | 55 +- bcs-app/backend/components/cc/hosts.py | 178 ++ bcs-app/backend/components/exceptions.py | 35 + bcs-app/backend/components/paas_auth.py | 2 +- bcs-app/backend/components/paas_cc.py | 9 +- bcs-app/backend/components/sops.py | 8 +- .../clusters/cc_host/utils.py | 2 +- .../clusters/cc_host/views.py | 40 +- .../container_service/clusters/constants.py | 2 +- .../flow_views/tools/cmdb/__init__.py | 35 - .../container_service/clusters/module_apis.py | 8 - .../container_service/clusters/urls.py | 5 - .../container_service/clusters/utils.py | 19 +- .../clusters/views/__init__.py | 1 - .../clusters/views/cluster.py | 4 +- .../container_service/clusters/views/node.py | 38 +- .../container_service/infras/hosts/host.py | 23 +- .../container_service/infras/hosts/perms.py | 6 +- .../container_service/projects/base/utils.py | 2 +- .../projects/open_apis/views.py | 4 +- .../container_service/projects/serializers.py | 2 +- .../container_service/projects/utils.py | 8 +- .../container_service/projects/views.py | 20 +- .../app/documentation/how-to-push-chart-en.md | 8 + .../app/documentation/how-to-push-chart.md | 8 + bcs-app/backend/helm/app/views.py | 7 +- bcs-app/backend/helm/helm/models/chart.py | 1 + bcs-app/backend/helm/helm/tasks.py | 2 +- .../open_apis/v1/views.py => iam/__init__.py} | 5 - .../bcs_iam_migration/__init__.py} | 5 - .../iam/bcs_iam_migration/apps.py | 3 +- .../migrations/0001_initial.py | 4 +- .../0002_bk_bcs_app_202108181450.py | 35 + .../0003_bk_bcs_app_202108181523.py | 35 + .../0004_bk_bcs_app_202108181524.py | 35 + .../0005_bk_bcs_app_202108181525.py | 35 + .../0006_bk_bcs_app_202108181525.py | 35 + .../0007_bk_bcs_app_202108262047.py | 35 + .../bcs_iam_migration/migrations/__init__.py | 14 + .../permissions.py => iam/legacy_perms.py} | 13 +- bcs-app/backend/iam/open_apis/__init__.py | 14 + .../iam/open_apis/authentication.py | 13 +- .../{bcs_web => }/iam/open_apis/constants.py | 28 +- .../{bcs_web => }/iam/open_apis/exceptions.py | 3 - .../iam/open_apis/provider/__init__.py | 14 + .../iam/open_apis/provider/namespace.py | 70 + .../backend/iam/open_apis/provider/project.py | 47 + .../iam/open_apis/provider/resource.py | 76 + .../backend/iam/open_apis/provider/utils.py | 20 + .../iam/open_apis/serializers.py | 10 +- .../{bcs_web => }/iam/open_apis/urls.py | 2 +- bcs-app/backend/iam/open_apis/v1/__init__.py | 14 + bcs-app/backend/iam/open_apis/v1/urls.py | 23 + .../{bcs_web => }/iam/open_apis/views.py | 10 +- bcs-app/backend/iam/permissions/__init__.py | 14 + bcs-app/backend/iam/permissions/apply_url.py | 45 + bcs-app/backend/iam/permissions/client.py | 102 ++ bcs-app/backend/iam/permissions/decorators.py | 141 ++ bcs-app/backend/iam/permissions/exceptions.py | 58 + bcs-app/backend/iam/permissions/perm.py | 180 ++ bcs-app/backend/iam/permissions/request.py | 98 ++ .../iam/permissions/resources/__init__.py | 14 + .../iam/permissions/resources/cluster.py | 117 ++ .../iam/permissions/resources/constants.py | 22 + .../iam/permissions/resources/namespace.py | 144 ++ .../iam/permissions/resources/project.py | 82 + .../iam/permissions/resources/templateset.py | 126 ++ bcs-app/backend/resources/client.py | 52 +- bcs-app/backend/resources/constants.py | 6 +- bcs-app/backend/settings/base.py | 9 +- bcs-app/backend/settings/ce/base.py | 9 +- bcs-app/backend/settings/ce/cors.py | 35 + bcs-app/backend/settings/ce/dev.py | 2 +- bcs-app/backend/settings/ce/saas_prod.py | 5 + .../tests/bcs_mocks/data/paas_cc_json.py | 72 + bcs-app/backend/tests/bcs_mocks/misc.py | 13 +- .../backend/tests/components/cc/__init__.py | 0 .../tests/components/cc/test_business.py | 66 + .../tests/components/cc/test_client.py | 136 ++ .../backend/tests/components/cc/test_hosts.py | 115 ++ .../backend/tests/components/test_paas_cc.py | 8 + .../tests/components/test_permissions.py | 2 +- .../clusters/test_cc_host.py | 47 +- .../infras/hosts/test_host.py | 29 +- .../{bcs_web => tests}/iam/__init__.py | 0 bcs-app/backend/tests/iam/conftest.py | 65 + bcs-app/backend/tests/iam/fake_iam.py | 95 ++ .../backend/tests/iam/open_apis/__init__.py | 14 + .../backend/tests/iam/open_apis/conftest.py | 26 + .../tests/iam/open_apis/test_namespace.py | 63 + .../tests/iam/open_apis/test_project.py | 55 + .../backend/tests/iam/permissions/__init__.py | 14 + .../backend/tests/iam/permissions/conftest.py | 34 + .../permissions/roles.py} | 36 +- .../tests/iam/permissions/test_cluster.py | 218 +++ .../tests/iam/permissions/test_namespace.py | 206 +++ .../tests/iam/permissions/test_project.py | 98 ++ .../tests/iam/permissions/test_templateset.py | 136 ++ .../backend/tests/resources/test_client.py | 2 + .../tests/testing_utils/mocks/paas_cc.py | 38 + bcs-app/backend/utils/async_run.py | 2 + bcs-app/backend/utils/error_codes.py | 5 +- bcs-app/backend/utils/permissions.py | 2 +- bcs-app/backend/utils/views.py | 11 +- bcs-app/backend/utils/whitelist.py | 32 + bcs-app/backend/web_console/pod_life_cycle.py | 5 +- bcs-app/backend/web_console/rest_api/views.py | 4 +- bcs-app/frontend/build/webpack.dev.conf.js | 9 +- bcs-app/frontend/login_success.html | 13 +- bcs-app/frontend/src/App.vue | 34 +- bcs-app/frontend/src/api/base.js | 5 +- bcs-app/frontend/src/api/index.js | 23 +- bcs-app/frontend/src/common/use-config.ts | 17 + .../ip-selector/ip-selector-bcs.vue | 3 +- .../components/ip-selector/ip-selector.css | 186 +- .../src/components/ip-selector/ip-selector.js | 4 +- .../src/components/terminal/index.vue | 2 +- .../src/components/tip-dialog/index.css | 2 +- bcs-app/frontend/src/i18n/lang.js | 9 +- bcs-app/frontend/src/router/index.js | 14 +- bcs-app/frontend/src/store/index.js | 13 +- bcs-app/frontend/src/store/modules/cluster.js | 15 - .../src/views/app/k8s/gamedeployments.vue | 38 +- .../src/views/app/k8s/gamestatefulset.vue | 39 +- .../frontend/src/views/cluster/apply-host.vue | 8 +- bcs-app/frontend/src/views/cluster/index.css | 27 +- bcs-app/frontend/src/views/cluster/index.vue | 1501 +++++------------ bcs-app/frontend/src/views/cluster/info.vue | 2 +- .../src/views/cluster/node-overview.vue | 2 +- .../src/views/cluster/node.external.vue | 2 +- bcs-app/frontend/src/views/cluster/node.vue | 24 +- .../frontend/src/views/cluster/use-cluster.ts | 177 ++ .../configuration/k8s-create/daemonset.vue | 6 +- .../configuration/k8s-create/deployment.vue | 6 +- .../views/configuration/k8s-create/job.vue | 6 +- .../configuration/k8s-create/statefulset.vue | 6 +- .../src/views/configuration/namespace.vue | 12 +- .../src/views/crdcontroller/index.vue | 4 +- .../views/dashboard/common/use-interval.ts | 7 +- .../resource-update/editor-status.vue | 2 +- .../resource-update/resource-update.vue | 46 +- bcs-app/frontend/src/views/helm/guide.vue | 3 +- bcs-app/frontend/src/views/index.vue | 11 +- bcs-app/frontend/src/views/navigation.vue | 14 +- bcs-app/frontend/src/views/side-nav.vue | 42 +- bcs-app/poetry.lock | 56 +- bcs-app/pyproject.toml | 1 + .../support-files/iam/0002_project_extra.json | 44 + bcs-app/support-files/iam/0003_cluster.json | 156 ++ bcs-app/support-files/iam/0004_namespace.json | 157 ++ .../support-files/iam/0005_templateset.json | 182 ++ .../support-files/iam/0006_action_groups.json | 96 ++ .../iam/0007_resource_creator_actions.json | 80 + 169 files changed, 5837 insertions(+), 2300 deletions(-) delete mode 100644 bcs-app/backend/bcs_web/iam/bcs_iam_migration/__init__.py delete mode 100644 bcs-app/backend/bcs_web/iam/bcs_iam_migration/migrations/__init__.py delete mode 100644 bcs-app/backend/bcs_web/iam/open_apis/__init__.py delete mode 100644 bcs-app/backend/bcs_web/iam/open_apis/resources/__init__.py delete mode 100644 bcs-app/backend/bcs_web/iam/open_apis/resources/project.py delete mode 100644 bcs-app/backend/bcs_web/iam/open_apis/resources/provider.py delete mode 100644 bcs-app/backend/bcs_web/iam/open_apis/v1/__init__.py delete mode 100644 bcs-app/backend/components/cc.py create mode 100644 bcs-app/backend/components/cc/__init__.py create mode 100644 bcs-app/backend/components/cc/business.py create mode 100644 bcs-app/backend/components/cc/client.py rename bcs-app/backend/{tests/testing_utils/mocks/bk_cc.py => components/cc/constants.py} (50%) create mode 100644 bcs-app/backend/components/cc/hosts.py create mode 100644 bcs-app/backend/components/exceptions.py delete mode 100644 bcs-app/backend/container_service/clusters/flow_views/tools/cmdb/__init__.py rename bcs-app/backend/{bcs_web/iam/open_apis/v1/views.py => iam/__init__.py} (89%) rename bcs-app/backend/{bcs_web/iam/open_apis/v1/urls.py => iam/bcs_iam_migration/__init__.py} (86%) rename bcs-app/backend/{bcs_web => }/iam/bcs_iam_migration/apps.py (92%) rename bcs-app/backend/{bcs_web => }/iam/bcs_iam_migration/migrations/0001_initial.py (94%) create mode 100644 bcs-app/backend/iam/bcs_iam_migration/migrations/0002_bk_bcs_app_202108181450.py create mode 100644 bcs-app/backend/iam/bcs_iam_migration/migrations/0003_bk_bcs_app_202108181523.py create mode 100644 bcs-app/backend/iam/bcs_iam_migration/migrations/0004_bk_bcs_app_202108181524.py create mode 100644 bcs-app/backend/iam/bcs_iam_migration/migrations/0005_bk_bcs_app_202108181525.py create mode 100644 bcs-app/backend/iam/bcs_iam_migration/migrations/0006_bk_bcs_app_202108181525.py create mode 100644 bcs-app/backend/iam/bcs_iam_migration/migrations/0007_bk_bcs_app_202108262047.py create mode 100644 bcs-app/backend/iam/bcs_iam_migration/migrations/__init__.py rename bcs-app/backend/{bcs_web/iam/permissions.py => iam/legacy_perms.py} (97%) create mode 100644 bcs-app/backend/iam/open_apis/__init__.py rename bcs-app/backend/{bcs_web => }/iam/open_apis/authentication.py (85%) rename bcs-app/backend/{bcs_web => }/iam/open_apis/constants.py (57%) rename bcs-app/backend/{bcs_web => }/iam/open_apis/exceptions.py (94%) create mode 100644 bcs-app/backend/iam/open_apis/provider/__init__.py create mode 100644 bcs-app/backend/iam/open_apis/provider/namespace.py create mode 100644 bcs-app/backend/iam/open_apis/provider/project.py create mode 100644 bcs-app/backend/iam/open_apis/provider/resource.py create mode 100644 bcs-app/backend/iam/open_apis/provider/utils.py rename bcs-app/backend/{bcs_web => }/iam/open_apis/serializers.py (74%) rename bcs-app/backend/{bcs_web => }/iam/open_apis/urls.py (91%) create mode 100644 bcs-app/backend/iam/open_apis/v1/__init__.py create mode 100644 bcs-app/backend/iam/open_apis/v1/urls.py rename bcs-app/backend/{bcs_web => }/iam/open_apis/views.py (82%) create mode 100644 bcs-app/backend/iam/permissions/__init__.py create mode 100644 bcs-app/backend/iam/permissions/apply_url.py create mode 100644 bcs-app/backend/iam/permissions/client.py create mode 100644 bcs-app/backend/iam/permissions/decorators.py create mode 100644 bcs-app/backend/iam/permissions/exceptions.py create mode 100644 bcs-app/backend/iam/permissions/perm.py create mode 100644 bcs-app/backend/iam/permissions/request.py create mode 100644 bcs-app/backend/iam/permissions/resources/__init__.py create mode 100644 bcs-app/backend/iam/permissions/resources/cluster.py create mode 100644 bcs-app/backend/iam/permissions/resources/constants.py create mode 100644 bcs-app/backend/iam/permissions/resources/namespace.py create mode 100644 bcs-app/backend/iam/permissions/resources/project.py create mode 100644 bcs-app/backend/iam/permissions/resources/templateset.py create mode 100644 bcs-app/backend/settings/ce/cors.py create mode 100644 bcs-app/backend/tests/components/cc/__init__.py create mode 100644 bcs-app/backend/tests/components/cc/test_business.py create mode 100644 bcs-app/backend/tests/components/cc/test_client.py create mode 100644 bcs-app/backend/tests/components/cc/test_hosts.py rename bcs-app/backend/{bcs_web => tests}/iam/__init__.py (100%) create mode 100644 bcs-app/backend/tests/iam/conftest.py create mode 100644 bcs-app/backend/tests/iam/fake_iam.py create mode 100644 bcs-app/backend/tests/iam/open_apis/__init__.py create mode 100644 bcs-app/backend/tests/iam/open_apis/conftest.py create mode 100644 bcs-app/backend/tests/iam/open_apis/test_namespace.py create mode 100644 bcs-app/backend/tests/iam/open_apis/test_project.py create mode 100644 bcs-app/backend/tests/iam/permissions/__init__.py create mode 100644 bcs-app/backend/tests/iam/permissions/conftest.py rename bcs-app/backend/tests/{components/test_bk_cc.py => iam/permissions/roles.py} (55%) create mode 100644 bcs-app/backend/tests/iam/permissions/test_cluster.py create mode 100644 bcs-app/backend/tests/iam/permissions/test_namespace.py create mode 100644 bcs-app/backend/tests/iam/permissions/test_project.py create mode 100644 bcs-app/backend/tests/iam/permissions/test_templateset.py create mode 100644 bcs-app/backend/utils/whitelist.py create mode 100644 bcs-app/frontend/src/common/use-config.ts create mode 100644 bcs-app/frontend/src/views/cluster/use-cluster.ts create mode 100644 bcs-app/support-files/iam/0002_project_extra.json create mode 100644 bcs-app/support-files/iam/0003_cluster.json create mode 100644 bcs-app/support-files/iam/0004_namespace.json create mode 100644 bcs-app/support-files/iam/0005_templateset.json create mode 100644 bcs-app/support-files/iam/0006_action_groups.json create mode 100644 bcs-app/support-files/iam/0007_resource_creator_actions.json diff --git a/bcs-app/backend/api_urls.py b/bcs-app/backend/api_urls.py index 1755451d5..40e4281c0 100644 --- a/bcs-app/backend/api_urls.py +++ b/bcs-app/backend/api_urls.py @@ -40,7 +40,7 @@ include("backend.templatesets.open_apis.template_urls"), ), # 提供给iam拉取资源实例的url(已注册到iam后台) - url(r"^iam/", include("backend.bcs_web.iam.open_apis.urls")), + url(r"^iam/", include("backend.iam.open_apis.urls")), # web_console API url( r"^projects/(?P[\w\-]+)/clusters/(?P[\w\-]+)/web_console/sessions/", diff --git a/bcs-app/backend/bcs_web/apis/permissions.py b/bcs-app/backend/bcs_web/apis/permissions.py index 494c98062..aa4bcc302 100644 --- a/bcs-app/backend/bcs_web/apis/permissions.py +++ b/bcs-app/backend/bcs_web/apis/permissions.py @@ -18,7 +18,7 @@ from rest_framework import permissions from backend.components.paas_auth import get_access_token -from backend.utils import FancyDict +from backend.utils import FancyDict, whitelist from .constants import ACCESS_TOKEN_KEY_NAME @@ -87,9 +87,5 @@ def has_permission(self, request, view): return False app_code = request.user.client.app.app_code - project_whitelist = settings.BK_APP_WHITELIST.get(app_code) or [] - if project_id_or_code in project_whitelist: - return True - - return False + return whitelist.check_app_access_webconsole_enable(app_code, project_id_or_code) diff --git a/bcs-app/backend/bcs_web/iam/bcs_iam_migration/__init__.py b/bcs-app/backend/bcs_web/iam/bcs_iam_migration/__init__.py deleted file mode 100644 index 40a96afc6..000000000 --- a/bcs-app/backend/bcs_web/iam/bcs_iam_migration/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# -*- coding: utf-8 -*- diff --git a/bcs-app/backend/bcs_web/iam/bcs_iam_migration/migrations/__init__.py b/bcs-app/backend/bcs_web/iam/bcs_iam_migration/migrations/__init__.py deleted file mode 100644 index 40a96afc6..000000000 --- a/bcs-app/backend/bcs_web/iam/bcs_iam_migration/migrations/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# -*- coding: utf-8 -*- diff --git a/bcs-app/backend/bcs_web/iam/open_apis/__init__.py b/bcs-app/backend/bcs_web/iam/open_apis/__init__.py deleted file mode 100644 index 40a96afc6..000000000 --- a/bcs-app/backend/bcs_web/iam/open_apis/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# -*- coding: utf-8 -*- diff --git a/bcs-app/backend/bcs_web/iam/open_apis/resources/__init__.py b/bcs-app/backend/bcs_web/iam/open_apis/resources/__init__.py deleted file mode 100644 index 40a96afc6..000000000 --- a/bcs-app/backend/bcs_web/iam/open_apis/resources/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# -*- coding: utf-8 -*- diff --git a/bcs-app/backend/bcs_web/iam/open_apis/resources/project.py b/bcs-app/backend/bcs_web/iam/open_apis/resources/project.py deleted file mode 100644 index b92b34cec..000000000 --- a/bcs-app/backend/bcs_web/iam/open_apis/resources/project.py +++ /dev/null @@ -1,49 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community -Edition) 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. -""" -try: - from iam.resource.provider import ListResult, ResourceProvider -except Exception: - pass -from backend.components import ssm -from backend.container_service.projects.base import filter_projects - - -class ProjectProvider(ResourceProvider): - def list_attr(self, **options): - return ListResult(results=[], count=0) - - def list_attr_value(self, filter, page, **options): - return ListResult(results=[], count=0) - - def list_instance(self, filter, page, **options): - access_token = ssm.get_client_access_token()["access_token"] - projects = filter_projects(access_token) - count = len(projects) - projects = projects[page.slice_from : page.slice_to] # noqa - results = [{"id": p["project_id"], "display_name": p["project_name"]} for p in projects] - return ListResult(results=results, count=count) - - def fetch_instance_info(self, filter, **options): - access_token = ssm.get_client_access_token()["access_token"] - query_params = None - if filter.ids: - query_params = {"project_ids": ",".join(filter.ids)} - projects = filter_projects(access_token, query_params) - results = [{"id": p["project_id"], "display_name": p["project_name"]} for p in projects] - return ListResult(results=results, count=len(results)) - - def list_instance_by_policy(self, filter, page, **options): - # TODO 确认基于实例的查询是不是就是id的过滤查询 - return ListResult(results=[], count=0) diff --git a/bcs-app/backend/bcs_web/iam/open_apis/resources/provider.py b/bcs-app/backend/bcs_web/iam/open_apis/resources/provider.py deleted file mode 100644 index 7558e247f..000000000 --- a/bcs-app/backend/bcs_web/iam/open_apis/resources/provider.py +++ /dev/null @@ -1,64 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community -Edition) 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. -""" -try: - from iam.resource.utils import get_filter_obj, get_page_obj -except Exception: - pass - -from ..exceptions import ResNotFoundError -from .project import ProjectProvider - -PROVIDER_CLS_MAP = {"project": ProjectProvider} - - -class BCSResourceProvider: - def __init__(self, resource_type): - try: - self.resource_provider = PROVIDER_CLS_MAP.get(resource_type)() - except Exception: - raise ResNotFoundError(f"unsupported resource type: {resource_type}") - - def _parse_filter_and_page(self, data): - filter_obj = get_filter_obj(data["filter"], ["ids", "parent", "search", "resource_type_chain"]) - page_obj = get_page_obj(data.get("page")) - return filter_obj, page_obj - - def provide(self, data, **options): - handler = getattr(self, data["method"]) - return handler(data, **options) - - def list_attr(self, data, **options): - result = self.resource_provider.list_attr(**options) - return result.to_list() - - def list_attr_value(self, data, **options): - filter, page = self._parse_filter_and_page(data) - result = self.resource_provider.list_attr_value(filter, page, **options) - return result.to_dict() - - def list_instance(self, data, **options): - filter, page = self._parse_filter_and_page(data) - result = self.resource_provider.list_instance(filter, page, **options) - return result.to_dict() - - def fetch_instance_info(self, data, **options): - filter, _ = self._parse_filter_and_page(data) - result = self.resource_provider.fetch_instance_info(filter, **options) - return result.to_list() - - def list_instance_by_policy(self, data, **options): - filter, page = self._parse_filter_and_page(data) - result = self.resource_provider.list_instance_by_policy(filter, page, **options) - return result.to_list() diff --git a/bcs-app/backend/bcs_web/iam/open_apis/v1/__init__.py b/bcs-app/backend/bcs_web/iam/open_apis/v1/__init__.py deleted file mode 100644 index 40a96afc6..000000000 --- a/bcs-app/backend/bcs_web/iam/open_apis/v1/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# -*- coding: utf-8 -*- diff --git a/bcs-app/backend/bcs_web/permissions.py b/bcs-app/backend/bcs_web/permissions.py index acb6a66f4..a9baeb6e1 100644 --- a/bcs-app/backend/bcs_web/permissions.py +++ b/bcs-app/backend/bcs_web/permissions.py @@ -19,11 +19,11 @@ from backend.accounts import bcs_perm from backend.bcs_web.audit_log.audit.context import AuditContext -from backend.bcs_web.iam import permissions from backend.components.base import ComponentAuth from backend.components.paas_cc import PaaSCCClient from backend.container_service.clusters.base.models import CtxCluster from backend.container_service.projects.base.models import CtxProject +from backend.iam import legacy_perms as permissions from backend.utils import FancyDict from backend.utils.cache import region diff --git a/bcs-app/backend/components/base.py b/bcs-app/backend/components/base.py index 30f3fca07..094323fd1 100644 --- a/bcs-app/backend/components/base.py +++ b/bcs-app/backend/components/base.py @@ -231,8 +231,8 @@ class BkCommonResponseHandler: :param func: 调用的函数 """ - def __init__(self, default_data: Optional[Any] = None, func: Callable = None): - self.default_data = default_data + def __init__(self, default: Optional[Any] = None, func: Callable = None): + self.default = default self.func = func def __get__(self, instance, cls): @@ -244,12 +244,16 @@ def __get__(self, instance, cls): def __call__(self, *args, **kwargs) -> Any: resp = self.func(*args, **kwargs) if resp.get("code") == 0 or resp.get("result") is True: - return resp.get("data") or self.default_data + return resp.get("data") or self._get_default() raise CompParseBkCommonResponseError(resp, resp.get("message")) + def _get_default(self): + """ 参考 drf Serializer Field get_default 方法,兼容可调用对象,如 dict, list """ + return self.default() if callable(self.default) else self.default + def raw_request(self, *args, **kwargs) -> Any: return self.func(*args, **kwargs) -def response_handler(default_data: Optional[Any] = None): - return functools.partial(BkCommonResponseHandler, default_data) +def response_handler(default: Optional[Any] = None): + return functools.partial(BkCommonResponseHandler, default) diff --git a/bcs-app/backend/components/bcs/k8s_client.py b/bcs-app/backend/components/bcs/k8s_client.py index c95e44a2a..9d66a4ce2 100644 --- a/bcs-app/backend/components/bcs/k8s_client.py +++ b/bcs-app/backend/components/bcs/k8s_client.py @@ -12,42 +12,30 @@ 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 json import logging +from functools import lru_cache from typing import Dict -from cachetools import LRUCache, cached -from cachetools.keys import hashkey from django.conf import settings from django.utils.functional import cached_property from kubernetes import client from backend.components.bcs import BCSClientBase, resources -from backend.components.utils import http_get +from backend.container_service.clusters.base import CtxCluster +from backend.resources.client import BcsKubeConfigurationService logger = logging.getLogger(__name__) -# 获取cluster context使用的缓存策略 -cluster_context_cache_policy = LRUCache(maxsize=128) - -@cached( - cache=cluster_context_cache_policy, - key=lambda url_prefix, access_token, project_id, cluster_id: hashkey(url_prefix, project_id, cluster_id), -) -def make_cluster_context(url_prefix: str, access_token: str, project_id: str, cluster_id: str) -> Dict: - """组装集群的Context""" - # 获取bcs api的集群信息 - url = f"{url_prefix}/bcs/query_by_id/" - params = {"access_token": access_token, "project_id": project_id, "cluster_id": cluster_id} - headers = {"Authorization": getattr(settings, "BCS_AUTH_TOKEN", ""), "Content-Type": "application/json"} - context = http_get(url, params=params, raise_for_status=False, headers=headers) - # 获取集群的credential - url = f"{url_prefix}/{context['id']}/client_credentials" - params = {"access_token": access_token} - credentials = http_get(url, params=params, raise_for_status=False, headers=headers) - context.update(credentials) - - return context +@lru_cache(maxsize=32) +def make_cluster_configuration(access_token: str, project_id: str, cluster_id: str) -> Dict: + ctx_cluster = CtxCluster.create( + id=cluster_id, + project_id=project_id, + token=access_token, + ) + return BcsKubeConfigurationService(ctx_cluster).make_configuration() class K8SAPIClient(BCSClientBase): @@ -61,19 +49,12 @@ def _headers_for_bcs_agent_api(self): @cached_property def api_client(self): - configure = client.Configuration() - configure.verify_ssl = False - # 获取集群context,如果调用接口或者其它异常导致失败,需要主动使缓存失效 - try: - context = make_cluster_context(self.rest_host, self.access_token, self.project_id, self.cluster_id) - configure.host = f"{self._bcs_server_host}{context['server_address_path']}".rstrip("/") - configure.api_key = {"authorization": f"Bearer {context['user_token']}"} - except Exception as e: - logger.exception("make cluster context error, %s", e) - # 当出现异常时,需要清空缓存 - cluster_context_cache_policy.clear() - raise - api_client = client.ApiClient(configure) + configure = make_cluster_configuration(self.access_token, self.project_id, self.cluster_id) + api_client = client.ApiClient( + configure, + header_name='X-BKAPI-AUTHORIZATION', + header_value=json.dumps({"access_token": self.access_token}), + ) return api_client diff --git a/bcs-app/backend/components/cc.py b/bcs-app/backend/components/cc.py deleted file mode 100644 index b0402c3a7..000000000 --- a/bcs-app/backend/components/cc.py +++ /dev/null @@ -1,479 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community -Edition) 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. -""" -import functools -import logging -import re -from copy import deepcopy -from dataclasses import asdict, dataclass -from itertools import chain -from typing import Dict, List, Optional - -from django.conf import settings -from requests import PreparedRequest -from requests.auth import AuthBase - -from backend.components.base import BaseHttpClient, BkApiClient, response_handler, update_request_body -from backend.components.utils import http_post -from backend.utils.async_run import async_run -from backend.utils.basic import getitems -from backend.utils.errcodes import ErrorCode -from backend.utils.error_codes import error_codes - -CC_HOST = settings.BK_PAAS_INNER_HOST -BK_APP_CODE = settings.APP_ID -BK_APP_SECRET = settings.APP_TOKEN -PREFIX_PATH = '/api/c/compapi' -# 方法名称 -> 请求路径 -FUNC_PATH_MAP = { - 'get_application': '/v2/cc/search_business/', - 'search_host': '/v2/cc/search_host/', - 'list_biz_hosts': '/v2/cc/list_biz_hosts/', - 'search_biz_inst_topo': '/v2/cc/search_biz_inst_topo/', - 'get_biz_internal_module': '/v2/cc/get_biz_internal_module/', -} -# 默认开发商账号 -DEFAULT_SUPPLIER_ACCOUNT = None -# 默认查询主机字段 -DEFAULT_HOST_FIELDS = [ - "bk_bak_operator", - "classify_level_name", - "svr_device_class", - "bk_svr_type_id", - "svr_type_name", - "hard_memo", - "bk_host_id", - "bk_host_name", - "idc_name", - "bk_idc_area", - "bk_idc_area_id", - "idc_id", - "idc_unit_name", - "idc_unit_id", - "bk_host_innerip", - "bk_comment", - "module_name", - "operator", - "bk_os_name", - "bk_os_version", - "bk_host_outerip", - "rack", - "bk_cloud_id", -] -# 默认从 0 开始查询 -DEFAULT_START_AT = 0 -# 用于查询 count 的 Limit,最小为 1 -LIMIT_FOR_COUNT = 1 -# CMDB 通用的最大 Limit 限制 -CMDB_MAX_LIMIT = 200 -# CMDB 请求主机列表最大 Limit 限制 -CMDB_LIST_HOSTS_MAX_LIMIT = 500 - -logger = logging.getLogger(__name__) - - -def cmdb_base_request(suffix_path: str, username: str, data: Dict, bk_supplier_account: Optional[str] = None) -> Dict: - """请求""" - data.update({"bk_app_code": BK_APP_CODE, "bk_app_secret": BK_APP_SECRET, "bk_username": username}) - if bk_supplier_account: - data["bk_supplier_account"] = bk_supplier_account - - return http_post(f"{CC_HOST}{PREFIX_PATH}{suffix_path}", json=data) - - -def get_application(username, bk_supplier_account=None): - resp = get_all_application(username, bk_supplier_account=bk_supplier_account) - data = resp.get("data") or [] - ret_data = {} - for info in data: - info["DisplayName"] = info["bk_biz_name"] - ret_data[info["bk_biz_id"]] = info - return ret_data - - -def get_app_by_user_role(username): - """获取运维和产品角色中包含username的业务""" - username_regex_info = "^{username},|,{username},|,{username}$|^{username}$".format(username=username) - regex_map = {"$regex": username_regex_info} - # NOTE: CMDB建议查询方式: 以admin用户身份跳过资源查询权限,然后CMDB接口根据传递的condition中用户,返回过滤的业务 - maintainers_resp = get_all_application("admin", condition={"bk_biz_maintainer": regex_map}) - # 组装数据 - if maintainers_resp.get("code") != ErrorCode.NoError: - return [] - data = maintainers_resp.get("data") or [] - - return [{"id": item["bk_biz_id"], "name": item["bk_biz_name"]} for item in data] - - -def get_application_name(username, bk_biz_id, bk_supplier_account=None): - """通过项目ID获取到项目名称 - TODO: 后面看实际耗时,再考虑是否缓存 - """ - resp = get_application_with_pagination( - username, bk_supplier_account=bk_supplier_account, fields=["bk_biz_name"], condition={"bk_biz_id": bk_biz_id} - ) - if resp.get("code") != ErrorCode.NoError: - return "" - biz_info = (resp.get("data") or {}).get("info") or [] - if not biz_info: - return "" - return biz_info[0].get("bk_biz_name") or "" - - -def get_all_application( - username, bk_supplier_account=None, condition=None, start=DEFAULT_START_AT, limit=CMDB_MAX_LIMIT -): - """获取用户有权限的所有业务""" - condition = condition or {} - resp_data = {"data": [], "message": "", "code": ErrorCode.NoError} - while True: - resp = get_application_with_pagination( - username, bk_supplier_account=bk_supplier_account, condition=condition, start=start, limit=limit - ) - if resp.get("code") != ErrorCode.NoError: - resp_data["code"] = resp.get("code") - resp_data["message"] = resp.get("message") - break - data = resp.get("data") or {} - biz_info = data.get("info") or [] - resp_data["data"].extend(biz_info) - - start = start + limit - if start >= data.get("count", 0) or not biz_info: - break - - return resp_data - - -def get_app_hosts(username, bk_biz_id, bk_supplier_account=None, bk_module_ids=None): - resp = list_biz_hosts(username, bk_biz_id, bk_supplier_account=bk_supplier_account, bk_module_ids=bk_module_ids) - if not resp.get("result"): - return resp - data = resp.get("data") or [] - if not data: - return resp - ret_data = [] - for host in data: - if not host: - continue - host["InnerIP"] = host["bk_host_innerip"] - host["HostName"] = host["bk_host_name"] - ret_data.append(host) - return {"result": True, "data": ret_data} - - -def get_host_by_operator(bk_biz_id, username, bk_supplier_account=None): - """获取业务下主备负责人为username的机器""" - resp = list_biz_hosts(username, bk_biz_id, bk_supplier_account=bk_supplier_account) - if resp.get("code") != ErrorCode.NoError: - return resp - data = resp.get("data") or [] - host_list = [] - for host in data: - if not host: - continue - operator = host.get("operator", "") - bak_operator = host.get("bk_bak_operator", "") - if (username == operator) or (username == bak_operator): - host["InnerIP"] = host["bk_host_innerip"] - host["HostName"] = host["bk_host_name"] - host_list.append(host) - - return {"result": True, "data": host_list} - - -def get_app_maintainers(username, bk_biz_id, bk_supplier_account=None): - """获取业务下的所有运维""" - resp = get_application_with_pagination( - username, - bk_supplier_account=bk_supplier_account, - fields=["bk_biz_maintainer"], - condition={"bk_biz_id": bk_biz_id}, - ) - if resp.get("code") != ErrorCode.NoError: - return [] - biz_info = (resp.get("data") or {}).get("info") or [] - if not biz_info: - return [] - maintainers = biz_info[0].get("bk_biz_maintainer") or "" - return re.findall(r"[^,;]+", maintainers) - - -def get_cc_hosts(bk_biz_id, username, bk_module_ids=None): - """查询业务下有权限的主机""" - all_maintainers = get_app_maintainers(username, bk_biz_id) - if username in all_maintainers: - return get_app_hosts(username, bk_biz_id, bk_module_ids=bk_module_ids) - return get_host_by_operator(bk_biz_id, username) - - -def get_application_host(username, bk_biz_id, inner_ip, bk_supplier_account=None): - """获取服务器信息 - 注意: 其中信息包含了先前get_host_base_info获取到的信息 - """ - resp = list_biz_hosts( - username, - bk_biz_id, - host_property_filter={ - "condition": "OR", - "rules": [ - {"field": "bk_bak_operator", "operator": "equal", "value": username}, - {"filed": "operator", "operator": "equal", "value": "username"}, - ], - }, - bk_supplier_account=bk_supplier_account, - ) - if resp.get("code") != ErrorCode.NoError: - return {} - data = resp.get("data") - if not data: - return {} - return data[0].get("host") or {} - - -def get_host_base_info(username, bk_biz_id, inner_ip): - data = get_application_host(username, bk_biz_id, inner_ip) - ret_data = { - "Cpu": {"CpuNum": data.get("bk_cpu", 0)}, - "Disk": {"Total": data.get("bk_disk", 0) * 1024 * 1024 * 1024}, - "InnerIP": data.get("bk_host_innerip", ""), - "HostID": data.get("bk_host_id", ""), - "Memory": {"Total": data.get("bk_mem", 0) * 1024 * 1024}, - "System": { - "OS": data.get("bk_os_name", ""), - "kernelVersion": data.get("bk_os_version", ""), - "clientDockerVersion": "", - "serverDockerVersion": "", - }, - "provider": "CMDB", - } - return ret_data - - -def get_application_with_pagination( - username, bk_supplier_account=None, condition=None, fields=None, start=DEFAULT_START_AT, limit=CMDB_MAX_LIMIT -): - """分页查询业务""" - condition = condition or {} - fields = fields or [] - data = {"condition": condition, "fields": fields, "page": {"start": start, "limit": limit}} - return cmdb_base_request(FUNC_PATH_MAP["get_application"], username, data, bk_supplier_account=bk_supplier_account) - - -def list_biz_hosts( - username, - bk_biz_id, - host_property_filter=None, - bk_module_ids=None, - start=DEFAULT_START_AT, - limit=CMDB_LIST_HOSTS_MAX_LIMIT, - bk_supplier_account=None, -): - """获取业务下所有主机信息""" - resp_data = {"data": [], "message": "", "code": ErrorCode.NoError, "result": True} - while True: - resp = list_hosts_by_pagination( - username, - bk_biz_id, - host_property_filter=host_property_filter, - bk_module_ids=bk_module_ids, - start=start, - limit=limit, - bk_supplier_account=bk_supplier_account, - ) - if resp.get("code") != ErrorCode.NoError: - resp_data.update({"code": resp.get("code"), "message": resp.get("message"), "result": resp.get("result")}) - break - data = resp.get("data") or {} - biz_info = data.get("info") or [] - resp_data["data"].extend(biz_info) - # 对比机器数量,满足条件时终止请求 - start = start + limit - if start >= data.get("count", 0) or not biz_info: - break - - return resp_data - - -def list_hosts_by_pagination( - username, - bk_biz_id, - host_property_filter=None, - bk_module_ids=None, - start=DEFAULT_START_AT, - limit=CMDB_LIST_HOSTS_MAX_LIMIT, - bk_supplier_account=None, -): - """根据分页参数,获取业务下主机信息""" - data = {"bk_biz_id": bk_biz_id, "page": {"start": start, "limit": limit}} - # host_property_filter 主机组合属性查询条件 - data["host_property_filter"] = host_property_filter - # bk_module_ids 模块ID列表 - data["bk_module_ids"] = bk_module_ids - # 添加fields字段 - data["fields"] = DEFAULT_HOST_FIELDS - - return cmdb_base_request(FUNC_PATH_MAP["list_biz_hosts"], username, data, bk_supplier_account=bk_supplier_account) - - -def search_biz_inst_topo(username: str, bk_biz_id: str) -> List: - """ - 查询业务拓扑 - - :param username: 查询者用户名 - :param bk_biz_id: 业务 ID - :return: 业务,集群,模块拓扑信息 - """ - resp = cmdb_base_request(FUNC_PATH_MAP['search_biz_inst_topo'], username, {'bk_biz_id': bk_biz_id}) - if not resp.get('result'): - raise error_codes.APIError(resp.get('message')) - return resp.get('data') or [] - - -def get_biz_internal_module(username: str, bk_biz_id: str) -> Dict: - """ - 查询业务的空闲机/故障机/待回收模块 - - :param username: 查询者用户名 - :param bk_biz_id: 业务 ID - :return: 内部模块拓扑信息 - """ - resp = cmdb_base_request(FUNC_PATH_MAP['get_biz_internal_module'], username, {'bk_biz_id': bk_biz_id}) - if not resp.get('result'): - raise error_codes.APIError(resp.get('message')) - return resp.get('data') or {} - - -def fetch_host_count_by_topo( - username: str, bk_biz_id: str, bk_set_ids: List = None, bk_module_ids: List = None -) -> int: - """ 查询指定条件下主机数量,用于后续并发查询用 """ - params = { - 'bk_biz_id': bk_biz_id, - 'page': { - 'start': DEFAULT_START_AT, - 'limit': LIMIT_FOR_COUNT, - }, - 'bk_set_ids': bk_set_ids, - 'bk_module_ids': bk_module_ids, - 'fields': ['bk_host_innerip'], - } - resp = cmdb_base_request(FUNC_PATH_MAP['list_biz_hosts'], username, params) - return getitems(resp, 'data.count', 0) - - -def list_all_hosts_by_topo( - username: str, bk_biz_id: str, bk_set_ids: List = None, bk_module_ids: List = None -) -> List[Dict]: - """ - 并发查询 CMDB,获取符合条件的全量主机信息,目前仅支持按 业务,集群,模块ID 过滤 - - :param username: 查询者用户名 - :param bk_biz_id: 业务 ID - :param bk_set_ids: 集群 ID 列表 - :parma bk_module_ids: 模块 ID 列表 - :return: 主机列表 - """ - total = fetch_host_count_by_topo(username, bk_biz_id, bk_set_ids, bk_module_ids) - if not total: - return [] - - default_params = { - 'bk_biz_id': bk_biz_id, - 'bk_set_ids': bk_set_ids, - 'bk_module_ids': bk_module_ids, - 'fields': DEFAULT_HOST_FIELDS, - } - tasks = [] - for start in range(DEFAULT_START_AT, total, CMDB_LIST_HOSTS_MAX_LIMIT): - params = deepcopy(default_params) - params['page'] = {'start': start, 'limit': CMDB_LIST_HOSTS_MAX_LIMIT} - # 组装并行任务配置信息 - tasks.append(functools.partial(cmdb_base_request, FUNC_PATH_MAP['list_biz_hosts'], username, params)) - - results = async_run(tasks) - # 任何一次请求不成功,都是不成功 - for r in results: - if r.ret['code'] != ErrorCode.NoError: - raise error_codes.APIError(r.ret['message']) - - # 所有的请求结果合并,即为全量数据 - return list(chain(*[r.ret['data']['info'] for r in results])) - - -class BkCCConfig: - """蓝鲸配置平台配置信息,提供后续使用的host, url等""" - - def __init__(self, host: str): - # 请求域名 - self.host = host - - # 请求地址 - self.search_biz_url = f"{host}/{PREFIX_PATH}/v2/cc/search_business/" - - -class BkCCAuth(AuthBase): - """用于蓝鲸配置平台接口的鉴权校验""" - - def __init__(self, username: str, bk_supplier_account: Optional[str] = DEFAULT_SUPPLIER_ACCOUNT): - self.bk_app_code = settings.BCS_APP_CODE - self.bk_app_secret = settings.BCS_APP_SECRET - self.operator = username - self.bk_username = username - self.bk_supplier_account = bk_supplier_account - - def __call__(self, r: PreparedRequest): - data = { - "bk_app_code": self.bk_app_code, - "bk_app_secret": self.bk_app_secret, - "bk_username": self.bk_username, - "operator": self.operator, - } - if self.bk_supplier_account: - data["bk_supplier_account"] = self.bk_supplier_account - r.body = update_request_body(r.body, data) - return r - - -@dataclass -class PageData: - start: int = DEFAULT_START_AT - limit: int = CMDB_MAX_LIMIT - sort: str = "" # 排序字段 - - -class BkCCClient(BkApiClient): - def __init__(self, username: str, bk_supplier_account: Optional[str] = DEFAULT_SUPPLIER_ACCOUNT): - self._config = BkCCConfig(host=settings.COMPONENT_HOST) - self._client = BaseHttpClient(BkCCAuth(username, bk_supplier_account=bk_supplier_account)) - - @response_handler(default_data={}) - def search_biz(self, page: PageData, fields: Optional[List] = None, condition: Optional[Dict] = None) -> Dict: - """获取业务信息 - :param page: 分页条件 - :param fields: 返回的字段 - :param condition: 查询条件 - :returns: 返回业务信息,格式:{'count': 1, 'info': [{'id': 1}]} - """ - url = self._config.search_biz_url - data = asdict(page) - data.update({"fields": fields, "condition": condition}) - return self._client.request_json("POST", url, json=data) - - -# 加载cc_ext的函数 -try: - from .cc_ext import * # noqa -except ImportError as e: - logger.debug("Load extension failed: %s", e) diff --git a/bcs-app/backend/components/cc/__init__.py b/bcs-app/backend/components/cc/__init__.py new file mode 100644 index 000000000..419f7fd72 --- /dev/null +++ b/bcs-app/backend/components/cc/__init__.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community +Edition) 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. +""" +import logging + +logger = logging.getLogger(__name__) + +# 仅包含外部模块使用的方法,导入示例: from backend.components.cc import xxx +from .business import AppQueryService, fetch_has_maintain_perm_apps, get_app_maintainers, get_application_name # noqa +from .hosts import BizTopoQueryService, HostQueryService, get_has_perm_hosts # noqa diff --git a/bcs-app/backend/components/cc/business.py b/bcs-app/backend/components/cc/business.py new file mode 100644 index 000000000..a8f906aa8 --- /dev/null +++ b/bcs-app/backend/components/cc/business.py @@ -0,0 +1,124 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community +Edition) 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. + +业务相关:business / application / app +""" +import functools +import logging +import re +from typing import Dict, List + +from django.conf import settings +from django.utils.translation import ugettext_lazy as _ + +from backend.components.base import CompParseBkCommonResponseError +from backend.components.cc import constants +from backend.components.cc.client import BkCCClient, PageData +from backend.components.exceptions import BaseCompUtilError, ResourceNotFoundError +from backend.utils.async_run import AsyncRunException, async_run + +logger = logging.getLogger(__name__) + + +class AppQueryService: + """ 业务查询相关服务 """ + + def __init__( + self, + username: str, + fields: List = None, + condition: Dict = None, + bk_supplier_account: str = settings.BKCC_DEFAULT_SUPPLIER_ACCOUNT, + ): + """ + :param username: 查询者用户名 + :param fields: 指定字段 + :param condition: 查询条件 + :param bk_supplier_account: 供应商 + """ + self.cc_client = BkCCClient(username) + self.fields = fields + self.condition = condition + self.bk_supplier_account = bk_supplier_account + + def _fetch_count(self) -> int: + """ 查询指定条件下业务数量,用于后续并发查询用 """ + page = PageData(start=constants.DEFAULT_START_AT, limit=constants.LIMIT_FOR_COUNT) + resp_data = self.cc_client.search_business(page, ['bk_biz_id'], self.condition, self.bk_supplier_account) + return resp_data['count'] + + def fetch_all(self) -> List[Dict]: + """ 并发查询 CMDB,获取符合条件的全量业务信息 """ + total = self._fetch_count() + tasks = [] + for start in range(constants.DEFAULT_START_AT, total, constants.CMDB_MAX_LIMIT): + # 组装并行任务配置信息 + tasks.append( + functools.partial( + self.cc_client.search_business, + PageData(start=start, limit=constants.CMDB_MAX_LIMIT), + self.fields, + self.condition, + self.bk_supplier_account, + ) + ) + + try: + results = async_run(tasks) + except AsyncRunException as e: + raise CompParseBkCommonResponseError(None, _('根据条件查询全量业务失败:{}').format(e)) + + # 所有的请求结果合并,即为全量数据 + return [app for r in results for app in r.ret['info']] + + def get(self, bk_biz_id: int) -> Dict: + """ 获取单个业务信息 """ + resp_data = self.cc_client.search_business( + PageData(), + fields=self.fields, + condition={'bk_biz_id': bk_biz_id}, + bk_supplier_account=self.bk_supplier_account, + ) + if not (resp_data['count'] and resp_data['info']): + raise ResourceNotFoundError(_('ID 为 {} 的业务不存在').format(bk_biz_id)) + return resp_data['info'][0] + + +def fetch_has_maintain_perm_apps(username: str) -> List[Dict]: + """ 获取有运维权限的业务信息 """ + username_regex_info = '^{username},|,{username},|,{username}$|^{username}$'.format(username=username) + regex_map = {'$regex': username_regex_info} + # NOTE: CMDB 建议查询方式: 以 admin 用户身份跳过资源查询权限,然后CMDB接口根据传递的 condition 中用户,返回过滤的业务 + maintainers_resp = AppQueryService(settings.ADMIN_USERNAME, condition={'bk_biz_maintainer': regex_map}).fetch_all() + return [{'id': item['bk_biz_id'], 'name': item['bk_biz_name']} for item in maintainers_resp] + + +def get_application_name(bk_biz_id: int) -> str: + """ 通过业务ID,获取业务名称 """ + try: + # 直接使用 admin 用户身份查询业务名称 + app_info = AppQueryService(settings.ADMIN_USERNAME).get(bk_biz_id) + except BaseCompUtilError: + return '' + return app_info.get('bk_biz_name', '') + + +def get_app_maintainers(username: str, bk_biz_id: int) -> List[str]: + """ 获取业务下的所有运维 """ + try: + app_info = AppQueryService(username).get(bk_biz_id) + except BaseCompUtilError: + return [] + maintainers = app_info.get('bk_biz_maintainer', '') + return re.findall(r"[^,;]+", maintainers) diff --git a/bcs-app/backend/components/cc/client.py b/bcs-app/backend/components/cc/client.py new file mode 100644 index 000000000..b90f643e7 --- /dev/null +++ b/bcs-app/backend/components/cc/client.py @@ -0,0 +1,168 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community +Edition) 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. +""" +import logging +from typing import Dict, List, Optional + +from attr import asdict, dataclass +from django.conf import settings +from requests import PreparedRequest +from requests.auth import AuthBase + +from backend.components.base import BaseHttpClient, BkApiClient, response_handler, update_request_body +from backend.components.cc import constants + +logger = logging.getLogger(__name__) + + +@dataclass +class PageData: + start: int = constants.DEFAULT_START_AT + limit: int = constants.CMDB_MAX_LIMIT + sort: str = "" # 排序字段 + + +class BkCCConfig: + """ 蓝鲸配置平台配置信息,提供后续使用的host, url等 """ + + def __init__(self, host: str): + # 请求域名 + self.host = host + self.prefix_path = 'api/c/compapi/v2/cc' + + # 请求地址 + # 查询业务信息 + self.search_business_url = f'{host}/{self.prefix_path}/search_business/' + # 查询业务拓扑 + self.search_biz_inst_topo_url = f'{host}/{self.prefix_path}/search_biz_inst_topo/' + # 查询内部模块拓扑 + self.get_biz_internal_module_url = f'{host}/{self.prefix_path}/get_biz_internal_module/' + # 查询业务下主机 + self.list_biz_hosts_url = f'{host}/{self.prefix_path}/list_biz_hosts/' + + +class BkCCAuth(AuthBase): + """用于蓝鲸配置平台接口的鉴权校验""" + + def __init__(self, username: str, bk_supplier_account: Optional[str] = settings.BKCC_DEFAULT_SUPPLIER_ACCOUNT): + self.bk_app_code = settings.BCS_APP_CODE + self.bk_app_secret = settings.BCS_APP_SECRET + self.operator = username + self.bk_username = username + self.bk_supplier_account = bk_supplier_account + + def __call__(self, r: PreparedRequest): + data = { + "bk_app_code": self.bk_app_code, + "bk_app_secret": self.bk_app_secret, + "bk_username": self.bk_username, + "operator": self.operator, + } + if self.bk_supplier_account: + data["bk_supplier_account"] = self.bk_supplier_account + r.body = update_request_body(r.body, data) + return r + + +class BkCCClient(BkApiClient): + """ CMDB API SDK """ + + def __init__(self, username: str, bk_supplier_account: Optional[str] = settings.BKCC_DEFAULT_SUPPLIER_ACCOUNT): + self._config = BkCCConfig(host=settings.COMPONENT_HOST) + self._client = BaseHttpClient(BkCCAuth(username, bk_supplier_account=bk_supplier_account)) + + @response_handler(default=dict) + def search_business( + self, + page: PageData, + fields: Optional[List] = None, + condition: Optional[Dict] = None, + bk_supplier_account: Optional[str] = None, + ) -> Dict: + """ + 获取业务信息 + + :param page: 分页条件 + :param fields: 返回的字段 + :param condition: 查询条件 + :parma bk_supplier_account: 供应商 + :return: 返回业务信息,格式:{'count': 1, 'info': [{'id': 1}]} + """ + url = self._config.search_business_url + params = { + 'page': asdict(page), + 'fields': fields, + 'condition': condition, + 'bk_supplier_account': bk_supplier_account, + } + return self._client.request_json("POST", url, json=params) + + @response_handler(default=list) + def search_biz_inst_topo(self, bk_biz_id: int) -> List: + """ + 获取业务拓扑信息 + + :param bk_biz_id: 业务 ID + :return: 业务拓扑信息 + """ + url = self._config.search_biz_inst_topo_url + params = {'bk_biz_id': bk_biz_id} + return self._client.request_json('POST', url, json=params) + + @response_handler(default=dict) + def get_biz_internal_module(self, bk_biz_id: int) -> Dict: + """ + 查询内部模块拓扑 + + :param bk_biz_id: 业务 ID + :return: 内部模块拓扑信息 + """ + url = self._config.get_biz_internal_module_url + params = {'bk_biz_id': bk_biz_id} + return self._client.request_json('POST', url, json=params) + + @response_handler(default=dict) + def list_biz_hosts( + self, + bk_biz_id: int, + page: PageData, + bk_set_ids: List, + bk_module_ids: List, + fields: List, + host_property_filter: Optional[Dict] = None, + bk_supplier_account: str = None, + ) -> Dict: + """ + 获取业务主机信息 + + :param bk_biz_id: 业务 ID + :param page: 分页配置 + :param bk_set_ids: 集群 ID 列表 + :param bk_module_ids: 模块 ID 列表 + :param fields: 指定的字段信息 + :param host_property_filter: 主机属性组合查询条件 + :param bk_supplier_account: 供应商 + :return: 主机信息,格式:{'count': 1, 'info': [{'bk_host_innerip': '127.0.0.1'}]} + """ + url = self._config.list_biz_hosts_url + params = { + 'bk_biz_id': bk_biz_id, + 'page': asdict(page), + 'bk_set_ids': bk_set_ids, + 'bk_module_ids': bk_module_ids, + 'fields': fields, + 'host_property_filter': host_property_filter, + 'bk_supplier_account': bk_supplier_account, + } + return self._client.request_json('POST', url, json=params) diff --git a/bcs-app/backend/tests/testing_utils/mocks/bk_cc.py b/bcs-app/backend/components/cc/constants.py similarity index 50% rename from bcs-app/backend/tests/testing_utils/mocks/bk_cc.py rename to bcs-app/backend/components/cc/constants.py index 3da1b4308..e6d09cf96 100644 --- a/bcs-app/backend/tests/testing_utils/mocks/bk_cc.py +++ b/bcs-app/backend/components/cc/constants.py @@ -12,28 +12,41 @@ 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. """ -from typing import Dict, List, Optional +# 默认查询主机字段 +DEFAULT_HOST_FIELDS = [ + 'bk_bak_operator', + 'classify_level_name', + 'svr_device_class', + 'bk_svr_type_id', + 'svr_type_name', + 'hard_memo', + 'bk_host_id', + 'bk_host_name', + 'idc_name', + 'bk_idc_area', + 'bk_idc_area_id', + 'idc_id', + 'idc_unit_name', + 'idc_unit_id', + 'bk_host_innerip', + 'bk_comment', + 'module_name', + 'operator', + 'bk_os_name', + 'bk_os_version', + 'bk_host_outerip', + 'rack', + 'bk_cloud_id', +] -from backend.components.cc import PageData +# 默认从 0 开始查询 +DEFAULT_START_AT = 0 +# 用于查询 count 的 Limit,最小为 1 +LIMIT_FOR_COUNT = 1 -class FakeBkCCClient: - def __init__(self, *args, **kwargs): - pass +# CMDB 通用的最大 Limit 限制 +CMDB_MAX_LIMIT = 200 - def search_biz(self, page: PageData, fields: Optional[List] = None, condition: Optional[Dict] = None) -> Dict: - return { - "count": 1, - "info": [ - { - "bs2_name_id": 1, - "bk_oper_plan": "admin", - "bk_biz_developer": "admin", - "bk_biz_maintainer": "admin", - "bk_dept_name_id": 1, - "bk_biz_name": "demo", - "bk_product_name": "demo", - "default": 0, - } - ], - } +# CMDB 请求主机列表最大 Limit 限制 +CMDB_LIST_HOSTS_MAX_LIMIT = 500 diff --git a/bcs-app/backend/components/cc/hosts.py b/bcs-app/backend/components/cc/hosts.py new file mode 100644 index 000000000..48c9af87b --- /dev/null +++ b/bcs-app/backend/components/cc/hosts.py @@ -0,0 +1,178 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community +Edition) 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. +""" +import functools +import logging +from typing import Dict, List + +from django.conf import settings +from django.utils.translation import ugettext_lazy as _ + +from backend.components.base import CompParseBkCommonResponseError +from backend.components.cc import constants +from backend.components.cc.business import get_app_maintainers +from backend.components.cc.client import BkCCClient, PageData +from backend.utils.async_run import AsyncRunException, async_run + +logger = logging.getLogger(__name__) + + +class HostQueryService: + """ 主机查询相关服务 """ + + def __init__( + self, + username: str, + bk_biz_id: int, + bk_set_ids: List = None, + bk_module_ids: List = None, + host_property_filter: Dict = None, + bk_supplier_account: str = settings.BKCC_DEFAULT_SUPPLIER_ACCOUNT, + ): + """ + :param username: 查询者用户名 + :param bk_biz_id: 业务 ID + :param bk_set_ids: 集群 ID 列表 + :parma bk_module_ids: 模块 ID 列表 + :param host_property_filter: 主机属性组合查询条件 + :param bk_supplier_account: 供应商 + """ + self.cc_client = BkCCClient(username) + self.bk_biz_id = bk_biz_id + self.bk_set_ids = bk_set_ids + self.bk_module_ids = bk_module_ids + self.host_property_filter = host_property_filter + self.bk_supplier_account = bk_supplier_account + + def _fetch_count(self) -> int: + """ 查询指定条件下主机数量 """ + resp_data = self.cc_client.list_biz_hosts( + self.bk_biz_id, + PageData(start=constants.DEFAULT_START_AT, limit=constants.LIMIT_FOR_COUNT), + self.bk_set_ids, + self.bk_module_ids, + ['bk_host_innerip'], + self.host_property_filter, + self.bk_supplier_account, + ) + return resp_data['count'] + + def fetch_all(self) -> List[Dict]: + """ + 并发查询 CMDB,获取符合条件的全量主机信息 + + :return: 主机列表 + """ + total = self._fetch_count() + tasks = [] + for start in range(constants.DEFAULT_START_AT, total, constants.CMDB_LIST_HOSTS_MAX_LIMIT): + # 组装并行任务配置信息 + tasks.append( + functools.partial( + self.cc_client.list_biz_hosts, + self.bk_biz_id, + PageData( + start=start, + limit=constants.CMDB_LIST_HOSTS_MAX_LIMIT, + ), + self.bk_set_ids, + self.bk_module_ids, + constants.DEFAULT_HOST_FIELDS, + self.host_property_filter, + self.bk_supplier_account, + ) + ) + + try: + results = async_run(tasks) + except AsyncRunException as e: + raise CompParseBkCommonResponseError(None, _('根据条件查询业务全量主机失败:{}').format(e)) + + # 所有的请求结果合并,即为全量数据 + return [host for r in results for host in r.ret['info']] + + +class BizTopoQueryService: + """ 业务拓扑信息查询 """ + + def __init__(self, username: str, bk_biz_id: int): + """ + :param username: 用户名 + :param bk_biz_id: 业务 ID + """ + self.cc_client = BkCCClient(username) + self.bk_biz_id = bk_biz_id + + def _fetch_biz_inst_topo(self) -> List: + """ + 查询业务拓扑 + + :return: 业务,集群,模块拓扑信息 + """ + return self.cc_client.search_biz_inst_topo(self.bk_biz_id) + + def _fetch_biz_internal_module(self) -> Dict: + """ + 查询业务的内部模块 + + :return: 业务的空闲机/故障机/待回收模块 + """ + return self.cc_client.get_biz_internal_module(self.bk_biz_id) + + def fetch(self) -> List: + """ + 查询全量业务拓扑 + + :return: 全量业务拓扑(包含普通拓扑,内部模块) + """ + biz_inst_topo = self._fetch_biz_inst_topo() + raw_inner_mod_topo = self._fetch_biz_internal_module() + # topo 最外层为业务,如果首个业务存在即为查询结果 + if biz_inst_topo and raw_inner_mod_topo: + # 将内部模块补充到业务下属集群首位 + inner_mod_topo = { + 'bk_obj_id': 'set', + 'bk_obj_name': _('集群'), + 'bk_inst_id': raw_inner_mod_topo['bk_set_id'], + 'bk_inst_name': raw_inner_mod_topo['bk_set_name'], + 'child': [ + { + 'bk_obj_id': 'module', + 'bk_obj_name': _('模块'), + 'bk_inst_id': mod['bk_module_id'], + 'bk_inst_name': mod['bk_module_name'], + 'child': [], + } + for mod in raw_inner_mod_topo['module'] + ], + } + biz_inst_topo[0]['child'].insert(0, inner_mod_topo) + + return biz_inst_topo + + +def get_has_perm_hosts(bk_biz_id: int, username: str) -> List: + """ 查询业务下有权限的主机 """ + all_maintainers = get_app_maintainers(username, bk_biz_id) + if username in all_maintainers: + # 如果是业务运维,查询全量主机 + return HostQueryService(username, bk_biz_id).fetch_all() + # 否则查询有主机负责人权限的主机 + return _get_hosts_by_operator(bk_biz_id, username) + + +def _get_hosts_by_operator(bk_biz_id: int, username: str) -> List: + """ 获取 指定用户 在业务下为 主备负责人 的机器 """ + host_list = HostQueryService(username, bk_biz_id).fetch_all() + return [h for h in host_list if username in [h.get('operator'), h.get('bk_bak_operator')]] diff --git a/bcs-app/backend/components/exceptions.py b/bcs-app/backend/components/exceptions.py new file mode 100644 index 000000000..e59aa0341 --- /dev/null +++ b/bcs-app/backend/components/exceptions.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community +Edition) 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. +""" +from .base import BaseCompError + + +class BaseCompUtilError(BaseCompError): + """ Component 工具类基础异常类 """ + + message = 'Component util exception' + + def __init__(self, message=None): + """ 初始化异常类,若无参数则使用默认值 """ + if message: + self.message = message + + def __str__(self): + return self.message + + +class ResourceNotFoundError(BaseCompUtilError): + """ 资源不存在 """ + + message = 'Resource Not Found' diff --git a/bcs-app/backend/components/paas_auth.py b/bcs-app/backend/components/paas_auth.py index 29d7c3b16..7beeb0b64 100644 --- a/bcs-app/backend/components/paas_auth.py +++ b/bcs-app/backend/components/paas_auth.py @@ -14,7 +14,7 @@ """ import logging -from backend.bcs_web.iam import permissions +from backend.iam import legacy_perms as permissions from .ssm import get_client_access_token diff --git a/bcs-app/backend/components/paas_cc.py b/bcs-app/backend/components/paas_cc.py index 80997f625..2cd6ca5c2 100644 --- a/bcs-app/backend/components/paas_cc.py +++ b/bcs-app/backend/components/paas_cc.py @@ -20,10 +20,10 @@ from django.conf import settings from django.utils.translation import ugettext_lazy as _ -from backend.bcs_web.iam import permissions from backend.components.base import BaseHttpClient, BkApiClient, ComponentAuth, response_handler from backend.components.utils import http_delete, http_get, http_patch, http_post, http_put from backend.container_service.clusters.models import CommonStatus +from backend.iam import legacy_perms as permissions from backend.utils.basic import getitems from backend.utils.decorators import parse_response_data from backend.utils.errcodes import ErrorCode @@ -523,6 +523,7 @@ def __init__(self, host: str): # PaaSCC 系统接口地址 self.get_cluster_url = f"{host}/projects/{{project_id}}/clusters/{{cluster_id}}" + self.get_cluster_by_id_url = f"{host}/clusters/{{cluster_id}}/" self.get_project_url = f"{host}/projects/{{project_id}}/" self.update_cluster_url = f"{host}/projects/{{project_id}}/clusters/{{cluster_id}}/" self.delete_cluster_url = f"{host}/projects/{{project_id}}/clusters/{{cluster_id}}/" @@ -552,6 +553,12 @@ def get_cluster(self, project_id: str, cluster_id: str) -> Dict: url = self._config.get_cluster_url.format(project_id=project_id, cluster_id=cluster_id) return self._client.request_json('GET', url) + @response_handler() + def get_cluster_by_id(self, cluster_id: str) -> Dict: + """根据集群ID获取集群信息""" + url = self._config.get_cluster_by_id_url.format(cluster_id=cluster_id) + return self._client.request_json('GET', url) + @parse_response_data() def get_project(self, project_id: str) -> Dict: """获取项目信息""" diff --git a/bcs-app/backend/components/sops.py b/bcs-app/backend/components/sops.py index 093d4be23..efbd22f20 100644 --- a/bcs-app/backend/components/sops.py +++ b/bcs-app/backend/components/sops.py @@ -71,7 +71,7 @@ def __init__(self): self._config = SopsConfig(host=settings.SOPS_API_HOST) self._client = BaseHttpClient(SopsAuth()) - @response_handler(default_data={}) + @response_handler(default=dict) def create_task(self, bk_biz_id: str, template_id: str, data: CreateTaskParams) -> Dict: """通过业务流程创建任务 @@ -83,7 +83,7 @@ def create_task(self, bk_biz_id: str, template_id: str, data: CreateTaskParams) url = self._config.create_task_url.format(template_id=template_id, bk_biz_id=bk_biz_id) return self._client.request_json("POST", url, json=asdict(data)) - @response_handler(default_data={}) + @response_handler(default=dict) def start_task(self, bk_biz_id: str, task_id: str) -> Dict: """启动任务 @@ -94,7 +94,7 @@ def start_task(self, bk_biz_id: str, task_id: str) -> Dict: url = self._config.start_task_url.format(task_id=task_id, bk_biz_id=bk_biz_id) return self._client.request_json("POST", url) - @response_handler(default_data={}) + @response_handler(default=dict) def get_task_status(self, bk_biz_id: str, task_id: str) -> Dict: """获取任务状态 @@ -105,7 +105,7 @@ def get_task_status(self, bk_biz_id: str, task_id: str) -> Dict: url = self._config.get_task_status_url.format(task_id=task_id, bk_biz_id=bk_biz_id) return self._client.request_json("GET", url) - @response_handler(default_data={}) + @response_handler(default=dict) def get_task_node_data(self, bk_biz_id: str, task_id: str, node_id: str) -> Dict: """获取任务步骤的详情 diff --git a/bcs-app/backend/container_service/clusters/cc_host/utils.py b/bcs-app/backend/container_service/clusters/cc_host/utils.py index 53b4ad4cb..428e7f542 100644 --- a/bcs-app/backend/container_service/clusters/cc_host/utils.py +++ b/bcs-app/backend/container_service/clusters/cc_host/utils.py @@ -31,7 +31,7 @@ def fetch_cc_app_hosts(username, bk_biz_id, bk_module_id=None, bk_set_id=None) - """ bk_module_ids = [bk_module_id] if bk_module_id else None bk_set_ids = [bk_set_id] if bk_set_id else None - return cc.list_all_hosts_by_topo(username, bk_biz_id, bk_module_ids, bk_set_ids) + return cc.HostQueryService(username, bk_biz_id, bk_module_ids, bk_set_ids).fetch_all() def fetch_all_cluster_nodes(access_token: str) -> Dict: diff --git a/bcs-app/backend/container_service/clusters/cc_host/views.py b/bcs-app/backend/container_service/clusters/cc_host/views.py index 0afb9ebee..726a8cdb1 100644 --- a/bcs-app/backend/container_service/clusters/cc_host/views.py +++ b/bcs-app/backend/container_service/clusters/cc_host/views.py @@ -20,7 +20,9 @@ from backend.bcs_web.viewsets import SystemViewSet from backend.components import cc +from backend.components.base import BaseCompError, CompParseBkCommonResponseError from backend.container_service.clusters.serializers import FetchCCHostSLZ +from backend.utils.error_codes import error_codes from backend.utils.filter import filter_by_ips from backend.utils.paginator import custom_paginator @@ -35,27 +37,13 @@ class CCViewSet(SystemViewSet): @action(methods=['GET'], url_path='topology', detail=False) def biz_inst_topo(self, request, project_id): """ 查询业务实例拓扑 """ - topo_info = cc.search_biz_inst_topo(request.user.username, request.project.cc_app_id) - raw_inner_mod_topo = cc.get_biz_internal_module(request.user.username, request.project.cc_app_id) - # topo 最外层为业务,如果存在首个业务即为查询的结果 - if topo_info and raw_inner_mod_topo: - inner_mod_topo = { - 'bk_obj_id': 'set', - 'bk_obj_name': _('集群'), - 'bk_inst_id': raw_inner_mod_topo['bk_set_id'], - 'bk_inst_name': raw_inner_mod_topo['bk_set_name'], - 'child': [ - { - 'bk_obj_id': 'module', - 'bk_obj_name': _('模块'), - 'bk_inst_id': mod['bk_module_id'], - 'bk_inst_name': mod['bk_module_name'], - 'child': [], - } - for mod in raw_inner_mod_topo['module'] - ], - } - topo_info[0]['child'].insert(0, inner_mod_topo) + try: + topo_info = cc.BizTopoQueryService(request.user.username, request.project.cc_app_id).fetch() + except CompParseBkCommonResponseError as e: + raise error_codes.ComponentError(_('查询业务拓扑信息失败:{}').format(e)) + except BaseCompError as e: + logger.error('查询业务拓扑信息失败:%s', e) + raise error_codes.ComponentError(_('发生未知错误,请稍候再试')) return Response(data=topo_info) @action(methods=['POST'], url_path='hosts', detail=False) @@ -67,8 +55,14 @@ def hosts(self, request, project_id): bk_biz_id = request.project.cc_app_id # 从 CMDB 获取可用主机信息,业务名称信息 - host_list = utils.fetch_cc_app_hosts(username, bk_biz_id, params['set_id'], params['module_id']) - cc_app_name = cc.get_application_name(username, bk_biz_id) + try: + host_list = utils.fetch_cc_app_hosts(username, bk_biz_id, params['set_id'], params['module_id']) + except CompParseBkCommonResponseError as e: + raise error_codes.ComponentError(str(e)) + except BaseCompError as e: + logger.error('获取主机信息失败:%s', e) + raise error_codes.ComponentError(_('发生未知错误,请稍候再试')) + cc_app_name = cc.get_application_name(bk_biz_id) # 根据指定的 IP 过滤 host_list = filter_by_ips(host_list, params['ip_list'], key='bk_host_innerip', fuzzy=params['fuzzy']) diff --git a/bcs-app/backend/container_service/clusters/constants.py b/bcs-app/backend/container_service/clusters/constants.py index d1914e790..bdceb1037 100644 --- a/bcs-app/backend/container_service/clusters/constants.py +++ b/bcs-app/backend/container_service/clusters/constants.py @@ -15,7 +15,7 @@ import re from collections import OrderedDict -from django.utils.translation import ugettext as _ +from django.utils.translation import ugettext_lazy as _ from backend.packages.blue_krill.data_types.enum import EnumField, StructuredEnum from backend.utils.basic import ChoicesEnum diff --git a/bcs-app/backend/container_service/clusters/flow_views/tools/cmdb/__init__.py b/bcs-app/backend/container_service/clusters/flow_views/tools/cmdb/__init__.py deleted file mode 100644 index 0902e2566..000000000 --- a/bcs-app/backend/container_service/clusters/flow_views/tools/cmdb/__init__.py +++ /dev/null @@ -1,35 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community -Edition) 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. -""" -from backend.components import cc -from backend.utils.error_codes import error_codes - - -class CMDBClient: - def __init__(self, request): - self.request = request - self.cc_app_id = request.project.cc_app_id - self.username = request.user.username - - def get_cc_hosts(self): - cc_host_info = cc.get_app_hosts(self.request.user.username, self.cc_app_id) - if not cc_host_info.get("result"): - raise error_codes.APIError(cc_host_info.get("message")) - return cc_host_info.get("data") or [] - - def get_cc_application_name(self): - return cc.get_application_name(self.username, self.cc_app_id) - - def get_host_base_info(self, inner_ip): - return cc.get_host_base_info(self.username, self.cc_app_id, inner_ip) diff --git a/bcs-app/backend/container_service/clusters/module_apis.py b/bcs-app/backend/container_service/clusters/module_apis.py index 741d80e2e..c9c30ad51 100644 --- a/bcs-app/backend/container_service/clusters/module_apis.py +++ b/bcs-app/backend/container_service/clusters/module_apis.py @@ -30,14 +30,6 @@ def get_cluster_node_mod(): return node -def get_cmdb_mod(): - try: - from backend.container_service.clusters.flow_views_ext.tools import cmdb - except ImportError: - from backend.container_service.clusters.flow_views.tools import cmdb - return cmdb - - def get_gse_mod(): try: from backend.container_service.clusters.flow_views_ext.tools import gse diff --git a/bcs-app/backend/container_service/clusters/urls.py b/bcs-app/backend/container_service/clusters/urls.py index a3b5415f9..ce2d2d0f2 100644 --- a/bcs-app/backend/container_service/clusters/urls.py +++ b/bcs-app/backend/container_service/clusters/urls.py @@ -79,11 +79,6 @@ views.NodeUpdateLogView.as_view({'get': 'get'}), name='api.projects.node_update_log', ), - # 单个节点docker版本信息 - url( - r'^api/projects/(?P[\w\-]+)/cluster/(?P[\w\-]+)/node/info/', - views.NodeInfo.as_view({'get': 'info'}), - ), url( r'^api/projects/(?P[\w\-]+)/cluster/(?P[\w\-]+)/node/containers/', views.NodeContainers.as_view({'get': 'list'}), diff --git a/bcs-app/backend/container_service/clusters/utils.py b/bcs-app/backend/container_service/clusters/utils.py index d02503142..6b2f89e7b 100644 --- a/bcs-app/backend/container_service/clusters/utils.py +++ b/bcs-app/backend/container_service/clusters/utils.py @@ -14,7 +14,7 @@ """ import json from datetime import datetime -from typing import List +from typing import Dict, List from django.conf import settings from django.utils.translation import ugettext_lazy as _ @@ -89,20 +89,17 @@ def can_use_hosts(bk_biz_id: int, username: str, host_ips: List): raise PermissionDeniedError(_("用户{}没有主机:{}的权限,请联系管理员在【配置平台】添加为业务运维人员角色").format(username, host_ips), "") -def get_cmdb_hosts(username, cc_app_id_list, host_property_filter): +def get_cmdb_hosts(username: str, cc_app_id: int, host_property_filter: Dict) -> List: + """ + 根据指定条件获取 CMDB 主机信息,包含字段映射等转换 + """ hosts = [] - for app_id in cc_app_id_list: - resp = cc.list_biz_hosts(username, int(app_id), host_property_filter=host_property_filter) - if resp.get("data"): - hosts = resp["data"] - break - cluster_masters = [] - for info in hosts: + for info in cc.HostQueryService(username, cc_app_id, host_property_filter=host_property_filter).fetch_all(): convert_host = convert_mappings(CCHostKeyMappings, info) convert_host["agent"] = AGENT_NORMAL_STATUS - cluster_masters.append(convert_host) + hosts.append(convert_host) - return cluster_masters + return hosts def use_tke(coes=None, access_token=None, project_id=None, cluster_id=None): diff --git a/bcs-app/backend/container_service/clusters/views/__init__.py b/bcs-app/backend/container_service/clusters/views/__init__.py index 3f28ba92f..88162e6e2 100644 --- a/bcs-app/backend/container_service/clusters/views/__init__.py +++ b/bcs-app/backend/container_service/clusters/views/__init__.py @@ -33,7 +33,6 @@ NodeCreateListViewSet, NodeForceDeleteViewSet, NodeGetUpdateDeleteViewSet, - NodeInfo, NodeLabelListViewSet, NodeLabelQueryCreateViewSet, NodeUpdateLogView, diff --git a/bcs-app/backend/container_service/clusters/views/cluster.py b/bcs-app/backend/container_service/clusters/views/cluster.py index 816f207ce..a08bd70b9 100644 --- a/bcs-app/backend/container_service/clusters/views/cluster.py +++ b/bcs-app/backend/container_service/clusters/views/cluster.py @@ -414,9 +414,7 @@ def cluster_masters(self, request, project_id, cluster_id): ], } username = settings.ADMIN_USERNAME - cluster_masters = get_cmdb_hosts( - username, [request.project.cc_app_id, settings.BCS_APP_ID], host_property_filter - ) + cluster_masters = get_cmdb_hosts(username, request.project.cc_app_id, host_property_filter) return response.Response(cluster_masters) diff --git a/bcs-app/backend/container_service/clusters/views/node.py b/bcs-app/backend/container_service/clusters/views/node.py index 6727cc356..e13215485 100644 --- a/bcs-app/backend/container_service/clusters/views/node.py +++ b/bcs-app/backend/container_service/clusters/views/node.py @@ -37,7 +37,7 @@ from backend.container_service.clusters.base.utils import get_cluster_nodes from backend.container_service.clusters.driver.k8s import K8SDriver from backend.container_service.clusters.models import CommonStatus, NodeLabel, NodeStatus, NodeUpdateLog -from backend.container_service.clusters.module_apis import get_cluster_node_mod, get_cmdb_mod, get_gse_mod +from backend.container_service.clusters.module_apis import get_cluster_node_mod, get_gse_mod from backend.container_service.clusters.tools.node import query_cluster_nodes from backend.container_service.clusters.utils import cluster_env_transfer, status_transfer from backend.utils.errcodes import ErrorCode @@ -47,7 +47,6 @@ # 导入相应模块 node = get_cluster_node_mod() -cmdb = get_cmdb_mod() gse = get_gse_mod() logger = logging.getLogger(__name__) @@ -457,41 +456,6 @@ def get(self, request, project_id, cluster_id, node_id): return Response(data) -class NodeInfo(NodeBase, viewsets.ViewSet): - renderer_classes = (BKAPIRenderer, BrowsableAPIRenderer) - - def get_node_id(self, inner_ip, node_list): - """get node id by bcs cc""" - node_id = 0 - for info in node_list: - if info['inner_ip'] != inner_ip: - continue - node_id = info['id'] - break - if not node_id: - raise error_codes.CheckFailed(f'inner_ip[{inner_ip}] not found') - return node_id - - def info(self, request, project_id, cluster_id): - """get host info by cmdb""" - self.can_view_cluster(request, project_id, cluster_id) - inner_ip = request.GET.get('res_id') - if not inner_ip: - raise error_codes.APIError('params[res_id] is null') - # get node list, compatible logic - try: - node_data = self.get_node_list(request, project_id, cluster_id) - node_list = node_data.get("results") or [] - except Exception as err: - logger.error('get node error, %s', err) - return Response([]) - node_id = self.get_node_id(inner_ip, node_list) - data = cmdb.CMDBClient(request).get_host_base_info(inner_ip) - # provider can only be CMDB - data.update({'provider': 'CMDB', 'id': node_id}) - return Response(data) - - class NodeContainers(NodeBase, viewsets.ViewSet): renderer_classes = (BKAPIRenderer, BrowsableAPIRenderer) diff --git a/bcs-app/backend/container_service/infras/hosts/host.py b/bcs-app/backend/container_service/infras/hosts/host.py index 4908ee55a..b2326d8bc 100644 --- a/bcs-app/backend/container_service/infras/hosts/host.py +++ b/bcs-app/backend/container_service/infras/hosts/host.py @@ -12,29 +12,12 @@ 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. """ -from dataclasses import asdict, dataclass, field -from typing import Dict, List, Tuple +from dataclasses import asdict, dataclass +from typing import Dict, List -from backend.components import cc, gse, sops +from backend.components import gse from backend.utils.data_types import make_dataclass_from_dict -from .constants import APPLY_HOST_TEMPLATE_ID, SOPS_BIZ_ID - - -def get_cc_hosts(cc_app_id: str, username: str, **extra_data) -> List[Dict]: - """获取主机信息 - - :param cc_app_id: 业务 ID - :param username: 当前请求的用户名 - :param extra_data: 资源池信息,默认为空 - - :returns: 返回列表,列表中包含主机的属性信息,比如IP、所属区域、机房、机架等 - """ - resp = cc.get_app_hosts(username, cc_app_id) - if not resp.get("result"): - return [] - return resp.get("data") or [] - @dataclass class HostData: diff --git a/bcs-app/backend/container_service/infras/hosts/perms.py b/bcs-app/backend/container_service/infras/hosts/perms.py index a7813b3ca..a534e254b 100644 --- a/bcs-app/backend/container_service/infras/hosts/perms.py +++ b/bcs-app/backend/container_service/infras/hosts/perms.py @@ -15,14 +15,14 @@ import re from typing import List -from backend.components.cc import get_cc_hosts +from backend.components.cc import get_has_perm_hosts def can_use_hosts(bk_biz_id: int, username: str, host_ip_list: List) -> bool: """校验用户是否有使用主机的权限""" - resp = get_cc_hosts(bk_biz_id, username) + has_perm_hosts = get_has_perm_hosts(bk_biz_id, username) ip_list = [] - for info in resp.get("data") or []: + for info in has_perm_hosts: inner_ip = info.get("bk_host_innerip", "") # 因为有多网卡的主机,因此需要拆分,便于后续的判断是否包含 inner_ip_list = re.findall(r'[^;,]+', inner_ip) diff --git a/bcs-app/backend/container_service/projects/base/utils.py b/bcs-app/backend/container_service/projects/base/utils.py index 2dac8ca69..e97611052 100644 --- a/bcs-app/backend/container_service/projects/base/utils.py +++ b/bcs-app/backend/container_service/projects/base/utils.py @@ -26,7 +26,7 @@ def query_projects(access_token, query_params=None): return paas_cc.get_projects(access_token, query_params) -def filter_projects(access_token, query_params=None): +def list_projects(access_token, query_params=None): data = query_projects(access_token, query_params) projects = data.get("results") or [] # 为了兼容导航的参数要求, 增加了project_code字段 diff --git a/bcs-app/backend/container_service/projects/open_apis/views.py b/bcs-app/backend/container_service/projects/open_apis/views.py index 7a92543cc..3f784afac 100644 --- a/bcs-app/backend/container_service/projects/open_apis/views.py +++ b/bcs-app/backend/container_service/projects/open_apis/views.py @@ -16,7 +16,7 @@ from backend.bcs_web.apis.authentication import JWTAuthentication from backend.bcs_web.apis.permissions import AccessTokenPermission -from backend.container_service.projects.base import filter_projects +from backend.container_service.projects.base import list_projects from backend.container_service.projects.views import NavProjectPermissionViewSet from backend.utils.renderers import BKAPIRenderer @@ -27,5 +27,5 @@ class ProjectsViewSet(NavProjectPermissionViewSet): permission_classes = (AccessTokenPermission,) def list_projects(self, request): - projects = filter_projects(request.user.token.access_token) + projects = list_projects(request.user.token.access_token) return Response(projects) diff --git a/bcs-app/backend/container_service/projects/serializers.py b/bcs-app/backend/container_service/projects/serializers.py index 5cf032238..f33b55026 100644 --- a/bcs-app/backend/container_service/projects/serializers.py +++ b/bcs-app/backend/container_service/projects/serializers.py @@ -14,8 +14,8 @@ """ from rest_framework import serializers -from backend.bcs_web.iam.permissions import ProjectActions from backend.container_service.projects.base.constants import ProjectKindID +from backend.iam.legacy_perms import ProjectActions class UpdateProjectNewSLZ(serializers.Serializer): diff --git a/bcs-app/backend/container_service/projects/utils.py b/bcs-app/backend/container_service/projects/utils.py index 032e85a1c..f9303710a 100644 --- a/bcs-app/backend/container_service/projects/utils.py +++ b/bcs-app/backend/container_service/projects/utils.py @@ -32,12 +32,8 @@ def backend_create_depot_path(request, project_id, pre_cc_app_id): logger.error("创建项目仓库路径失败,详细信息: %s" % err) -def get_application_name(request): - return cc.get_application_name(request.user.username, request.project.cc_app_id) - - -def get_app_by_user_role(request): - return cc.get_app_by_user_role(request.user.username) +def fetch_has_maintain_perm_apps(request): + return cc.fetch_has_maintain_perm_apps(request.user.username) def update_bcs_service_for_project(request, project_id, data): diff --git a/bcs-app/backend/container_service/projects/views.py b/bcs-app/backend/container_service/projects/views.py index 079fbb9d7..9d00e848c 100644 --- a/bcs-app/backend/container_service/projects/views.py +++ b/bcs-app/backend/container_service/projects/views.py @@ -24,15 +24,11 @@ from backend.bcs_web.audit_log import client from backend.bcs_web.constants import bcs_project_cache_key -from backend.bcs_web.iam.permissions import ProjectPermission from backend.bcs_web.viewsets import SystemViewSet -from backend.components import paas_cc +from backend.components import cc, paas_cc from backend.container_service.projects import base as Project -from backend.container_service.projects.utils import ( - get_app_by_user_role, - get_application_name, - update_bcs_service_for_project, -) +from backend.container_service.projects.utils import fetch_has_maintain_perm_apps, update_bcs_service_for_project +from backend.iam.legacy_perms import ProjectPermission from backend.utils.basic import normalize_datetime from backend.utils.cache import region from backend.utils.errcodes import ErrorCode @@ -116,7 +112,7 @@ def info(self, request, project_id): data["created_at"], data["updated_at"] ) # 添加业务名称 - data["cc_app_name"] = get_application_name(request) + data["cc_app_name"] = cc.get_application_name(request.project.cc_app_id) data["can_edit"] = self.can_edit(request, project_id) return Response(data) @@ -172,7 +168,7 @@ class CC(viewsets.ViewSet): def list(self, request): """获取当前用户CC列表""" - data = get_app_by_user_role(request) + data = fetch_has_maintain_perm_apps(request) return Response(data) @@ -233,11 +229,11 @@ def filter_projects(self, request): access_token = request.user.token.access_token if project_code: - projects = Project.filter_projects(access_token, {"english_names": project_code}) + projects = Project.list_projects(access_token, {"english_names": project_code}) elif project_name: - projects = Project.filter_projects(access_token, {"project_names": project_name}) + projects = Project.list_projects(access_token, {"project_names": project_name}) else: - projects = Project.filter_projects(access_token) + projects = Project.list_projects(access_token) if not projects: return Response(projects) diff --git a/bcs-app/backend/helm/app/documentation/how-to-push-chart-en.md b/bcs-app/backend/helm/app/documentation/how-to-push-chart-en.md index 43af0b020..f9b3e7d90 100644 --- a/bcs-app/backend/helm/app/documentation/how-to-push-chart-en.md +++ b/bcs-app/backend/helm/app/documentation/how-to-push-chart-en.md @@ -63,6 +63,14 @@ sed -E -i.bak s/version\:\ .+/version\:\ 1\.0\.1/g rumpetroll/Chart.yaml - Push Chart +if the version of `push` plugin is gte 0.10.0, command line is the following: + +``` +helm cm-push rumpetroll/ {{ project_code }} +``` + +else, command line is the following: + ``` helm push rumpetroll/ {{ project_code }} ``` diff --git a/bcs-app/backend/helm/app/documentation/how-to-push-chart.md b/bcs-app/backend/helm/app/documentation/how-to-push-chart.md index 0b77cfdd3..db9967567 100644 --- a/bcs-app/backend/helm/app/documentation/how-to-push-chart.md +++ b/bcs-app/backend/helm/app/documentation/how-to-push-chart.md @@ -52,6 +52,14 @@ tar -xf rumpetroll.tgz - 推送 Chart +如果 `push` 插件版本大于等于0.10.0,必须使用如下命令 + +``` +helm cm-push rumpetroll/ {{ project_code }} +``` + +其它版本,使用如下命令 + ``` helm push rumpetroll/ {{ project_code }} ``` diff --git a/bcs-app/backend/helm/app/views.py b/bcs-app/backend/helm/app/views.py index 168e2a16c..02abe201e 100644 --- a/bcs-app/backend/helm/app/views.py +++ b/bcs-app/backend/helm/app/views.py @@ -36,6 +36,7 @@ from rest_framework.serializers import Serializer from backend.accounts import bcs_perm +from backend.bcs_web.viewsets import SystemViewSet from backend.components import paas_cc from backend.components.bcs import k8s from backend.container_service.misc.bke_client.client import BCSClusterCredentialsNotFound, BCSClusterNotFound @@ -822,7 +823,7 @@ class AppAPIView(viewsets.ModelViewSet): queryset = App.objects.all() -class HowToPushHelmChartView(AccessTokenMixin, viewsets.GenericViewSet): +class HowToPushHelmChartView(SystemViewSet): def get_private_repo_info(self, user, project): private_repo = get_or_create_private_repo(user, project) @@ -868,9 +869,7 @@ def retrieve(self, request, project_id, *args, **kwargs): template = Template(f.read()) content = template.render(**context) - response = HttpResponse(content=content, content_type='text/plain; charset=UTF-8') - response['Content-Disposition'] = 'attachment; filename="how-to-push-helm-chart.md"' - return response + return Response({"content": content}) @with_code_wrapper diff --git a/bcs-app/backend/helm/helm/models/chart.py b/bcs-app/backend/helm/helm/models/chart.py index 5bed816a7..69199a66a 100644 --- a/bcs-app/backend/helm/helm/models/chart.py +++ b/bcs-app/backend/helm/helm/models/chart.py @@ -62,6 +62,7 @@ class Chart(BaseTSModel): annotations = JSONField(default={}) objects = ChartManager() + default_objects = models.Manager() class Meta: unique_together = ('name', 'repository') diff --git a/bcs-app/backend/helm/helm/tasks.py b/bcs-app/backend/helm/helm/tasks.py index fa445d4f5..b741a25a6 100644 --- a/bcs-app/backend/helm/helm/tasks.py +++ b/bcs-app/backend/helm/helm/tasks.py @@ -232,7 +232,7 @@ def _do_helm_repo_charts_update(repo, sign, charts, index_hash, force=False): old_charts = _get_old_charts(repo) for chart_name, versions in charts.items(): - chart, chart_created = Chart.objects.get_or_create(name=chart_name, repository=repo) + chart, chart_created = Chart.default_objects.get_or_create(name=chart_name, repository=repo) chart.clean_deleted_status() # 1. prepare data diff --git a/bcs-app/backend/bcs_web/iam/open_apis/v1/views.py b/bcs-app/backend/iam/__init__.py similarity index 89% rename from bcs-app/backend/bcs_web/iam/open_apis/v1/views.py rename to bcs-app/backend/iam/__init__.py index 59a1e9c91..08f8f6ab8 100644 --- a/bcs-app/backend/bcs_web/iam/open_apis/v1/views.py +++ b/bcs-app/backend/iam/__init__.py @@ -12,8 +12,3 @@ 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. """ -from ..views import ResourceAPIView - - -class ProjectAPIView(ResourceAPIView): - pass diff --git a/bcs-app/backend/bcs_web/iam/open_apis/v1/urls.py b/bcs-app/backend/iam/bcs_iam_migration/__init__.py similarity index 86% rename from bcs-app/backend/bcs_web/iam/open_apis/v1/urls.py rename to bcs-app/backend/iam/bcs_iam_migration/__init__.py index f1bb3b727..08f8f6ab8 100644 --- a/bcs-app/backend/bcs_web/iam/open_apis/v1/urls.py +++ b/bcs-app/backend/iam/bcs_iam_migration/__init__.py @@ -12,8 +12,3 @@ 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. """ -from django.conf.urls import url - -from . import views - -urlpatterns = [url(r"^projects/", views.ProjectAPIView.as_view())] diff --git a/bcs-app/backend/bcs_web/iam/bcs_iam_migration/apps.py b/bcs-app/backend/iam/bcs_iam_migration/apps.py similarity index 92% rename from bcs-app/backend/bcs_web/iam/bcs_iam_migration/apps.py rename to bcs-app/backend/iam/bcs_iam_migration/apps.py index 4618ee720..e745c8562 100644 --- a/bcs-app/backend/bcs_web/iam/bcs_iam_migration/apps.py +++ b/bcs-app/backend/iam/bcs_iam_migration/apps.py @@ -18,4 +18,5 @@ class BcsIamMigrationConfig(AppConfig): - name = "backend.bcs_web.iam.bcs_iam_migration" + name = "backend.iam.bcs_iam_migration" + label = "bcs_iam_migration" diff --git a/bcs-app/backend/bcs_web/iam/bcs_iam_migration/migrations/0001_initial.py b/bcs-app/backend/iam/bcs_iam_migration/migrations/0001_initial.py similarity index 94% rename from bcs-app/backend/bcs_web/iam/bcs_iam_migration/migrations/0001_initial.py rename to bcs-app/backend/iam/bcs_iam_migration/migrations/0001_initial.py index df90f86df..128bc875f 100644 --- a/bcs-app/backend/bcs_web/iam/bcs_iam_migration/migrations/0001_initial.py +++ b/bcs-app/backend/iam/bcs_iam_migration/migrations/0001_initial.py @@ -32,6 +32,4 @@ class Migration(migrations.Migration): dependencies = [] - operations = [ - migrations.RunPython(forward_func) - ] + operations = [migrations.RunPython(forward_func)] diff --git a/bcs-app/backend/iam/bcs_iam_migration/migrations/0002_bk_bcs_app_202108181450.py b/bcs-app/backend/iam/bcs_iam_migration/migrations/0002_bk_bcs_app_202108181450.py new file mode 100644 index 000000000..b869146cd --- /dev/null +++ b/bcs-app/backend/iam/bcs_iam_migration/migrations/0002_bk_bcs_app_202108181450.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community +Edition) 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. +""" +import codecs +import json +import os + +from django.conf import settings +from django.db import migrations +from iam.contrib.iam_migration.migrator import IAMMigrator + + +def forward_func(apps, schema_editor): + + migrator = IAMMigrator(Migration.migration_json) + migrator.migrate() + + +class Migration(migrations.Migration): + migration_json = "0002_project_extra.json" + + dependencies = [('bcs_iam_migration', '0001_initial')] + + operations = [migrations.RunPython(forward_func)] diff --git a/bcs-app/backend/iam/bcs_iam_migration/migrations/0003_bk_bcs_app_202108181523.py b/bcs-app/backend/iam/bcs_iam_migration/migrations/0003_bk_bcs_app_202108181523.py new file mode 100644 index 000000000..e8cbe960a --- /dev/null +++ b/bcs-app/backend/iam/bcs_iam_migration/migrations/0003_bk_bcs_app_202108181523.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community +Edition) 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. +""" +import codecs +import json +import os + +from django.conf import settings +from django.db import migrations +from iam.contrib.iam_migration.migrator import IAMMigrator + + +def forward_func(apps, schema_editor): + + migrator = IAMMigrator(Migration.migration_json) + migrator.migrate() + + +class Migration(migrations.Migration): + migration_json = "0003_cluster.json" + + dependencies = [('bcs_iam_migration', '0002_bk_bcs_app_202108181450')] + + operations = [migrations.RunPython(forward_func)] diff --git a/bcs-app/backend/iam/bcs_iam_migration/migrations/0004_bk_bcs_app_202108181524.py b/bcs-app/backend/iam/bcs_iam_migration/migrations/0004_bk_bcs_app_202108181524.py new file mode 100644 index 000000000..765aa7f2c --- /dev/null +++ b/bcs-app/backend/iam/bcs_iam_migration/migrations/0004_bk_bcs_app_202108181524.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community +Edition) 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. +""" +import codecs +import json +import os + +from django.conf import settings +from django.db import migrations +from iam.contrib.iam_migration.migrator import IAMMigrator + + +def forward_func(apps, schema_editor): + + migrator = IAMMigrator(Migration.migration_json) + migrator.migrate() + + +class Migration(migrations.Migration): + migration_json = "0004_namespace.json" + + dependencies = [('bcs_iam_migration', '0003_bk_bcs_app_202108181523')] + + operations = [migrations.RunPython(forward_func)] diff --git a/bcs-app/backend/iam/bcs_iam_migration/migrations/0005_bk_bcs_app_202108181525.py b/bcs-app/backend/iam/bcs_iam_migration/migrations/0005_bk_bcs_app_202108181525.py new file mode 100644 index 000000000..6887db145 --- /dev/null +++ b/bcs-app/backend/iam/bcs_iam_migration/migrations/0005_bk_bcs_app_202108181525.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community +Edition) 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. +""" +import codecs +import json +import os + +from django.conf import settings +from django.db import migrations +from iam.contrib.iam_migration.migrator import IAMMigrator + + +def forward_func(apps, schema_editor): + + migrator = IAMMigrator(Migration.migration_json) + migrator.migrate() + + +class Migration(migrations.Migration): + migration_json = "0005_templateset.json" + + dependencies = [('bcs_iam_migration', '0004_bk_bcs_app_202108181524')] + + operations = [migrations.RunPython(forward_func)] diff --git a/bcs-app/backend/iam/bcs_iam_migration/migrations/0006_bk_bcs_app_202108181525.py b/bcs-app/backend/iam/bcs_iam_migration/migrations/0006_bk_bcs_app_202108181525.py new file mode 100644 index 000000000..63b5d80b8 --- /dev/null +++ b/bcs-app/backend/iam/bcs_iam_migration/migrations/0006_bk_bcs_app_202108181525.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community +Edition) 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. +""" +import codecs +import json +import os + +from django.conf import settings +from django.db import migrations +from iam.contrib.iam_migration.migrator import IAMMigrator + + +def forward_func(apps, schema_editor): + + migrator = IAMMigrator(Migration.migration_json) + migrator.migrate() + + +class Migration(migrations.Migration): + migration_json = "0006_action_groups.json" + + dependencies = [('bcs_iam_migration', '0005_bk_bcs_app_202108181525')] + + operations = [migrations.RunPython(forward_func)] diff --git a/bcs-app/backend/iam/bcs_iam_migration/migrations/0007_bk_bcs_app_202108262047.py b/bcs-app/backend/iam/bcs_iam_migration/migrations/0007_bk_bcs_app_202108262047.py new file mode 100644 index 000000000..23ad50b44 --- /dev/null +++ b/bcs-app/backend/iam/bcs_iam_migration/migrations/0007_bk_bcs_app_202108262047.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community +Edition) 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. +""" +import codecs +import json +import os + +from django.conf import settings +from django.db import migrations +from iam.contrib.iam_migration.migrator import IAMMigrator + + +def forward_func(apps, schema_editor): + + migrator = IAMMigrator(Migration.migration_json) + migrator.migrate() + + +class Migration(migrations.Migration): + migration_json = "0007_resource_creator_actions.json" + + dependencies = [('bcs_iam_migration', '0006_bk_bcs_app_202108181525')] + + operations = [migrations.RunPython(forward_func)] diff --git a/bcs-app/backend/iam/bcs_iam_migration/migrations/__init__.py b/bcs-app/backend/iam/bcs_iam_migration/migrations/__init__.py new file mode 100644 index 000000000..08f8f6ab8 --- /dev/null +++ b/bcs-app/backend/iam/bcs_iam_migration/migrations/__init__.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community +Edition) 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. +""" diff --git a/bcs-app/backend/bcs_web/iam/permissions.py b/bcs-app/backend/iam/legacy_perms.py similarity index 97% rename from bcs-app/backend/bcs_web/iam/permissions.py rename to bcs-app/backend/iam/legacy_perms.py index 1d39bfb39..d5c9833c2 100644 --- a/bcs-app/backend/bcs_web/iam/permissions.py +++ b/bcs-app/backend/iam/legacy_perms.py @@ -15,15 +15,12 @@ from typing import Dict, Optional from django.conf import settings +from iam import IAM, OP, Action, MultiActionRequest, Request, Resource, Subject +from iam.api.client import Client +from iam.api.http import http_get, http_post +from iam.apply import models +from iam.exceptions import AuthAPIError, AuthInvalidRequest -try: - from iam import IAM, OP, Action, MultiActionRequest, Request, Resource, Subject - from iam.api.client import Client - from iam.api.http import http_get, http_post - from iam.apply import models - from iam.exceptions import AuthAPIError, AuthInvalidRequest -except Exception: - pass from backend.utils.basic import ChoicesEnum from backend.utils.exceptions import PermissionDeniedError diff --git a/bcs-app/backend/iam/open_apis/__init__.py b/bcs-app/backend/iam/open_apis/__init__.py new file mode 100644 index 000000000..08f8f6ab8 --- /dev/null +++ b/bcs-app/backend/iam/open_apis/__init__.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community +Edition) 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. +""" diff --git a/bcs-app/backend/bcs_web/iam/open_apis/authentication.py b/bcs-app/backend/iam/open_apis/authentication.py similarity index 85% rename from bcs-app/backend/bcs_web/iam/open_apis/authentication.py rename to bcs-app/backend/iam/open_apis/authentication.py index 363bbcc51..be7d78cdf 100644 --- a/bcs-app/backend/bcs_web/iam/open_apis/authentication.py +++ b/bcs-app/backend/iam/open_apis/authentication.py @@ -13,29 +13,28 @@ specific language governing permissions and limitations under the License. """ from django.conf import settings -from rest_framework import exceptions +from iam import IAM from rest_framework.authentication import BasicAuthentication +from rest_framework.exceptions import AuthenticationFailed as RESTAuthenticationFailed -try: - from iam import IAM -except Exception: - pass from backend.utils import FancyDict from .exceptions import AuthenticationFailed class IamBasicAuthentication(BasicAuthentication): + """自定义认证逻辑, 对权限中心请求认证""" + def authenticate(self, request): try: result = super().authenticate(request) if result is None: raise AuthenticationFailed("basic auth failed") - except exceptions.AuthenticationFailed as e: + except RESTAuthenticationFailed as e: raise AuthenticationFailed(str(e)) return result - def authenticate_credentials(self, userid, password): + def authenticate_credentials(self, userid: str, password: str, request=None): if userid != "bk_iam": raise AuthenticationFailed("username is not bk_iam") diff --git a/bcs-app/backend/bcs_web/iam/open_apis/constants.py b/bcs-app/backend/iam/open_apis/constants.py similarity index 57% rename from bcs-app/backend/bcs_web/iam/open_apis/constants.py rename to bcs-app/backend/iam/open_apis/constants.py index fc280e557..73e43586e 100644 --- a/bcs-app/backend/bcs_web/iam/open_apis/constants.py +++ b/bcs-app/backend/iam/open_apis/constants.py @@ -12,20 +12,20 @@ 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. """ -from backend.utils.basic import ChoicesEnum +from backend.iam.permissions.resources import constants +from backend.packages.blue_krill.data_types.enum import StructuredEnum +ResourceType = constants.ResourceType -class MethodChoices(ChoicesEnum): - LIST_ATTR = "list_attr" - LIST_ATTR_VALUE = "list_attr_value" - LIST_INSTANCE = "list_instance" - FETCH_INSTANCE_INFO = "fetch_instance_info" - LIST_INSTANCE_BY_POLICY = "list_instance_by_policy" - _choices_labels = ( - (LIST_ATTR, "list_attr"), - (LIST_ATTR_VALUE, "list_attr_value"), - (LIST_INSTANCE, "list_instance"), - (FETCH_INSTANCE_INFO, "fetch_instance_info"), - (LIST_INSTANCE_BY_POLICY, "list_instance_by_policy"), - ) +class MethodType(str, StructuredEnum): + """ + 权限中心拉取资源的 method 参数值 + 字段协议说明 https://bk.tencent.com/docs/document/6.0/160/8427?r=1 + """ + + LIST_ATTR = 'list_attr' + LIST_ATTR_VALUE = 'list_attr_value' + LIST_INSTANCE = 'list_instance' + FETCH_INSTANCE_INFO = 'fetch_instance_info' + LIST_INSTANCE_BY_POLICY = 'list_instance_by_policy' diff --git a/bcs-app/backend/bcs_web/iam/open_apis/exceptions.py b/bcs-app/backend/iam/open_apis/exceptions.py similarity index 94% rename from bcs-app/backend/bcs_web/iam/open_apis/exceptions.py rename to bcs-app/backend/iam/open_apis/exceptions.py index 2b87587b1..027d9e902 100644 --- a/bcs-app/backend/bcs_web/iam/open_apis/exceptions.py +++ b/bcs-app/backend/iam/open_apis/exceptions.py @@ -17,6 +17,3 @@ class AuthenticationFailed(exceptions.APIError): code = 401 - - -ResNotFoundError = exceptions.ResNotFoundError diff --git a/bcs-app/backend/iam/open_apis/provider/__init__.py b/bcs-app/backend/iam/open_apis/provider/__init__.py new file mode 100644 index 000000000..08f8f6ab8 --- /dev/null +++ b/bcs-app/backend/iam/open_apis/provider/__init__.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community +Edition) 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. +""" diff --git a/bcs-app/backend/iam/open_apis/provider/namespace.py b/bcs-app/backend/iam/open_apis/provider/namespace.py new file mode 100644 index 000000000..82d113602 --- /dev/null +++ b/bcs-app/backend/iam/open_apis/provider/namespace.py @@ -0,0 +1,70 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community +Edition) 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. +""" +from typing import Dict, List + +from iam.collection import FancyDict +from iam.resource.provider import ListResult, ResourceProvider +from iam.resource.utils import Page + +from backend.components.base import ComponentAuth +from backend.components.paas_cc import PaaSCCClient + +from .utils import get_system_token + + +class NamespaceProvider(ResourceProvider): + """命名空间 Provider""" + + def list_instance(self, filter_obj: FancyDict, page_obj: Page, **options) -> ListResult: + cluster_id = filter_obj.parent['id'] + namespace_list = self._list_namespaces(cluster_id) + + namespace_slice = namespace_list[page_obj.slice_from : page_obj.slice_to] + results = [{'id': f"{cluster_id}:{ns['name']}", 'display_name': ns['name']} for ns in namespace_slice] + + return ListResult(results=results, count=len(namespace_list)) + + def fetch_instance_info(self, filter_obj: FancyDict, **options) -> ListResult: + cluster_id = filter_obj.parent['id'] + namespace_list = self._list_namespaces(cluster_id) + + if filter_obj.ids: + # cluster_ns_id 结构如 BCS-K8S-40000:test + filter_ns_list = [cluster_ns_id.split(':')[1] for cluster_ns_id in filter_obj.ids] + results = [ + {'id': f"{cluster_id}:{ns['name']}", 'display_name': ns['name']} + for ns in namespace_list + if ns['name'] in filter_ns_list + ] + else: + results = [{'id': f"{cluster_id}:{ns['name']}", 'display_name': ns['name']} for ns in namespace_list] + + return ListResult(results=results, count=len(results)) + + def list_instance_by_policy(self, filter_obj: FancyDict, page_obj: Page, **options) -> ListResult: + # TODO 确认基于实例的查询是不是就是id的过滤查询 + return ListResult(results=[], count=0) + + def list_attr(self, **options) -> ListResult: + return ListResult(results=[], count=0) + + def list_attr_value(self, filter_obj: FancyDict, page_obj: Page, **options) -> ListResult: + return ListResult(results=[], count=0) + + def _list_namespaces(self, cluster_id: str) -> List[Dict]: + paas_cc = PaaSCCClient(auth=ComponentAuth(get_system_token())) + cluster = paas_cc.get_cluster_by_id(cluster_id=cluster_id) + ns_data = paas_cc.get_cluster_namespace_list(project_id=cluster['project_id'], cluster_id=cluster_id) + return ns_data['results'] diff --git a/bcs-app/backend/iam/open_apis/provider/project.py b/bcs-app/backend/iam/open_apis/provider/project.py new file mode 100644 index 000000000..7951a7a00 --- /dev/null +++ b/bcs-app/backend/iam/open_apis/provider/project.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community +Edition) 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. +""" +from iam.collection import FancyDict +from iam.resource.provider import ListResult, ResourceProvider +from iam.resource.utils import Page + +from backend.container_service.projects.base import list_projects + +from .utils import get_system_token + + +class ProjectProvider(ResourceProvider): + """项目资源的 Provider""" + + def list_instance(self, filter_obj: FancyDict, page_obj: Page, **options) -> ListResult: + projects = list_projects(get_system_token()) + projects_slice = projects[page_obj.slice_from : page_obj.slice_to] + results = [{'id': p['project_id'], 'display_name': p['project_name']} for p in projects_slice] + return ListResult(results=results, count=len(projects)) + + def fetch_instance_info(self, filter_obj: FancyDict, **options) -> ListResult: + query_params = {'project_ids': ','.join(filter_obj.ids)} + projects = list_projects(get_system_token(), query_params) + results = [{'id': p['project_id'], 'display_name': p['project_name']} for p in projects] + return ListResult(results=results, count=len(results)) + + def list_instance_by_policy(self, filter_obj: FancyDict, page_obj: Page, **options) -> ListResult: + # TODO 确认基于实例的查询是不是就是id的过滤查询 + return ListResult(results=[], count=0) + + def list_attr(self, **options) -> ListResult: + return ListResult(results=[], count=0) + + def list_attr_value(self, filter_obj: FancyDict, page_obj: Page, **options) -> ListResult: + return ListResult(results=[], count=0) diff --git a/bcs-app/backend/iam/open_apis/provider/resource.py b/bcs-app/backend/iam/open_apis/provider/resource.py new file mode 100644 index 000000000..703f5785a --- /dev/null +++ b/bcs-app/backend/iam/open_apis/provider/resource.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community +Edition) 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. +""" +from typing import Dict, List, Optional, Union + +from iam.collection import FancyDict +from iam.resource.utils import Page, get_filter_obj, get_page_obj + +from ..constants import ResourceType +from .namespace import NamespaceProvider +from .project import ProjectProvider + +PROVIDER_CLS_MAP = {ResourceType.Project: ProjectProvider, ResourceType.Namespace: NamespaceProvider} + + +class ResourceProvider: + def __init__(self, resource_type: str): + """:param resource_type: 资源类型 如 project, cluster 等""" + self.resource_provider = PROVIDER_CLS_MAP[resource_type]() + + def provide(self, method: str, data: Dict, **options) -> Union[List, Dict]: + """ + 根据 method 值, 调用对应的方法返回数据 + :param method: 值包括 list_attr, list_attr_value, list_instance 等 + :param data: 其他查询条件数据,如分页数据等 + """ + handler = getattr(self, method) + return handler(data, **options) + + def list_attr(self, data: Optional[Dict] = None, **options) -> List[Dict]: + """ + 查询某个资源类型可用于配置权限的属性列表 + :param data: 占位字段,为了上面provide方法的处理统一 + """ + result = self.resource_provider.list_attr(**options) + return result.to_list() + + def list_attr_value(self, data: Dict, **options) -> Dict: + """获取一个资源类型某个属性的值列表""" + filter_obj, page_obj = self._parse_filter_and_page(data) + result = self.resource_provider.list_attr_value(filter_obj, page_obj, **options) + return result.to_dict() + + def list_instance(self, data: Dict, **options) -> Dict: + """查询资源实例列表(支持分页)""" + filter_obj, page_obj = self._parse_filter_and_page(data) + result = self.resource_provider.list_instance(filter_obj, page_obj, **options) + return result.to_dict() + + def fetch_instance_info(self, data: Dict, **options) -> List[Dict]: + """查询资源实例列表""" + filter_obj, _ = self._parse_filter_and_page(data) + result = self.resource_provider.fetch_instance_info(filter_obj, **options) + return result.to_list() + + def list_instance_by_policy(self, data: Dict, **options) -> List[Dict]: + """根据策略表达式查询资源实例""" + filter_obj, page_obj = self._parse_filter_and_page(data) + result = self.resource_provider.list_instance_by_policy(filter_obj, page_obj, **options) + return result.to_list() + + def _parse_filter_and_page(self, data: Dict) -> (FancyDict, Page): + filter_obj = get_filter_obj(data["filter"], ["ids", "parent", "search", "resource_type_chain"]) + page_obj = get_page_obj(data.get("page")) + return filter_obj, page_obj diff --git a/bcs-app/backend/iam/open_apis/provider/utils.py b/bcs-app/backend/iam/open_apis/provider/utils.py new file mode 100644 index 000000000..3fd7664ce --- /dev/null +++ b/bcs-app/backend/iam/open_apis/provider/utils.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community +Edition) 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. +""" +from backend.components import ssm + + +def get_system_token(): + """获取非用户 access_token""" + return ssm.get_client_access_token()["access_token"] diff --git a/bcs-app/backend/bcs_web/iam/open_apis/serializers.py b/bcs-app/backend/iam/open_apis/serializers.py similarity index 74% rename from bcs-app/backend/bcs_web/iam/open_apis/serializers.py rename to bcs-app/backend/iam/open_apis/serializers.py index 33ef4cbb2..82b12b1ba 100644 --- a/bcs-app/backend/bcs_web/iam/open_apis/serializers.py +++ b/bcs-app/backend/iam/open_apis/serializers.py @@ -14,11 +14,11 @@ """ from rest_framework import serializers -from .constants import MethodChoices +from .constants import MethodType, ResourceType class QueryResourceSLZ(serializers.Serializer): - method = serializers.ChoiceField(choices=MethodChoices.get_choices()) - type = serializers.CharField() - filter = serializers.JSONField(default={}) - page = serializers.JSONField(default={}) + method = serializers.ChoiceField(choices=MethodType.get_choices()) + type = serializers.ChoiceField(choices=ResourceType.get_choices()) + filter = serializers.JSONField(default=dict) + page = serializers.JSONField(default=dict) diff --git a/bcs-app/backend/bcs_web/iam/open_apis/urls.py b/bcs-app/backend/iam/open_apis/urls.py similarity index 91% rename from bcs-app/backend/bcs_web/iam/open_apis/urls.py rename to bcs-app/backend/iam/open_apis/urls.py index 2dde8d822..fc94015a6 100644 --- a/bcs-app/backend/bcs_web/iam/open_apis/urls.py +++ b/bcs-app/backend/iam/open_apis/urls.py @@ -14,4 +14,4 @@ """ from django.conf.urls import include, url -urlpatterns = [url(r"^v1/", include("backend.bcs_web.iam.open_apis.v1.urls"))] +urlpatterns = [url(r"^v1/", include("backend.iam.open_apis.v1.urls"))] diff --git a/bcs-app/backend/iam/open_apis/v1/__init__.py b/bcs-app/backend/iam/open_apis/v1/__init__.py new file mode 100644 index 000000000..08f8f6ab8 --- /dev/null +++ b/bcs-app/backend/iam/open_apis/v1/__init__.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community +Edition) 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. +""" diff --git a/bcs-app/backend/iam/open_apis/v1/urls.py b/bcs-app/backend/iam/open_apis/v1/urls.py new file mode 100644 index 000000000..fe206cb59 --- /dev/null +++ b/bcs-app/backend/iam/open_apis/v1/urls.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community +Edition) 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. +""" +from django.conf.urls import url + +from .. import views + +resource_types = 'projects|namespaces' + +urlpatterns = [ + url(r'^(?P{})/'.format(resource_types), views.ResourceAPIView.as_view()), +] diff --git a/bcs-app/backend/bcs_web/iam/open_apis/views.py b/bcs-app/backend/iam/open_apis/views.py similarity index 82% rename from bcs-app/backend/bcs_web/iam/open_apis/views.py rename to bcs-app/backend/iam/open_apis/views.py index 7fc1d38de..59e84665f 100644 --- a/bcs-app/backend/bcs_web/iam/open_apis/views.py +++ b/bcs-app/backend/iam/open_apis/views.py @@ -18,11 +18,13 @@ from backend.utils.renderers import BKAPIRenderer from .authentication import IamBasicAuthentication -from .resources.provider import BCSResourceProvider +from .provider.resource import ResourceProvider from .serializers import QueryResourceSLZ class ResourceAPIView(APIView): + """统一入口: 提供给权限中心拉取各类资源""" + renderer_classes = (BKAPIRenderer,) authentication_classes = (IamBasicAuthentication,) permission_classes = () @@ -35,11 +37,11 @@ def _get_options(self, request): request.LANGUAGE_CODE = "en" return {"language": language} - def post(self, request): + def post(self, request, *args, **kwargs): serializer = QueryResourceSLZ(data=request.data) serializer.is_valid(raise_exception=True) validated_data = serializer.validated_data - provider = BCSResourceProvider(resource_type=validated_data["type"]) - resp = provider.provide(validated_data, **self._get_options(request)) + provider = ResourceProvider(resource_type=validated_data["type"]) + resp = provider.provide(method=validated_data['method'], data=validated_data, **self._get_options(request)) return Response(resp) diff --git a/bcs-app/backend/iam/permissions/__init__.py b/bcs-app/backend/iam/permissions/__init__.py new file mode 100644 index 000000000..08f8f6ab8 --- /dev/null +++ b/bcs-app/backend/iam/permissions/__init__.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community +Edition) 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. +""" diff --git a/bcs-app/backend/iam/permissions/apply_url.py b/bcs-app/backend/iam/permissions/apply_url.py new file mode 100644 index 000000000..44d63f6e7 --- /dev/null +++ b/bcs-app/backend/iam/permissions/apply_url.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community +Edition) 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. +""" +import logging +from typing import List + +from django.conf import settings +from iam import IAM +from iam.apply import models + +from .request import ActionResourcesRequest + +logger = logging.getLogger(__name__) + + +class ApplyURLGenerator: + iam = IAM(settings.APP_ID, settings.APP_TOKEN, settings.BK_IAM_HOST, settings.BK_PAAS_INNER_HOST) + + @classmethod + def generate_apply_url(cls, username: str, action_request_list: List[ActionResourcesRequest]) -> str: + """ + 生成权限申请跳转 url + 参考 https://github.com/TencentBlueKing/iam-python-sdk/blob/master/docs/usage.md#14-获取无权限申请跳转url + """ + app = cls._make_application(action_request_list) + ok, _, url = cls.iam.get_apply_url(app, bk_username=username) + if not ok: + return settings.BK_IAM_APP_URL + return url + + @staticmethod + def _make_application(action_request_list: List[ActionResourcesRequest]) -> models.Application: + """为 generate_apply_url 方法生成 models.Application""" + return models.Application(settings.APP_ID, actions=[req.to_action() for req in action_request_list]) diff --git a/bcs-app/backend/iam/permissions/client.py b/bcs-app/backend/iam/permissions/client.py new file mode 100644 index 000000000..af2d29f44 --- /dev/null +++ b/bcs-app/backend/iam/permissions/client.py @@ -0,0 +1,102 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community +Edition) 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. +""" +from typing import Dict, List, Optional + +from django.conf import settings +from iam import IAM, Action, MultiActionRequest, Request, Resource, Subject + +from .request import ResourceRequest + + +class IAMClient: + """提供基础的 iam client 方法封装""" + + iam = IAM(settings.APP_ID, settings.APP_TOKEN, settings.BK_IAM_HOST, settings.BK_PAAS_INNER_HOST) + + def resource_type_allowed(self, username: str, action_id: str, use_cache: bool = False) -> bool: + """ + 判断用户是否具备某个操作的权限 + note: 权限判断与资源实例无关,如创建某资源 + """ + request = self._make_request(username, action_id) + if not use_cache: + return self.iam.is_allowed(request) + return self.iam.is_allowed_with_cache(request) + + def resource_inst_allowed( + self, username: str, action_id: str, res_request: ResourceRequest, use_cache: bool = False + ) -> bool: + """ + 判断用户对某个资源实例是否具有指定操作的权限 + note: 权限判断与资源实例有关,如更新某个具体资源 + """ + request = self._make_request(username, action_id, resources=res_request.make_resources()) + if not use_cache: + return self.iam.is_allowed(request) + return self.iam.is_allowed_with_cache(request) + + def resource_type_multi_actions_allowed(self, username: str, action_ids: List[str]) -> Dict[str, bool]: + """ + 判断用户是否具备多个操作的权限 + note: 权限判断与资源实例无关,如创建某资源 + + :returns 示例 {'project_create': True} + """ + return {action_id: self.resource_type_allowed(username, action_id) for action_id in action_ids} + + def resource_inst_multi_actions_allowed( + self, username: str, action_ids: List[str], res_request: ResourceRequest + ) -> Dict[str, bool]: + """ + 判断用户对某个资源实例是否具有多个操作的权限. + note: 权限判断与资源实例有关,如更新某个具体资源 + + :return 示例 {'project_view': True, 'project_edit': False} + """ + actions = [Action(action_id) for action_id in action_ids] + request = MultiActionRequest( + settings.APP_ID, Subject("user", username), actions, res_request.make_resources(), None + ) + return self.iam.resource_multi_actions_allowed(request) + + def batch_resource_multi_actions_allowed( + self, username: str, action_ids: List[str], res_request: ResourceRequest + ) -> Dict[str, Dict[str, bool]]: + """ + 判断用户对某些资源是否具有多个指定操作的权限 + note: 当前sdk仅支持同类型的资源 + + :return 示例 {'0ad86c25363f4ef8adcb7ac67a483837': {'project_view': True, 'project_edit': False}} + """ + actions = [Action(action_id) for action_id in action_ids] + request = MultiActionRequest(settings.APP_ID, Subject("user", username), actions, [], None) + resources_list = [[res] for res in res_request.make_resources()] + return self.iam.batch_resource_multi_actions_allowed(request, resources_list) + + def _make_request(self, username: str, action_id: str, resources: Optional[List[Resource]] = None) -> Request: + return Request( + settings.APP_ID, + Subject("user", username), + Action(action_id), + resources, + None, + ) + + def _grant_resource_creator_actions(self, username: str, data: Dict): + """ + 用于创建资源时,注册用户对该资源的关联操作权限. + note: 具体的关联操作见权限模型的 resource_creator_actions 字段 + """ + return self.iam._client.grant_resource_creator_actions(None, username, data) diff --git a/bcs-app/backend/iam/permissions/decorators.py b/bcs-app/backend/iam/permissions/decorators.py new file mode 100644 index 000000000..4d0d34361 --- /dev/null +++ b/bcs-app/backend/iam/permissions/decorators.py @@ -0,0 +1,141 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community +Edition) 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. +""" +import importlib +import logging +from abc import ABCMeta, abstractmethod + +import wrapt + +from .exceptions import PermissionDeniedError +from .perm import PermCtx +from .perm import Permission as PermPermission + +logger = logging.getLogger(__name__) + + +class RelatedPermission(metaclass=ABCMeta): + """ + 用于资源 Permission 类的方法装饰, 目的是支持 related_actions. + + 如 related_project_perm 和 related_cluster_perm 装饰器的用法: + + class ClusterPermission(Permission): + + resource_type: str = 'cluster' + resource_request_cls: Type[ResourceRequest] = ClusterRequest + + @related_project_perm(method_name='can_view') + def can_view(self, perm_ctx: ClusterPermCtx, raise_exception: bool = True, just_raise: bool = False) -> bool: + return self.can_action(perm_ctx, ClusterAction.VIEW, raise_exception, just_raise) + + @related_cluster_perm(method_name='can_view') + def can_manage(self, perm_ctx: ClusterPermCtx, raise_exception: bool = True, just_raise: bool = False) -> bool: + return self.can_action(perm_ctx, ClusterAction.MANAGE, raise_exception, just_raise) + + """ + + module_name: str # 资源模块名 如 cluster, project + + def __init__(self, method_name: str): + """ + :param method_name: 权限类的 can_{action} 方法名,用于校验用户是否具有对应的操作权限 + """ + self.method_name = method_name + + def _gen_perm_obj(self) -> PermPermission: + """获取权限类实例,如 project.ProjectPermission""" + p_module_name = __name__[: __name__.rfind(".")] + try: + return getattr( + importlib.import_module(f'{p_module_name}.resources.{self.module_name}'), + f'{self.module_name.capitalize()}Permission', + )() + except (ModuleNotFoundError, AttributeError) as e: + logger.error('_gen_perm_obj error: %s', e) + + @wrapt.decorator + def __call__(self, wrapped, instance, args, kwargs): + self.perm_obj = self._gen_perm_obj() + + perm_ctx = self._convert_perm_ctx(instance, args, kwargs) + + try: + is_allowed = wrapped(*args, **kwargs) + except PermissionDeniedError as e: + # 按照权限中心的建议,无论关联资源操作是否有权限,统一按照无权限返回,目的是生成最终的 apply_url + perm_ctx.force_raise = True + try: + getattr(self.perm_obj, self.method_name)(perm_ctx) + except PermissionDeniedError as err: + raise PermissionDeniedError( + f'{e.message}; {err.message}', + username=perm_ctx.username, + action_request_list=e.action_request_list + err.action_request_list, + ) + else: + # 无权限,并且没有抛出 PermissionDeniedError, 说明 raise_exception = False + if not is_allowed: + return is_allowed + + logger.debug(f'continue to verify {self.method_name} {self.module_name} permission...') + + # 有权限时,继续校验关联操作的权限 + raise_exception = kwargs.get('raise_exception', True) + return getattr(self.perm_obj, self.method_name)(perm_ctx, raise_exception=raise_exception) + + @abstractmethod + def _convert_perm_ctx(self, instance, args, kwargs) -> PermCtx: + """将被装饰的方法中的 perm_ctx 转换成 perm_obj.method_name 需要的 perm_ctx""" + + @property + def action_id(self) -> str: + return f'{self.perm_obj.resource_type}_{self.method_name[4:]}' + + +class Permission: + """鉴权装饰器基类,用于装饰函数或者方法""" + + module_name: str # 资源模块名 如 cluster, project + + def __init__(self, method_name: str): + """ + :param method_name: 权限类的 can_{action} 方法名,用于校验用户是否具有对应的操作权限 + """ + self.method_name = method_name + + def _gen_perm_obj(self) -> PermPermission: + """获取权限类实例,如 project.ProjectPermission""" + p_module_name = __name__[: __name__.rfind(".")] + try: + return getattr( + importlib.import_module(f'{p_module_name}.resources.{self.module_name}'), + f'{self.module_name.capitalize()}Permission', + )() + except (ModuleNotFoundError, AttributeError) as e: + logger.error('_gen_perm_obj error: %s', e) + + @wrapt.decorator + def __call__(self, wrapped, instance, args, kwargs): + + self.perm_obj = self._gen_perm_obj() + + if len(args) <= 0: + raise TypeError('missing PermCtx instance argument') + if not isinstance(args[0], PermCtx): + raise TypeError('missing ProjectPermCtx instance argument') + + getattr(self.perm_obj, self.method_name)(args[0]) + + return wrapped(*args, **kwargs) diff --git a/bcs-app/backend/iam/permissions/exceptions.py b/bcs-app/backend/iam/permissions/exceptions.py new file mode 100644 index 000000000..bafe684a5 --- /dev/null +++ b/bcs-app/backend/iam/permissions/exceptions.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community +Edition) 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. +""" +from typing import Dict, List, Optional + +from .apply_url import ApplyURLGenerator +from .request import ActionResourcesRequest + + +class PermissionDeniedError(Exception): + message = 'Permission denied' + code = 40300 + + def __init__( + self, + message: str = '', + username: str = '', + action_request_list: Optional[List[ActionResourcesRequest]] = None, + ): + """ + :param message: 异常信息 + :param username: 无权限的用户名, 用于生成 apply_url + :param action_request_list: 用于生成 apply_url + """ + + if message: + self.message = message + + self.username = username + self.action_request_list = action_request_list + + @property + def data(self) -> Dict: + return { + 'apply_url': ApplyURLGenerator.generate_apply_url(self.username, self.action_request_list), + 'action_list': [ + {'resource_type': action_request.resource_type, 'action_id': action_request.action_id} + for action_request in self.action_request_list + ], + } + + def __str__(self): + return f'{self.code}: {self.message}' + + +class AttrValidationError(Exception): + """属性字段校验异常""" diff --git a/bcs-app/backend/iam/permissions/perm.py b/bcs-app/backend/iam/permissions/perm.py new file mode 100644 index 000000000..16150838a --- /dev/null +++ b/bcs-app/backend/iam/permissions/perm.py @@ -0,0 +1,180 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community +Edition) 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. +""" +import logging +from abc import ABC, abstractmethod +from typing import List, Optional, Type + +import attr +from django.conf import settings + +from .client import IAMClient +from .exceptions import AttrValidationError, PermissionDeniedError +from .request import ActionResourcesRequest, IAMResource, ResourceRequest + +logger = logging.getLogger(__name__) + + +@attr.dataclass +class PermCtx: + """ + 权限参数上下文 + note: 由于 force_raise 默认值的原因,其子类属性必须设置默认值 + """ + + username: str + force_raise: bool = False # 如果为 True, 表示不做权限校验,直接以无权限方式抛出异常 + + def validate_resource_id(self): + """校验资源实例 ID. 如果校验不过,抛出 AttrValidationError 异常""" + if not self.resource_id: + raise AttrValidationError(f'missing valid resource_id') + + @property + def resource_id(self) -> str: + return '' + + +class Permission(ABC, IAMClient): + """ + 对接 IAM 的权限基类 + """ + + resource_type: str = '' + resource_request_cls: Type[ResourceRequest] = ResourceRequest + parent_res_perm: Optional['Permission'] = None # 父级资源的权限类对象 + + def can_action_with_view( + self, perm_ctx: PermCtx, action_id: str, view_action_id: str, raise_exception: bool, use_cache: bool = False + ) -> bool: + """ + 校验用户的 action_id 权限时,级联校验对资源的查看(view_action_id)权限 + + :param perm_ctx: 权限校验的上下文 + :param action_id: 资源操作 ID + :param raise_exception: 无权限时,是否抛出异常 + :param use_cache: 是否使用本地缓存 (缓存时间 1 min) 校验权限。用于非敏感操作鉴权,比如 view 操作 + """ + if action_id == view_action_id: + raise ValueError('parameter action_id and view_action_id are equal') + + if not view_action_id.endswith('_view'): + raise ValueError("parameter view_action_id must ends with '_view'") + + try: + is_allowed = self.can_action(perm_ctx, action_id, raise_exception, use_cache) + except PermissionDeniedError as e: + # 按照权限中心的建议,无论关联资源操作是否有权限,统一按照无权限返回,目的是生成最终的 apply_url + perm_ctx.force_raise = True + try: + self.can_action(perm_ctx, view_action_id, raise_exception, use_cache) + except PermissionDeniedError as err: + raise PermissionDeniedError( + f'{e.message}; {err.message}', + username=perm_ctx.username, + action_request_list=e.action_request_list + err.action_request_list, + ) + else: + # action_id 无权限,并且没有抛出 PermissionDeniedError, 说明 raise_exception = False + if not is_allowed: + return is_allowed + # action_id 有权限时,继续校验 view_action_id 权限 + logger.debug(f'continue to verify {view_action_id} permission...') + return self.can_action(perm_ctx, view_action_id, raise_exception, use_cache) + + def can_action(self, perm_ctx: PermCtx, action_id: str, raise_exception: bool, use_cache: bool = False) -> bool: + """ + 校验用户的 action_id 权限 + + :param perm_ctx: 权限校验的上下文 + :param action_id: 资源操作 ID + :param raise_exception: 无权限时,是否抛出异常 + :param use_cache: 是否使用本地缓存 (缓存时间 1 min) 校验权限。用于非敏感操作鉴权,比如 view 操作 + """ + if perm_ctx.force_raise: + self._raise_permission_denied_error(perm_ctx, action_id) + + is_allowed = self._can_action(perm_ctx, action_id, use_cache) + + if raise_exception and not is_allowed: + self._raise_permission_denied_error(perm_ctx, action_id) + + return is_allowed + + def grant_resource_creator_actions(self, username: str, resource_id: str, resource_name: str): + """ + 用于创建资源时,注册用户对该资源的关联操作权限. + note: 具体的关联操作见权限模型的 resource_creator_actions 字段 + TODO 需要针对层级资源重构 + """ + data = { + "type": self.resource_type, + "id": resource_id, + "name": resource_name, + "system": settings.APP_ID, + "creator": username, + } + return self.iam._client.grant_resource_creator_actions(None, username, data) + + def make_res_request(self, res_id: str, perm_ctx: PermCtx) -> ResourceRequest: + """创建当前资源 request""" + return self.resource_request_cls(res_id) + + def has_parent_resource(self) -> bool: + return self.parent_res_perm is not None + + @abstractmethod + def get_parent_chain(self, perm_ctx: PermCtx) -> List[IAMResource]: + """从 ctx 中获取 parent_chain""" + + def _can_action(self, perm_ctx: PermCtx, action_id: str, use_cache: bool = False) -> bool: + res_id = self.get_resource_id(perm_ctx) + + if res_id: # 与当前资源实例相关 + res_request = self.make_res_request(res_id, perm_ctx) + return self.resource_inst_allowed(perm_ctx.username, action_id, res_request, use_cache) + + # 与当前资源实例无关, 并且无关联上级资源, 按资源实例无关处理 + if not self.has_parent_resource(): + return self.resource_type_allowed(perm_ctx.username, action_id, use_cache) + + # 有关联上级资源 + res_request = self.parent_res_perm.make_res_request( + res_id=self.parent_res_perm.get_resource_id(perm_ctx), perm_ctx=perm_ctx + ) + return self.resource_inst_allowed(perm_ctx.username, action_id, res_request, use_cache) + + def _raise_permission_denied_error(self, perm_ctx: PermCtx, action_id: str): + res_id = self.get_resource_id(perm_ctx) + resources = None + resource_type = self.resource_type + parent_chain = None + + if res_id: + resources = [res_id] + parent_chain = self.get_parent_chain(perm_ctx) + elif self.has_parent_resource(): + resource_type = self.parent_res_perm.resource_type + resources = [self.parent_res_perm.get_resource_id(perm_ctx)] + parent_chain = self.parent_res_perm.get_parent_chain(perm_ctx) + + raise PermissionDeniedError( + f"no {action_id} permission", + username=perm_ctx.username, + action_request_list=[ActionResourcesRequest(action_id, resource_type, resources, parent_chain)], + ) + + @abstractmethod + def get_resource_id(self, perm_ctx: PermCtx) -> Optional[str]: + """从 ctx 中获取当前资源对应的 id""" diff --git a/bcs-app/backend/iam/permissions/request.py b/bcs-app/backend/iam/permissions/request.py new file mode 100644 index 000000000..9a4382bbb --- /dev/null +++ b/bcs-app/backend/iam/permissions/request.py @@ -0,0 +1,98 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community +Edition) 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. +""" +from abc import ABC +from collections import namedtuple +from typing import Dict, List, Optional, Union + +from django.conf import settings +from iam import Resource +from iam.apply import models + + +class ResourceRequest(ABC): + resource_type: str = '' + attr: Optional[Dict] = None + + def __init__(self, res: Union[List[str], str], **attr_kwargs): + """ + :param res: 单个资源 ID 或资源 ID 列表 + :param attr_kwargs: 用于替换 attr 中可能需要 format 的值 + """ + self.res = [str(res_id) for res_id in res] if isinstance(res, list) else str(res) + self.attr_kwargs = dict(**attr_kwargs) + self._validate_attr_kwargs() + + def make_resources(self) -> List[Resource]: + if isinstance(self.res, str): + return [Resource(settings.APP_ID, self.resource_type, self.res, self._make_attribute(self.res))] + + return [ + Resource(settings.APP_ID, self.resource_type, res_id, self._make_attribute(res_id)) for res_id in self.res + ] + + def _validate_attr_kwargs(self): + """如果校验不通过,抛出 AttrValidateError 异常""" + return + + def _make_attribute(self, res_id: str) -> Dict: + return {} + + +IAMResource = namedtuple('IAMResource', 'resource_type resource_id') + + +class ActionResourcesRequest: + """ + 操作资源请求. + note: resources 是由资源 ID 构成的列表. 为 None 时,表示资源无关. + 资源实例相关时,resources 表示的资源必须具有相同的父实例。以命名空间为例,它们必须是同项目同集群下 + """ + + def __init__( + self, + action_id: str, + resource_type: Optional[str] = None, + resources: Optional[List[str]] = None, + parent_chain: List[IAMResource] = None, + ): + """ + :param action_id: 操作 ID + :param resource_type: 资源类型 + :param resources: 资源 ID 列表 + :param parent_chain: 按照父类层级排序(父->子) [(resource_type, resource_id), ] + """ + self.action_id = action_id + self.resource_type = resource_type + self.resources = resources + self.parent_chain = parent_chain + + def to_action(self) -> Union[models.ActionWithResources, models.ActionWithoutResources]: + # 资源实例相关 + if self.resources: + parent_chain_node = self._to_parent_chain_node() + instances = [ + models.ResourceInstance(parent_chain_node + [models.ResourceNode(self.resource_type, res_id, res_id)]) + for res_id in self.resources + ] + related_resource_type = models.RelatedResourceType(settings.APP_ID, self.resource_type, instances) + return models.ActionWithResources(self.action_id, [related_resource_type]) + + # 资源实例无关 + return models.ActionWithoutResources(self.action_id) + + def _to_parent_chain_node(self) -> List[models.ResourceNode]: + if self.parent_chain: + return [models.ResourceNode(p.resource_type, p.resource_id, p.resource_id) for p in self.parent_chain] + return [] diff --git a/bcs-app/backend/iam/permissions/resources/__init__.py b/bcs-app/backend/iam/permissions/resources/__init__.py new file mode 100644 index 000000000..08f8f6ab8 --- /dev/null +++ b/bcs-app/backend/iam/permissions/resources/__init__.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community +Edition) 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. +""" diff --git a/bcs-app/backend/iam/permissions/resources/cluster.py b/bcs-app/backend/iam/permissions/resources/cluster.py new file mode 100644 index 000000000..5df347c4f --- /dev/null +++ b/bcs-app/backend/iam/permissions/resources/cluster.py @@ -0,0 +1,117 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community +Edition) 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. +""" +from typing import Dict, List, Optional, Type + +import attr + +from backend.iam.permissions import decorators +from backend.iam.permissions.exceptions import AttrValidationError +from backend.iam.permissions.perm import PermCtx, Permission, ResourceRequest +from backend.iam.permissions.request import IAMResource +from backend.packages.blue_krill.data_types.enum import EnumField, StructuredEnum + +from .constants import ResourceType +from .project import ProjectPermission, related_project_perm + + +class ClusterAction(str, StructuredEnum): + CREATE = EnumField('cluster_create', label='cluster_create') + VIEW = EnumField('cluster_view', label='cluster_view') + MANAGE = EnumField('cluster_manage', label='cluster_manage') + DELETE = EnumField('cluster_delete', label='cluster_delete') + USE = EnumField('cluster_use', label='cluster_use') + + +@attr.dataclass +class ClusterPermCtx(PermCtx): + project_id: str = '' + cluster_id: Optional[str] = None + + @property + def resource_id(self) -> str: + return self.cluster_id + + +class ClusterRequest(ResourceRequest): + resource_type: str = ResourceType.Cluster + attr = {'_bk_iam_path_': f'/project,{{project_id}}/'} + + def _make_attribute(self, res_id: str) -> Dict: + return {'_bk_iam_path_': self.attr['_bk_iam_path_'].format(project_id=self.attr_kwargs['project_id'])} + + def _validate_attr_kwargs(self): + if not self.attr_kwargs.get('project_id'): + raise AttrValidationError('missing project_id or project_id is invalid') + + +class related_cluster_perm(decorators.RelatedPermission): + + module_name: str = ResourceType.Cluster + + def _convert_perm_ctx(self, instance, args, kwargs) -> PermCtx: + """仅支持第一个参数是 PermCtx 子类实例""" + if len(args) <= 0: + raise TypeError('missing ClusterPermCtx instance argument') + if isinstance(args[0], PermCtx): + return ClusterPermCtx( + username=args[0].username, project_id=args[0].project_id, cluster_id=args[0].cluster_id + ) + else: + raise TypeError('missing ClusterPermCtx instance argument') + + +class cluster_perm(decorators.Permission): + module_name: str = ResourceType.Cluster + + +class ClusterPermission(Permission): + """集群权限""" + + resource_type: str = ResourceType.Cluster + resource_request_cls: Type[ResourceRequest] = ClusterRequest + parent_res_perm = ProjectPermission() + + @related_project_perm(method_name='can_view') + def can_create(self, perm_ctx: ClusterPermCtx, raise_exception: bool = True) -> bool: + return self.can_action(perm_ctx, ClusterAction.CREATE, raise_exception) + + @related_project_perm(method_name='can_view') + def can_view(self, perm_ctx: ClusterPermCtx, raise_exception: bool = True) -> bool: + perm_ctx.validate_resource_id() + return self.can_action(perm_ctx, ClusterAction.VIEW, raise_exception) + + @related_cluster_perm(method_name='can_view') + def can_manage(self, perm_ctx: ClusterPermCtx, raise_exception: bool = True) -> bool: + perm_ctx.validate_resource_id() + return self.can_action(perm_ctx, ClusterAction.MANAGE, raise_exception) + + @related_cluster_perm(method_name='can_view') + def can_delete(self, perm_ctx: ClusterPermCtx, raise_exception: bool = True) -> bool: + perm_ctx.validate_resource_id() + return self.can_action(perm_ctx, ClusterAction.DELETE, raise_exception) + + @related_cluster_perm(method_name='can_view') + def can_use(self, perm_ctx: ClusterPermCtx, raise_exception: bool = True) -> bool: + perm_ctx.validate_resource_id() + return self.can_action(perm_ctx, ClusterAction.USE, raise_exception) + + def make_res_request(self, res_id: str, perm_ctx: ClusterPermCtx) -> ResourceRequest: + return self.resource_request_cls(res_id, project_id=perm_ctx.project_id) + + def get_parent_chain(self, perm_ctx: ClusterPermCtx) -> List[IAMResource]: + return [IAMResource(ResourceType.Project, perm_ctx.project_id)] + + def get_resource_id(self, perm_ctx: ClusterPermCtx) -> Optional[str]: + return perm_ctx.cluster_id diff --git a/bcs-app/backend/iam/permissions/resources/constants.py b/bcs-app/backend/iam/permissions/resources/constants.py new file mode 100644 index 000000000..85b62c74b --- /dev/null +++ b/bcs-app/backend/iam/permissions/resources/constants.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community +Edition) 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. +""" +from backend.packages.blue_krill.data_types.enum import StructuredEnum + + +class ResourceType(str, StructuredEnum): + Project = 'project' + Cluster = 'cluster' + Namespace = 'namespace' + Templateset = "templateset" diff --git a/bcs-app/backend/iam/permissions/resources/namespace.py b/bcs-app/backend/iam/permissions/resources/namespace.py new file mode 100644 index 000000000..e645373fb --- /dev/null +++ b/bcs-app/backend/iam/permissions/resources/namespace.py @@ -0,0 +1,144 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community +Edition) 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. +""" +from typing import Dict, List, Optional, Type + +import attr + +from backend.iam.permissions import decorators +from backend.iam.permissions.exceptions import AttrValidationError +from backend.iam.permissions.perm import PermCtx, Permission +from backend.iam.permissions.request import IAMResource, ResourceRequest +from backend.packages.blue_krill.data_types.enum import EnumField, StructuredEnum + +from .cluster import ClusterPermission, related_cluster_perm +from .constants import ResourceType + + +def calc_iam_ns_id(cluster_id: str, name: Optional[str] = None, max_length: int = 32) -> Optional[str]: + """ + 计算出注册到权限中心的命名空间ID,具备唯一性. + TODO 下一个 PR 中实现,方便 review + """ + return "default" + + +class NamespaceAction(str, StructuredEnum): + CREATE = EnumField('namespace_create', label='namespace_create') + VIEW = EnumField('namespace_view', label='namespace_view') + UPDATE = EnumField('namespace_update', label='namespace_update') + DELETE = EnumField('namespace_delete', label='namespace_delete') + USE = EnumField('namespace_use', label='namespace_use') + + +@attr.dataclass +class NamespacePermCtx(PermCtx): + project_id: str = '' + cluster_id: str = '' + name: Optional[str] = None # 命名空间名 + iam_ns_id: Optional[str] = None # 注册到权限中心的命名空间ID + + def __attrs_post_init__(self): + """权限中心的 resource_id 长度限制为32位""" + self.iam_ns_id = calc_iam_ns_id(self.cluster_id, self.name) + + @property + def resource_id(self) -> str: + return self.iam_ns_id + + +class NamespaceRequest(ResourceRequest): + resource_type: str = ResourceType.Namespace + attr = {'_bk_iam_path_': f'/project,{{project_id}}/cluster,{{cluster_id}}/'} + + def _make_attribute(self, res_id: str) -> Dict: + return { + '_bk_iam_path_': self.attr['_bk_iam_path_'].format( + project_id=self.attr_kwargs['project_id'], cluster_id=self.attr_kwargs['cluster_id'] + ) + } + + def _validate_attr_kwargs(self): + if not self.attr_kwargs.get('project_id'): + raise AttrValidationError('missing project_id or project_id is invalid') + + if not self.attr_kwargs.get('cluster_id'): + raise AttrValidationError('missing cluster_id or cluster_id is invalid') + + +class related_namespace_perm(decorators.RelatedPermission): + + module_name: str = ResourceType.Namespace + + def _convert_perm_ctx(self, instance, args, kwargs) -> PermCtx: + """仅支持第一个参数是 PermCtx 子类实例""" + if len(args) <= 0: + raise TypeError('missing NamespacePermCtx instance argument') + if isinstance(args[0], PermCtx): + return NamespacePermCtx( + username=args[0].username, + project_id=args[0].project_id, + cluster_id=args[0].cluster_id, + name=args[0].name, + ) + else: + raise TypeError('missing NamespacePermCtx instance argument') + + +class namespace_perm(decorators.Permission): + module_name: str = ResourceType.Namespace + + +class NamespacePermission(Permission): + """命名空间权限""" + + resource_type: str = ResourceType.Namespace + resource_request_cls: Type[ResourceRequest] = NamespaceRequest + parent_res_perm = ClusterPermission() + + @related_cluster_perm(method_name='can_use') + def can_create(self, perm_ctx: NamespacePermCtx, raise_exception: bool = True) -> bool: + return self.can_action(perm_ctx, NamespaceAction.CREATE, raise_exception) + + @related_cluster_perm(method_name='can_view') + def can_view(self, perm_ctx: NamespacePermCtx, raise_exception: bool = True) -> bool: + perm_ctx.validate_resource_id() + return self.can_action(perm_ctx, NamespaceAction.VIEW, raise_exception) + + @related_cluster_perm(method_name='can_use') + def can_update(self, perm_ctx: NamespacePermCtx, raise_exception: bool = True) -> bool: + perm_ctx.validate_resource_id() + return self.can_action_with_view(perm_ctx, NamespaceAction.UPDATE, NamespaceAction.VIEW, raise_exception) + + @related_cluster_perm(method_name='can_use') + def can_delete(self, perm_ctx: NamespacePermCtx, raise_exception: bool = True) -> bool: + perm_ctx.validate_resource_id() + return self.can_action_with_view(perm_ctx, NamespaceAction.DELETE, NamespaceAction.VIEW, raise_exception) + + @related_cluster_perm(method_name='can_use') + def can_use(self, perm_ctx: NamespacePermCtx, raise_exception: bool = True) -> bool: + perm_ctx.validate_resource_id() + return self.can_action_with_view(perm_ctx, NamespaceAction.USE, NamespaceAction.VIEW, raise_exception) + + def make_res_request(self, res_id: str, perm_ctx: NamespacePermCtx) -> ResourceRequest: + return self.resource_request_cls(res_id, project_id=perm_ctx.project_id, cluster_id=perm_ctx.cluster_id) + + def get_parent_chain(self, perm_ctx: NamespacePermCtx) -> List[IAMResource]: + return [ + IAMResource(ResourceType.Project, perm_ctx.project_id), + IAMResource(ResourceType.Cluster, perm_ctx.cluster_id), + ] + + def get_resource_id(self, perm_ctx: NamespacePermCtx) -> Optional[str]: + return perm_ctx.iam_ns_id diff --git a/bcs-app/backend/iam/permissions/resources/project.py b/bcs-app/backend/iam/permissions/resources/project.py new file mode 100644 index 000000000..66a3d34b1 --- /dev/null +++ b/bcs-app/backend/iam/permissions/resources/project.py @@ -0,0 +1,82 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community +Edition) 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. +""" +from typing import List, Optional, Type + +import attr + +from backend.iam.permissions import decorators +from backend.iam.permissions.perm import PermCtx, Permission, ResourceRequest +from backend.iam.permissions.request import IAMResource +from backend.packages.blue_krill.data_types.enum import EnumField, StructuredEnum + +from .constants import ResourceType + + +class ProjectAction(str, StructuredEnum): + CREATE = EnumField('project_create', label='project_create') + VIEW = EnumField('project_view', label='project_view') + EDIT = EnumField('project_edit', label='project_edit') + + +class ProjectRequest(ResourceRequest): + resource_type: str = ResourceType.Project + + +@attr.dataclass +class ProjectPermCtx(PermCtx): + project_id: Optional[str] = None + + @property + def resource_id(self) -> str: + return self.project_id + + +class related_project_perm(decorators.RelatedPermission): + + module_name: str = ResourceType.Project + + def _convert_perm_ctx(self, instance, args, kwargs) -> PermCtx: + """仅支持第一个参数是 PermCtx 子类实例""" + if len(args) <= 0: + raise TypeError('missing ProjectPermCtx instance argument') + if isinstance(args[0], PermCtx): + return ProjectPermCtx(username=args[0].username, project_id=args[0].project_id) + else: + raise TypeError('missing ProjectPermCtx instance argument') + + +class ProjectPermission(Permission): + """项目权限""" + + resource_type: str = ResourceType.Project + resource_request_cls: Type[ResourceRequest] = ProjectRequest + + def can_create(self, perm_ctx: ProjectPermCtx, raise_exception: bool = True) -> bool: + return self.can_action(perm_ctx, ProjectAction.CREATE, raise_exception) + + def can_view(self, perm_ctx: ProjectPermCtx, raise_exception: bool = True) -> bool: + perm_ctx.validate_resource_id() + return self.can_action(perm_ctx, ProjectAction.VIEW, raise_exception) + + @related_project_perm(method_name='can_view') + def can_edit(self, perm_ctx: ProjectPermCtx, raise_exception: bool = True) -> bool: + perm_ctx.validate_resource_id() + return self.can_action(perm_ctx, ProjectAction.EDIT, raise_exception) + + def get_parent_chain(self, perm_ctx: ProjectPermCtx) -> List[IAMResource]: + return [] + + def get_resource_id(self, perm_ctx: ProjectPermCtx) -> Optional[str]: + return perm_ctx.project_id diff --git a/bcs-app/backend/iam/permissions/resources/templateset.py b/bcs-app/backend/iam/permissions/resources/templateset.py new file mode 100644 index 000000000..791440478 --- /dev/null +++ b/bcs-app/backend/iam/permissions/resources/templateset.py @@ -0,0 +1,126 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community +Edition) 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. +""" +from typing import Dict, List, Optional, Type, Union + +import attr + +from backend.iam.permissions import decorators +from backend.iam.permissions.exceptions import AttrValidationError +from backend.iam.permissions.perm import PermCtx, Permission +from backend.iam.permissions.request import IAMResource, ResourceRequest +from backend.packages.blue_krill.data_types.enum import EnumField, StructuredEnum + +from .constants import ResourceType +from .project import ProjectPermission, related_project_perm + + +class TemplatesetAction(str, StructuredEnum): + CREATE = EnumField("templateset_create", label="templateset_create") + VIEW = EnumField("templateset_view", label="templateset_view") + UPDATE = EnumField("templateset_update", label="templateset_update") + DELETE = EnumField("templateset_delete", label="templateset_delete") + INSTANTIATE = EnumField("templateset_instantiate", label="templateset_instantiate") + COPY = EnumField("templateset_copy", label="templateset_copy") + + +@attr.dataclass +class TemplatesetPermCtx(PermCtx): + project_id: str = '' + template_id: Union[str, int, None] = None + + def __attrs_post_init__(self): + if self.template_id: + self.template_id = str(self.template_id) + + @property + def resource_id(self) -> str: + return self.template_id + + +class TemplatesetRequest(ResourceRequest): + resource_type: str = ResourceType.Templateset + attr = {'_bk_iam_path_': f'/project,{{project_id}}/'} + + def _make_attribute(self, res_id: str) -> Dict: + return {'_bk_iam_path_': self.attr['_bk_iam_path_'].format(project_id=self.attr_kwargs['project_id'])} + + def _validate_attr_kwargs(self): + if not self.attr_kwargs.get('project_id'): + raise AttrValidationError('missing project_id or project_id is invalid') + + +class related_templateset_perm(decorators.RelatedPermission): + module_name: str = ResourceType.Templateset + + def _convert_perm_ctx(self, instance, args, kwargs) -> PermCtx: + """仅支持第一个参数是 PermCtx 子类实例""" + if len(args) <= 0: + raise TypeError('missing TemplatesetPermCtx instance a rgument') + if isinstance(args[0], PermCtx): + return TemplatesetPermCtx( + username=args[0].username, project_id=args[0].project_id, template_id=args[0].template_id + ) + else: + raise TypeError('missing TemplatesetPermCtx instance argument') + + +class templateset_perm(decorators.Permission): + module_name: str = ResourceType.Templateset + + +class TemplatesetPermission(Permission): + """模板集权限""" + + resource_type: str = ResourceType.Templateset + resource_request_cls: Type[ResourceRequest] = TemplatesetRequest + parent_res_perm = ProjectPermission() + + @related_project_perm(method_name="can_view") + def can_create(self, perm_ctx: TemplatesetPermCtx, raise_exception: bool = True) -> bool: + return self.can_action(perm_ctx, TemplatesetAction.CREATE, raise_exception) + + @related_project_perm(method_name="can_view") + def can_view(self, perm_ctx: TemplatesetPermCtx, raise_exception: bool = True) -> bool: + perm_ctx.validate_resource_id() + return self.can_action(perm_ctx, TemplatesetAction.VIEW, raise_exception) + + @related_templateset_perm(method_name='can_view') + def can_copy(self, perm_ctx: TemplatesetPermCtx, raise_exception: bool = True) -> bool: + perm_ctx.validate_resource_id() + return self.can_action(perm_ctx, TemplatesetAction.COPY, raise_exception) + + @related_templateset_perm(method_name='can_view') + def can_update(self, perm_ctx: TemplatesetPermCtx, raise_exception: bool = True) -> bool: + perm_ctx.validate_resource_id() + return self.can_action(perm_ctx, TemplatesetAction.UPDATE, raise_exception) + + @related_templateset_perm(method_name='can_view') + def can_delete(self, perm_ctx: TemplatesetPermCtx, raise_exception: bool = True) -> bool: + perm_ctx.validate_resource_id() + return self.can_action(perm_ctx, TemplatesetAction.DELETE, raise_exception) + + @related_templateset_perm(method_name='can_view') + def can_instantiate(self, perm_ctx: TemplatesetPermCtx, raise_exception: bool = True) -> bool: + perm_ctx.validate_resource_id() + return self.can_action(perm_ctx, TemplatesetAction.INSTANTIATE, raise_exception) + + def make_res_request(self, res_id: str, perm_ctx: TemplatesetPermCtx) -> ResourceRequest: + return self.resource_request_cls(res_id, project_id=perm_ctx.project_id) + + def get_parent_chain(self, perm_ctx: TemplatesetPermCtx) -> List[IAMResource]: + return [IAMResource(ResourceType.Project, perm_ctx.project_id)] + + def get_resource_id(self, perm_ctx: TemplatesetPermCtx) -> Optional[str]: + return perm_ctx.template_id diff --git a/bcs-app/backend/resources/client.py b/bcs-app/backend/resources/client.py index df62436fe..1ab32fbd2 100644 --- a/bcs-app/backend/resources/client.py +++ b/bcs-app/backend/resources/client.py @@ -22,7 +22,11 @@ from backend.components.bcs import k8s from backend.container_service.clusters.base import CtxCluster from backend.utils import exceptions +from backend.utils.cache import region from backend.utils.errcodes import ErrorCode +from backend.utils.whitelist import check_bcs_api_gateway_enabled + +from .constants import BCS_CLUSTER_EXPIRATION_TIME logger = logging.getLogger(__name__) @@ -79,19 +83,49 @@ def make_configuration(self) -> Configuration: config = client.Configuration() config.verify_ssl = False # Get credentials - credentials = self.get_client_credentials() - config.host = '{}{}'.format(self._get_apiservers_host(env_name), credentials['server_address_path']).rstrip( - "/" - ) + credentials = self.get_client_credentials(env_name) + config.host = credentials['host'] config.api_key = {"authorization": f"Bearer {credentials['user_token']}"} return config - def get_client_credentials(self) -> Dict[str, Any]: - """获取访问集群 apiserver 所需的鉴权信息,比如证书、user_token、server_address_path 等""" - env_name = self.env_querier.do() + def get_client_credentials(self, env_name: str) -> Dict[str, str]: + """获取访问集群 apiserver 所需的鉴权信息,包含 user_token、server_address_path 等 + NOTE: 白名单控制访问集群的链路,白名单中的集群通过 bcs-api-gateway 访问 apiserver,其它通过 bcs-api 访问 apiserver + """ + if check_bcs_api_gateway_enabled(self.cluster.id): + return self._bcs_api_gateway_credentials(env_name) + return self._get_bcs_api_credentials(env_name) - bcs_cluster_id = self.bcs_api.query_cluster_id(env_name, self.cluster.project_id, self.cluster.id) - return self.bcs_api.get_cluster_credentials(env_name, bcs_cluster_id) + def _get_bcs_api_credentials(self, env_name: str) -> Dict[str, str]: + """获取通过 bcs api 网关访问集群 apiserver的鉴权信息 + + :param env_name: 集群所属环境,包含正式环境和测试环境 + """ + # TODO: 兼容逻辑,待 bcs api 新架构稳定后,废弃下面逻辑 + # 因为bcs cluster id(带有后缀随机字符的cluster id)注册后,不会变动;因此,可以长期缓存 + cache_key = f"BK_DEVOPS_BCS:CLUSTER_ID:{self.cluster.id}" + bcs_cluster_id = region.get(cache_key, expiration_time=BCS_CLUSTER_EXPIRATION_TIME) + + if not bcs_cluster_id: + bcs_cluster_id = self.bcs_api.query_cluster_id(env_name, self.cluster.project_id, self.cluster.id) + region.set(cache_key, bcs_cluster_id) + # 获取对应的credentials信息 + credentials = self.bcs_api.get_cluster_credentials(env_name, bcs_cluster_id) + + return { + "host": f"{self._get_apiservers_host(env_name)}{credentials['server_address_path']}".rstrip("/"), + "user_token": credentials["user_token"], + } + + def _bcs_api_gateway_credentials(self, env_name: str) -> Dict[str, str]: + """获取通过 bcs api gateway 网关访问集群 apiserver 所需的鉴权信息 + + :param env_name: 集群所属环境,包含正式环境和测试环境 + """ + return { + "host": f"{getattr(settings, 'BCS_API_GW_DOMAIN', '')}/{env_name}/v4/clusters/{self.cluster.id}", + "user_token": getattr(settings, "BCS_API_GW_AUTH_TOKEN", ""), + } @staticmethod def _get_apiservers_host(api_env_name: str) -> str: diff --git a/bcs-app/backend/resources/constants.py b/bcs-app/backend/resources/constants.py index fe956a8cb..da5084d4b 100644 --- a/bcs-app/backend/resources/constants.py +++ b/bcs-app/backend/resources/constants.py @@ -193,7 +193,7 @@ def shortname(self): class MetricSourceType(str, StructuredEnum): - """ k8s MetricSourceType """ + """k8s MetricSourceType""" Object = EnumField('Object') Pods = EnumField('Pods') @@ -222,3 +222,7 @@ class NodeConditionType(str, StructuredEnum): DiskPressure = EnumField("DiskPressure", label="kubelet is under pressure due to insufficient available disk") PIDPressure = EnumField("PIDPressure", label="kubelet is under pressure due to insufficient available PID") NetworkUnavailable = EnumField("NetworkUnavailable", label="network for the node is not correctly configured") + + +# 设置 bcs cluster id 缓存时间为7天 +BCS_CLUSTER_EXPIRATION_TIME = 3600 * 24 * 7 diff --git a/bcs-app/backend/settings/base.py b/bcs-app/backend/settings/base.py index bf6dfa292..dc84c9496 100644 --- a/bcs-app/backend/settings/base.py +++ b/bcs-app/backend/settings/base.py @@ -388,9 +388,6 @@ def get_logging_config(log_level, rds_hander_settings=None, log_path="app.log"): # 灰度功能提示消息 GRAYSCALE_FEATURE_MSG = "功能灰度测试中,请联系管理员添加白名单" -# APIGW APP权限控制 -BK_APP_WHITELIST = {} - # 平台组件部署到的命名空间 BCS_SYSTEM_NAMESPACE = "bcs-system" @@ -460,6 +457,12 @@ def get_logging_config(log_level, rds_hander_settings=None, log_path="app.log"): # 默认主键 3.2 版本需要主动指定 DEFAULT_AUTO_FIELD = "django.db.models.AutoField" +# 访问 bcs-api 服务需要的token +BCS_AUTH_TOKEN = os.environ.get("BCS_AUTH_TOKEN", "") +# 访问 bcs-api-gateway 服务需要的token +BCS_API_GW_AUTH_TOKEN = os.environ.get("BCS_API_GW_AUTH_TOKEN", "") + + try: from .base_ext import * # noqa except ImportError as e: diff --git a/bcs-app/backend/settings/ce/base.py b/bcs-app/backend/settings/ce/base.py index 047d6005d..fae22275f 100644 --- a/bcs-app/backend/settings/ce/base.py +++ b/bcs-app/backend/settings/ce/base.py @@ -36,7 +36,7 @@ "backend.uniapps.apis", "backend.bcs_web.apis.apps.APIConfig", "iam.contrib.iam_migration", - "backend.bcs_web.iam.bcs_iam_migration.apps.BcsIamMigrationConfig", + "backend.iam.bcs_iam_migration.apps.BcsIamMigrationConfig", ] # 统一登录页面 @@ -55,10 +55,6 @@ }, } -# cors settings -CORS_ORIGIN_REGEX_WHITELIST = (r".*",) -CORS_ALLOW_CREDENTIALS = True - REDIS_HOST = os.environ.get("REDIS_HOST", "127.0.0.1") REDIS_PORT = os.environ.get("REDIS_PORT", 6379) REDIS_DB = os.environ.get("REDIS_DB", 0) @@ -274,3 +270,6 @@ def configure_workers(*args, **kwargs): HELM_REPO_DOMAIN = os.environ.get('HELM_REPO_DOMAIN') HELM_MERELY_REPO_URL = HELM_REPO_DOMAIN BK_REPO_URL_PREFIX = os.environ.get('BK_REPO_URL_PREFIX') + +# 默认 BKCC 设备供应方 +BKCC_DEFAULT_SUPPLIER_ACCOUNT = os.environ.get('BKCC_DEFAULT_SUPPLIER_ACCOUNT', None) diff --git a/bcs-app/backend/settings/ce/cors.py b/bcs-app/backend/settings/ce/cors.py new file mode 100644 index 000000000..e459784e1 --- /dev/null +++ b/bcs-app/backend/settings/ce/cors.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community +Edition) 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. +""" +from typing import List +from urllib import parse + + +def get_cors_allowed_origins(raw_urls: List[str]) -> List[str]: + """获取允许的origin列表(精确过滤)""" + + origins = [] + for url in raw_urls: + parsed = parse.urlparse(url) + origin = f'{parsed.scheme}://{parsed.netloc}' + origins.append(origin) + + if not parsed.port: + continue + + # 如果 origin 带端口,将不带端口的也加入 origins 中(主要是处理80和443) + origins.append(origin[: origin.rfind(str(parsed.port)) - 1]) + + # 去重返回 + return list(set(origins)) diff --git a/bcs-app/backend/settings/ce/dev.py b/bcs-app/backend/settings/ce/dev.py index e90c49324..965c86d9a 100644 --- a/bcs-app/backend/settings/ce/dev.py +++ b/bcs-app/backend/settings/ce/dev.py @@ -37,7 +37,7 @@ ] # 本地开发先去除权限中心v3的数据初始逻辑 -INSTALLED_APPS.remove("backend.bcs_web.iam.bcs_iam_migration.apps.BcsIamMigrationConfig") +INSTALLED_APPS.remove("backend.iam.bcs_iam_migration.apps.BcsIamMigrationConfig") LOG_LEVEL = "DEBUG" LOGGING = get_logging_config(LOG_LEVEL) diff --git a/bcs-app/backend/settings/ce/saas_prod.py b/bcs-app/backend/settings/ce/saas_prod.py index eabbb3c40..52a79c01b 100644 --- a/bcs-app/backend/settings/ce/saas_prod.py +++ b/bcs-app/backend/settings/ce/saas_prod.py @@ -19,6 +19,7 @@ from .base import * # noqa from .base import INSTALLED_APPS +from .cors import get_cors_allowed_origins INSTALLED_APPS += [ "backend.celery_app.CeleryConfig", @@ -170,3 +171,7 @@ # 初始化时渲染K8S版本 K8S_VERSION = os.environ.get("BKAPP_K8S_VERSION") + +# cors settings +CORS_ALLOWED_ORIGINS = get_cors_allowed_origins([DEVOPS_HOST, BK_PAAS_HOST, DEVOPS_BCS_HOST, DEVOPS_BCS_API_URL]) +CORS_ALLOW_CREDENTIALS = True diff --git a/bcs-app/backend/tests/bcs_mocks/data/paas_cc_json.py b/bcs-app/backend/tests/bcs_mocks/data/paas_cc_json.py index 0090fa371..36a22ecd3 100644 --- a/bcs-app/backend/tests/bcs_mocks/data/paas_cc_json.py +++ b/bcs-app/backend/tests/bcs_mocks/data/paas_cc_json.py @@ -51,6 +51,78 @@ "request_id": uuid.uuid4().hex, } +resp_filter_projects_ok = { + "data": { + "results": [ + { + "approval_status": 2, + "approval_time": "2020-01-01T00:00:00+08:00", + "approver": "", + "bg_id": -1, + "bg_name": "", + "cc_app_id": 100, + "center_id": 100, + "center_name": "", + "created_at": "2020-01-01 00:00:00", + "creator": "unknown", + "data_id": 0, + "deploy_type": "null", + "dept_id": -1, + "dept_name": "", + "description": "", + "english_name": "unittest-cluster", + "extra": {}, + "is_offlined": False, + "is_secrecy": False, + "kind": 1, + "logo_addr": "", + "project_id": uuid.uuid4().hex, + "project_name": "unittest-cluster", + "project_type": 1, + "remark": "", + "updated_at": "2020-01-01 00:00:00", + "use_bk": False, + "cc_app_name": "demo-app", + "can_edit": False, + }, + { + "approval_status": 2, + "approval_time": "2020-01-01T00:00:00+08:00", + "approver": "", + "bg_id": -1, + "bg_name": "", + "cc_app_id": 100, + "center_id": 100, + "center_name": "", + "created_at": "2020-01-01 00:00:00", + "creator": "unknown", + "data_id": 0, + "deploy_type": "null", + "dept_id": -1, + "dept_name": "", + "description": "", + "english_name": "unittest-cluster-a", + "extra": {}, + "is_offlined": False, + "is_secrecy": False, + "kind": 1, + "logo_addr": "", + "project_id": uuid.uuid4().hex, + "project_name": "unittest-cluster-a", + "project_type": 1, + "remark": "", + "updated_at": "2020-01-01 00:00:00", + "use_bk": False, + "cc_app_name": "demo-app", + "can_edit": False, + }, + ] + }, + "code": 0, + "message": "OK", + "request_id": uuid.uuid4().hex, +} + resp_get_clusters_ok = { "code": 0, "data": { diff --git a/bcs-app/backend/tests/bcs_mocks/misc.py b/bcs-app/backend/tests/bcs_mocks/misc.py index b151c5ee0..caf5349d3 100644 --- a/bcs-app/backend/tests/bcs_mocks/misc.py +++ b/bcs-app/backend/tests/bcs_mocks/misc.py @@ -13,7 +13,7 @@ specific language governing permissions and limitations under the License. """ import copy -from typing import Dict +from typing import Dict, Optional from .data import paas_cc_json @@ -26,6 +26,17 @@ def get_project(self, access_token: str, project_id: str) -> Dict: resp_get_project_ok['data']['project_id'] = project_id return self._resp(resp_get_project_ok) + def get_projects(self, access_token: str, query_params: Optional[Dict]) -> Dict: + resp = self._resp(paas_cc_json.resp_filter_projects_ok) + + if not query_params: + return resp + + project_id_list = query_params['project_ids'].split(',') + resp['data']['results'][0]['project_id'] = project_id_list[0] + + return resp + def get_all_clusters(self, access_token, project_id, limit=None, offset=None, desire_all_data=0): resp = self._resp(paas_cc_json.resp_get_clusters_ok) for info in resp['data']['results']: diff --git a/bcs-app/backend/tests/components/cc/__init__.py b/bcs-app/backend/tests/components/cc/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/bcs-app/backend/tests/components/cc/test_business.py b/bcs-app/backend/tests/components/cc/test_business.py new file mode 100644 index 000000000..f6b68feb0 --- /dev/null +++ b/bcs-app/backend/tests/components/cc/test_business.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community +Edition) 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. +""" +import mock +import pytest + +from backend.components.cc import ( + AppQueryService, + fetch_has_maintain_perm_apps, + get_app_maintainers, + get_application_name, +) + +FAKE_BIZS_INFO = { + # 设置会 201,刚好可以触发全量查询时候查询两次 + "count": 201, + "info": [ + { + "bs2_name_id": 1, + "bk_biz_id": 1001, + "bk_biz_name": "测试业务", + "default": 0, + "bk_biz_maintainer": "admin,admin1", + } + ], +} + + +class TestComponentCCBusiness: + @pytest.fixture(autouse=True) + def patch_api_call(self): + with mock.patch('backend.components.cc.business.BkCCClient.search_business', return_value=FAKE_BIZS_INFO): + yield + + def test_fetch_has_maintain_perm_apps(self): + ret = fetch_has_maintain_perm_apps('admin') + assert ret == [{'id': 1001, "name": "测试业务"}, {'id': 1001, "name": "测试业务"}] + + def test_get_single_app_info(self): + """ 测试 AppQueryService.get """ + ret = AppQueryService('admin').get(1001) + assert ret == FAKE_BIZS_INFO['info'][0] + + def test_get_application_name(self): + ret = get_application_name(1001) + assert ret == '测试业务' + + def test_get_app_maintainers(self): + ret = get_app_maintainers('admin', 1001) + assert ret == ['admin', 'admin1'] + + def test_fetch_all_apps(self): + """ 测试 AppQueryService.fetch_all """ + ret = AppQueryService('admin').fetch_all() + assert len(ret) == 2 diff --git a/bcs-app/backend/tests/components/cc/test_client.py b/bcs-app/backend/tests/components/cc/test_client.py new file mode 100644 index 000000000..90f32ef4c --- /dev/null +++ b/bcs-app/backend/tests/components/cc/test_client.py @@ -0,0 +1,136 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community +Edition) 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. +""" +from requests_mock import ANY + +from backend.components.cc.client import BkCCClient, PageData + +# 测试用 search_business 返回结果 +FAKE_BIZS_RESP = { + "code": 0, + "data": { + "count": 1, + "info": [ + { + "bs2_name_id": 1, + "default": 0, + } + ], + }, +} + +# 测试用 search_biz_inst_topo 返回结果 +FAKE_TOPO_RESP = { + "code": 0, + "data": [ + { + "default": 0, + "bk_obj_name": "业务", + "bk_obj_id": "biz", + "child": [ + { + "default": 0, + "bk_obj_name": "集群", + "bk_obj_id": "set", + "child": [ + { + "default": 0, + "bk_obj_name": "模块", + "bk_obj_id": "module", + "child": [], + "bk_inst_id": 5003, + "bk_inst_name": "bcs-master", + } + ], + "bk_inst_id": 5001, + "bk_inst_name": "BCS-K8S-1001", + } + ], + "bk_inst_id": 10001, + "bk_inst_name": "BCS", + } + ], +} + +# 测试用 get_biz_internal_module 放回结果 +FAKE_INTERNAL_MODULE_RESP = { + "code": 0, + "data": { + "bk_set_id": 1, + "bk_set_name": "空闲机池", + "module": [ + { + "bk_module_id": 11, + "bk_module_name": "空闲机", + }, + { + "bk_module_id": 12, + "bk_module_name": "故障机", + }, + ], + }, +} + +# 测试用 list_biz_hosts 返回结果 +FAKE_HOSTS_RESP = { + "code": 0, + "data": { + "count": 2, + "info": [ + { + "bk_cloud_id": 0, + "bk_host_id": 1, + "bk_host_innerip": "127.0.0.1", + "svr_device_class": "S1234", + }, + { + "bk_cloud_id": 0, + "bk_host_id": 2, + "bk_host_innerip": "127.0.0.16", + "svr_device_class": "S1234", + }, + ], + }, +} + + +class TestBkCCClient: + def test_search_business(self, request_user, requests_mock): + requests_mock.post(ANY, json=FAKE_BIZS_RESP) + client = BkCCClient(request_user.username) + data = client.search_business(PageData(), ["bs2_name_id"], {"bk_biz_id": 1}) + assert data["info"][0]["bs2_name_id"] == 1 + assert requests_mock.called + + def test_search_biz_inst_topo(self, request_user, requests_mock): + requests_mock.post(ANY, json=FAKE_TOPO_RESP) + topo = BkCCClient(request_user.username).search_biz_inst_topo(1) + assert topo[0]['child'][0]['bk_inst_id'] == 5001 + assert requests_mock.called + + def test_get_biz_internal_module(self, request_user, requests_mock): + requests_mock.post(ANY, json=FAKE_INTERNAL_MODULE_RESP) + internal_module = BkCCClient(request_user.username).get_biz_internal_module(1) + assert internal_module['bk_set_name'] == '空闲机池' + assert internal_module['module'][0]['bk_module_id'] == 11 + assert requests_mock.called + + def test_list_biz_hosts(self, request_user, requests_mock): + requests_mock.post(ANY, json=FAKE_HOSTS_RESP) + resp = BkCCClient(request_user.username).list_biz_hosts( + 1001, PageData(), bk_set_ids=[], bk_module_ids=[], fields=[] + ) + assert resp['count'] == 2 + assert resp['info'][0]['bk_host_innerip'] == '127.0.0.1' + assert requests_mock.called diff --git a/bcs-app/backend/tests/components/cc/test_hosts.py b/bcs-app/backend/tests/components/cc/test_hosts.py new file mode 100644 index 000000000..3df1a5da4 --- /dev/null +++ b/bcs-app/backend/tests/components/cc/test_hosts.py @@ -0,0 +1,115 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community +Edition) 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. +""" +import mock +import pytest + +from backend.components.cc import BizTopoQueryService, HostQueryService, get_has_perm_hosts + +# 测试用 search_biz_inst_topo 返回结果 +FAKE_TOPO_RESP = [ + { + "default": 0, + "bk_obj_name": "业务", + "bk_obj_id": "biz", + "child": [ + { + "default": 0, + "bk_obj_name": "集群", + "bk_obj_id": "set", + "child": [ + { + "default": 0, + "bk_obj_name": "模块", + "bk_obj_id": "module", + "child": [], + "bk_inst_id": 5003, + "bk_inst_name": "bcs-master", + } + ], + "bk_inst_id": 5001, + "bk_inst_name": "BCS-K8S-1001", + } + ], + "bk_inst_id": 10001, + "bk_inst_name": "BCS", + } +] + +# 测试用 get_biz_internal_module 返回结果 +FAKE_INTERNAL_MODULE_RESP = { + "bk_set_id": 1, + "bk_set_name": "空闲机池", + "module": [ + { + "bk_module_id": 11, + "bk_module_name": "空闲机", + }, + { + "bk_module_id": 12, + "bk_module_name": "故障机", + }, + ], +} + +# 测试用 list_biz_hosts 返回结果 +FAKE_HOSTS_RESP = { + # 设置为 501,刚好触发查询两次逻辑 + "count": 501, + "info": [ + { + "bk_cloud_id": 0, + "bk_host_id": 1, + "bk_host_innerip": "127.0.0.1", + "svr_device_class": "S1234", + "operator": "admin", + }, + { + "bk_cloud_id": 0, + "bk_host_id": 2, + "bk_host_innerip": "127.0.0.16", + "svr_device_class": "S1234", + }, + ], +} + + +class TestComponentCCHosts: + @pytest.fixture(autouse=True) + def patch_api_call(self): + with mock.patch( + 'backend.components.cc.hosts.BkCCClient.search_biz_inst_topo', return_value=FAKE_TOPO_RESP + ), mock.patch( + 'backend.components.cc.hosts.BkCCClient.list_biz_hosts', return_value=FAKE_HOSTS_RESP + ), mock.patch( + 'backend.components.cc.hosts.BkCCClient.get_biz_internal_module', return_value=FAKE_INTERNAL_MODULE_RESP + ), mock.patch( + 'backend.components.cc.hosts.get_app_maintainers', return_value=[] + ): + yield + + def test_get_has_perm_hosts(self): + # 通过 mock get_app_maintainers 走 _get_hosts_by_operator 逻辑 + ret = get_has_perm_hosts(1001, 'admin') + assert len(ret) == 2 + + def test_search_biz_inst_topo(self): + ret = BizTopoQueryService('admin', 1001).fetch() + print(ret) + assert ret[0]['child'][0]['child'][0]['bk_inst_id'] == 11 + assert ret[0]['child'][1]['bk_inst_id'] == 5001 + + def test_fetch_all_hosts(self): + ret = HostQueryService('admin', 1001).fetch_all() + assert len(ret) == 4 diff --git a/bcs-app/backend/tests/components/test_paas_cc.py b/bcs-app/backend/tests/components/test_paas_cc.py index a73055a9b..2d6ea694d 100644 --- a/bcs-app/backend/tests/components/test_paas_cc.py +++ b/bcs-app/backend/tests/components/test_paas_cc.py @@ -27,6 +27,14 @@ def test_get_cluster_simple(self, project_id, cluster_id, requests_mock): assert resp == {'foo': 'bar'} assert requests_mock.called + def test_get_cluster_by_id(self, cluster_id, requests_mock): + requests_mock.get(ANY, json={'code': 0, 'data': {'cluster_id': cluster_id}}) + + client = PaaSCCClient(ComponentAuth('token')) + resp = client.get_cluster_by_id(cluster_id) + assert resp == {'cluster_id': cluster_id} + assert requests_mock.called + def test_update_cluster(self, project_id, cluster_id, requests_mock): requests_mock.put( ANY, json={"code": 0, "data": {"cluster_id": cluster_id, "project_id": project_id, "status": "normal"}} diff --git a/bcs-app/backend/tests/components/test_permissions.py b/bcs-app/backend/tests/components/test_permissions.py index 606786205..53ffb8a4a 100644 --- a/bcs-app/backend/tests/components/test_permissions.py +++ b/bcs-app/backend/tests/components/test_permissions.py @@ -15,7 +15,7 @@ import pytest from iam import OP -from backend.bcs_web.iam.permissions import ProjectPermission +from backend.iam.legacy_perms import ProjectPermission test_dict_filter_data = [ ({'op': OP.IN, 'value': [2, 1], 'field': 'project.id'}, {'project_id_list': [1, 2], 'op': OP.IN}), diff --git a/bcs-app/backend/tests/container_service/clusters/test_cc_host.py b/bcs-app/backend/tests/container_service/clusters/test_cc_host.py index 8fd98da23..ae1025cfe 100644 --- a/bcs-app/backend/tests/container_service/clusters/test_cc_host.py +++ b/bcs-app/backend/tests/container_service/clusters/test_cc_host.py @@ -21,7 +21,7 @@ API_URL_PREFIX = f'/api/projects/{TEST_PROJECT_ID}/cc' -def fake_search_topo(*args, **kwargs): +def fake_fetch_topo(*args, **kwargs): """ 返回测试用 topo 数据 """ return [ { @@ -29,6 +29,23 @@ def fake_search_topo(*args, **kwargs): "bk_obj_name": "业务", "bk_obj_id": "biz", "child": [ + { + "default": 0, + "bk_obj_name": "集群", + "bk_obj_id": "set", + "child": [ + { + "default": 0, + "bk_obj_name": "模块", + "bk_obj_id": "module", + "child": [], + "bk_inst_id": 1001, + "bk_inst_name": "空闲机", + } + ], + "bk_inst_id": 1, + "bk_inst_name": "BCS-K8S-1000", + }, { "default": 0, "bk_obj_name": "集群", @@ -53,7 +70,7 @@ def fake_search_topo(*args, **kwargs): ], "bk_inst_id": 5001, "bk_inst_name": "BCS-K8S-1001", - } + }, ], "bk_inst_id": 10001, "bk_inst_name": "BCS", @@ -61,25 +78,7 @@ def fake_search_topo(*args, **kwargs): ] -def fake_inner_mod_topo(*args, **kwargs): - """ 返回测试用 internal module topo 数据 """ - return { - 'bk_set_id': 1, - 'bk_set_name': '空闲机池', - 'module': [ - { - 'bk_module_id': 11, - 'bk_module_name': '空闲机', - }, - { - 'bk_module_id': 12, - 'bk_module_name': '故障机', - }, - ], - } - - -def fake_list_all_hosts(*args, **kwargs): +def fake_fetch_all_hosts(*args, **kwargs): """ 返回测试用主机数据(省略非必须字段) """ return [ # 被使用的 @@ -160,8 +159,7 @@ def fake_get_agent_status(*args, **kwargs): class TestCCAPI: """ 测试 CMDB API 相关接口 """ - @mock.patch('backend.container_service.clusters.cc_host.views.cc.search_biz_inst_topo', new=fake_search_topo) - @mock.patch('backend.container_service.clusters.cc_host.views.cc.get_biz_internal_module', new=fake_inner_mod_topo) + @mock.patch('backend.container_service.clusters.cc_host.views.cc.BizTopoQueryService.fetch', new=fake_fetch_topo) def test_get_biz_inst_topology(self, api_client): """ 测试创建资源接口 """ response = api_client.get(f'{API_URL_PREFIX}/topology/') @@ -174,7 +172,8 @@ def patch_list_hosts_api_call(self): 'backend.container_service.clusters.cc_host.views.cc.get_application_name', new=lambda *args, **kwargs: 'test-app-name', ), mock.patch( - 'backend.container_service.clusters.cc_host.views.cc.list_all_hosts_by_topo', new=fake_list_all_hosts + 'backend.container_service.clusters.cc_host.views.cc.HostQueryService.fetch_all', + new=fake_fetch_all_hosts, ), mock.patch( 'backend.container_service.clusters.cc_host.utils.paas_cc.get_project_cluster_resource', new=fake_get_project_cluster_resource, diff --git a/bcs-app/backend/tests/container_service/infras/hosts/test_host.py b/bcs-app/backend/tests/container_service/infras/hosts/test_host.py index d73e2bcb0..4278660af 100644 --- a/bcs-app/backend/tests/container_service/infras/hosts/test_host.py +++ b/bcs-app/backend/tests/container_service/infras/hosts/test_host.py @@ -14,46 +14,33 @@ """ from unittest.mock import patch -import pytest - from backend.container_service.infras.hosts import host from backend.container_service.infras.hosts.perms import can_use_hosts from backend.container_service.infras.hosts.terraform.engines import sops from backend.tests.bcs_mocks.fake_sops import FakeSopsMod, sops_json -fake_cc_host_ok_results = { - "result": True, - "data": [{"bk_host_innerip": "127.0.0.1,127.0.0.3"}, {"bk_host_innerip": "127.0.0.2"}], -} -fake_cc_host_null_results = {"result": False} -fake_cc_host_not_match_results = {"results": True, "data": [{"bk_host_innerip": "127.0.0.1"}]} +fake_cc_host_ok_results = [{"bk_host_innerip": "127.0.0.1,127.0.0.3"}, {"bk_host_innerip": "127.0.0.2"}] +fake_cc_host_null_results = [] +fake_cc_host_not_match_results = [{"bk_host_innerip": "127.0.0.1"}] expect_used_ip_list = ["127.0.0.1", "127.0.0.2"] class TestCheckUseHost: - @patch("backend.container_service.infras.hosts.perms.get_cc_hosts", return_value=fake_cc_host_ok_results) + @patch("backend.container_service.infras.hosts.perms.get_has_perm_hosts", return_value=fake_cc_host_ok_results) def test_ok(self, biz_id, username): assert can_use_hosts(biz_id, username, expect_used_ip_list) - @patch("backend.container_service.infras.hosts.perms.get_cc_hosts", return_value=fake_cc_host_null_results) + @patch("backend.container_service.infras.hosts.perms.get_has_perm_hosts", return_value=fake_cc_host_null_results) def test_null_resp_failed(self, biz_id, username): assert not can_use_hosts(biz_id, username, expect_used_ip_list) - @patch("backend.container_service.infras.hosts.perms.get_cc_hosts", return_value=fake_cc_host_not_match_results) + @patch( + "backend.container_service.infras.hosts.perms.get_has_perm_hosts", return_value=fake_cc_host_not_match_results + ) def test_not_match_failed(self, biz_id, username): assert not can_use_hosts(biz_id, username, expect_used_ip_list) -class TestGetCCHosts: - @patch("backend.container_service.infras.hosts.host.cc.get_app_hosts", return_value={"result": False}) - def test_get_null_hosts(self, mock_get_app_hosts, biz_id, username): - assert host.get_cc_hosts(username, biz_id) == [] - - @patch("backend.container_service.infras.hosts.host.cc.get_app_hosts", return_value=fake_cc_host_ok_results) - def test_get_hosts(self, mock_get_app_hosts, biz_id, username): - assert host.get_cc_hosts(username, biz_id) == fake_cc_host_ok_results["data"] - - class TestGetAgentStatus: @patch( "backend.container_service.infras.hosts.host.gse.get_agent_status", diff --git a/bcs-app/backend/bcs_web/iam/__init__.py b/bcs-app/backend/tests/iam/__init__.py similarity index 100% rename from bcs-app/backend/bcs_web/iam/__init__.py rename to bcs-app/backend/tests/iam/__init__.py diff --git a/bcs-app/backend/tests/iam/conftest.py b/bcs-app/backend/tests/iam/conftest.py new file mode 100644 index 000000000..f411532e6 --- /dev/null +++ b/bcs-app/backend/tests/iam/conftest.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community +Edition) 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. +""" +from typing import List +from unittest import mock + +import pytest + +from backend.iam.permissions.apply_url import ApplyURLGenerator +from backend.iam.permissions.request import ActionResourcesRequest +from backend.iam.permissions.resources.cluster import ClusterPermission +from backend.iam.permissions.resources.namespace import NamespacePermission +from backend.iam.permissions.resources.project import ProjectPermission + +from .fake_iam import FakeClusterPermission, FakeNamespacePermission, FakeProjectPermission + + +def generate_apply_url(username: str, action_request_list: List[ActionResourcesRequest]) -> List[str]: + expect = [] + for req in action_request_list: + resources = '' + if req.resources: + resources = ''.join(req.resources) + + parent_chain = '' + if req.parent_chain: + parent_chain = ''.join([f'{item.resource_type}/{item.resource_id}' for item in req.parent_chain]) + expect.append(f'{req.resource_type}:{req.action_id}:{resources}:{parent_chain}') + + return expect + + +@pytest.fixture(autouse=True) +def patch_generate_apply_url(): + with mock.patch.object(ApplyURLGenerator, 'generate_apply_url', new=generate_apply_url): + yield + + +@pytest.fixture +def project_permission_obj(): + patcher = mock.patch.object(ProjectPermission, '__bases__', (FakeProjectPermission,)) + with patcher: + patcher.is_local = True # 标注为本地属性,__exit__ 的时候恢复成 patcher.temp_original + yield ProjectPermission() + + +@pytest.fixture +def namespace_permission_obj(): + cluster_patcher = mock.patch.object(ClusterPermission, '__bases__', (FakeClusterPermission,)) + project_patcher = mock.patch.object(ProjectPermission, '__bases__', (FakeProjectPermission,)) + namespace_patcher = mock.patch.object(NamespacePermission, '__bases__', (FakeNamespacePermission,)) + with cluster_patcher, project_patcher, namespace_patcher: + cluster_patcher.is_local = True # 标注为本地属性,__exit__ 的时候恢复成 patcher.temp_original + project_patcher.is_local = True + namespace_patcher.is_local = True + yield NamespacePermission() diff --git a/bcs-app/backend/tests/iam/fake_iam.py b/bcs-app/backend/tests/iam/fake_iam.py new file mode 100644 index 000000000..c4c292f38 --- /dev/null +++ b/bcs-app/backend/tests/iam/fake_iam.py @@ -0,0 +1,95 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community +Edition) 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. +""" +from iam import Request + +from backend.iam.permissions.perm import Permission +from backend.iam.permissions.resources.project import ProjectAction + +from .permissions import roles + + +class FakeProjectIAM: + def is_allowed(self, request: Request) -> bool: + if request.subject.id in [roles.ADMIN_USER, roles.PROJECT_CLUSTER_USER, roles.PROJECT_NO_CLUSTER_USER]: + return True + + if request.subject.id == roles.PROJECT_NO_VIEW_USER: + if request.action.id == ProjectAction.VIEW: + return False + return True + + return False + + def is_allowed_with_cache(self, request: Request) -> bool: + return self.is_allowed(request) + + +class FakeProjectPermission(Permission): + iam = FakeProjectIAM() + + +class FakeClusterIAM: + def is_allowed(self, request: Request) -> bool: + if request.subject.id in [ + roles.ADMIN_USER, + roles.CLUSTER_USER, + roles.PROJECT_CLUSTER_USER, + roles.CLUSTER_NO_PROJECT_USER, + ]: + return True + return False + + def is_allowed_with_cache(self, request: Request) -> bool: + return self.is_allowed(request) + + +class FakeClusterPermission(Permission): + iam = FakeClusterIAM() + + +class FakeNamespaceIAM: + def is_allowed(self, request: Request) -> bool: + if request.subject.id in [roles.ADMIN_USER, roles.NAMESPACE_NO_CLUSTER_PROJECT_USER]: + return True + return False + + def is_allowed_with_cache(self, request: Request) -> bool: + return self.is_allowed(request) + + +class FakeNamespacePermission(Permission): + iam = FakeNamespaceIAM() + + +class FakeTemplatesetIAM: + def __init__(self, *args, **kwargs): + """""" + + def is_allowed(self, request: Request) -> bool: + if request.subject.id in [ + roles.ADMIN_USER, + roles.TEMPLATESET_USER, + roles.PROJECT_TEMPLATESET_USER, + roles.TEMPLATESET_NO_PROJECT_USER, + ]: + return True + return False + + def is_allowed_with_cache(self, request: Request) -> bool: + return self.is_allowed(request) + + +class FakeTemplatesetPermission(Permission): + iam = FakeTemplatesetIAM() diff --git a/bcs-app/backend/tests/iam/open_apis/__init__.py b/bcs-app/backend/tests/iam/open_apis/__init__.py new file mode 100644 index 000000000..08f8f6ab8 --- /dev/null +++ b/bcs-app/backend/tests/iam/open_apis/__init__.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community +Edition) 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. +""" diff --git a/bcs-app/backend/tests/iam/open_apis/conftest.py b/bcs-app/backend/tests/iam/open_apis/conftest.py new file mode 100644 index 000000000..bdef52054 --- /dev/null +++ b/bcs-app/backend/tests/iam/open_apis/conftest.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community +Edition) 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. +""" +import mock +import pytest + + +@pytest.fixture(autouse=True) +def patch4resource_api(): + with mock.patch( + 'backend.iam.open_apis.authentication.IamBasicAuthentication.authenticate', new=lambda *args, **kwargs: None + ), mock.patch( + 'backend.components.ssm.get_client_access_token', new=lambda *args, **kwargs: {"access_token": "test"} + ): + yield diff --git a/bcs-app/backend/tests/iam/open_apis/test_namespace.py b/bcs-app/backend/tests/iam/open_apis/test_namespace.py new file mode 100644 index 000000000..5f9117580 --- /dev/null +++ b/bcs-app/backend/tests/iam/open_apis/test_namespace.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community +Edition) 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. +""" +import mock +import pytest +from rest_framework.test import APIRequestFactory + +from backend.iam.open_apis.views import ResourceAPIView +from backend.tests.testing_utils.mocks.paas_cc import StubPaaSCCClient + +factory = APIRequestFactory() + + +@pytest.fixture(autouse=True) +def patch_paas_cc(): + with mock.patch('backend.iam.open_apis.provider.namespace.PaaSCCClient', new=StubPaaSCCClient): + yield + + +class TestNamespaceAPI: + def test_list_instance(self, project_id): + request = factory.post( + '/apis/iam/v1/namespaces/', + { + 'method': 'list_instance', + 'type': 'namespace', + 'page': {'offset': 0, 'limit': 1}, + 'filter': {'parent': {'id': project_id}}, + }, + ) + p_view = ResourceAPIView.as_view() + response = p_view(request) + data = response.data + assert data['count'] == 1 + assert data['results'][0]['display_name'] == 'default' + + def test_fetch_instance_info(self, cluster_id): + fetch_id = f'{cluster_id}:default' + request = factory.post( + '/apis/iam/v1/namespaces/', + { + 'method': 'fetch_instance_info', + 'type': 'namespace', + 'filter': {'ids': [fetch_id], 'parent': {'id': cluster_id}}, + }, + ) + p_view = ResourceAPIView.as_view() + response = p_view(request) + data = response.data + assert len(data) == 1 + assert data[0]['id'] == fetch_id + assert data[0]['display_name'] == 'default' diff --git a/bcs-app/backend/tests/iam/open_apis/test_project.py b/bcs-app/backend/tests/iam/open_apis/test_project.py new file mode 100644 index 000000000..9594d0b04 --- /dev/null +++ b/bcs-app/backend/tests/iam/open_apis/test_project.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community +Edition) 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. +""" +import mock +import pytest +from rest_framework.test import APIRequestFactory + +from backend.iam.open_apis.views import ResourceAPIView +from backend.tests.bcs_mocks.misc import FakePaaSCCMod + +factory = APIRequestFactory() + + +@pytest.fixture(autouse=True) +def patch_paas_cc(): + with mock.patch('backend.container_service.projects.base.utils.paas_cc', new=FakePaaSCCMod()): + yield + + +class TestProjectAPI: + def test_list_instance(self): + request = factory.post( + '/apis/iam/v1/projects/', {'method': 'list_instance', 'type': 'project', 'page': {'offset': 0, 'limit': 1}} + ) + p_view = ResourceAPIView.as_view() + response = p_view(request) + data = response.data + assert data['count'] == 2 + assert data['results'][0]['display_name'] == 'unittest-cluster' + + def test_fetch_instance_info(self, project_id): + request = factory.post( + '/apis/iam/v1/projects/', + { + 'method': 'fetch_instance_info', + 'type': 'project', + 'filter': {'ids': [project_id]}, + }, + ) + p_view = ResourceAPIView.as_view() + response = p_view(request) + data = response.data + assert data[0]['display_name'] == 'unittest-cluster' + assert data[0]['id'] == project_id diff --git a/bcs-app/backend/tests/iam/permissions/__init__.py b/bcs-app/backend/tests/iam/permissions/__init__.py new file mode 100644 index 000000000..08f8f6ab8 --- /dev/null +++ b/bcs-app/backend/tests/iam/permissions/__init__.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community +Edition) 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. +""" diff --git a/bcs-app/backend/tests/iam/permissions/conftest.py b/bcs-app/backend/tests/iam/permissions/conftest.py new file mode 100644 index 000000000..89034aad0 --- /dev/null +++ b/bcs-app/backend/tests/iam/permissions/conftest.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community +Edition) 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. +""" +import pytest + +from backend.tests.testing_utils.base import generate_random_string + + +@pytest.fixture +def namespace_name(): + return generate_random_string(16) + + +@pytest.fixture +def template_id(): + """生成一个随机模板集 ID""" + return generate_random_string(32) + + +@pytest.fixture +def template_name(): + """生成一个随机模板集 name""" + return generate_random_string(16) diff --git a/bcs-app/backend/tests/components/test_bk_cc.py b/bcs-app/backend/tests/iam/permissions/roles.py similarity index 55% rename from bcs-app/backend/tests/components/test_bk_cc.py rename to bcs-app/backend/tests/iam/permissions/roles.py index dbfa76d2f..c87f072ee 100644 --- a/bcs-app/backend/tests/components/test_bk_cc.py +++ b/bcs-app/backend/tests/iam/permissions/roles.py @@ -12,21 +12,29 @@ 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. """ -from requests_mock import ANY -from backend.components.cc import BkCCClient, PageData +ADMIN_USER = 'admin' -fake_biz_id = 1 -fake_bs2_id = 1 +ANONYMOUS_USER = 'anonymous_user' +PROJECT_NO_VIEW_USER = 'project_no_view_user' -class TestBkCCClient: - def test_search_biz(self, request_user, requests_mock): - requests_mock.post( - ANY, json={"code": 0, "data": {"count": 1, "info": [{"bs2_name_id": fake_bs2_id, "default": 0}]}} - ) - page = PageData() - client = BkCCClient(request_user.username) - data = client.search_biz(page, ["bs2_name_id"], {"bk_biz_id": fake_biz_id}) - assert data["info"][0]["bs2_name_id"] == fake_bs2_id - assert requests_mock.called +NO_PROJECT_USER = 'no_project_user' + +CLUSTER_USER = 'cluster_user' + +PROJECT_CLUSTER_USER = 'project_cluster_user' + +PROJECT_NO_CLUSTER_USER = 'project_no_cluster_user' + +CLUSTER_NO_PROJECT_USER = 'cluster_no_project_user' + +NAMESPACE_NO_CLUSTER_PROJECT_USER = 'namespace_no_cluster_project_user' + +TEMPLATESET_USER = "templateset_user" + +PROJECT_TEMPLATESET_USER = 'project_templateset_user' + +PROJECT_NO_TEMPLATESET_USER = 'project_no_templateset_user' + +TEMPLATESET_NO_PROJECT_USER = 'templateset_no_project_user' diff --git a/bcs-app/backend/tests/iam/permissions/test_cluster.py b/bcs-app/backend/tests/iam/permissions/test_cluster.py new file mode 100644 index 000000000..2c07d7981 --- /dev/null +++ b/bcs-app/backend/tests/iam/permissions/test_cluster.py @@ -0,0 +1,218 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community +Edition) 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. +""" +from unittest import mock + +import pytest + +from backend.iam.permissions.exceptions import PermissionDeniedError +from backend.iam.permissions.request import ActionResourcesRequest, IAMResource +from backend.iam.permissions.resources.cluster import ClusterAction, ClusterPermCtx, ClusterPermission, cluster_perm +from backend.iam.permissions.resources.constants import ResourceType +from backend.iam.permissions.resources.project import ProjectAction, ProjectPermission +from backend.tests.iam.conftest import generate_apply_url + +from ..fake_iam import FakeClusterPermission, FakeProjectPermission +from . import roles + + +@pytest.fixture +def cluster_permission_obj(): + cluster_patcher = mock.patch.object(ClusterPermission, '__bases__', (FakeClusterPermission,)) + project_patcher = mock.patch.object(ProjectPermission, '__bases__', (FakeProjectPermission,)) + with cluster_patcher, project_patcher: + cluster_patcher.is_local = True # 标注为本地属性,__exit__ 的时候恢复成 patcher.temp_original + project_patcher.is_local = True + yield ClusterPermission() + + +class TestClusterPermission: + """ + 集群资源权限 + note: 仅测试 cluster_create 和 cluster_view 两种代表性的权限,其他操作权限逻辑重复 + """ + + def test_can_create(self, cluster_permission_obj, project_id, cluster_id): + """测试场景:有集群创建权限(同时有项目查看权限)""" + perm_ctx = ClusterPermCtx(username=roles.ADMIN_USER, project_id=project_id) + assert cluster_permission_obj.can_create(perm_ctx) + + def test_can_not_create(self, cluster_permission_obj, project_id, cluster_id): + """测试场景:无集群创建权限(同时无项目查看权限)""" + perm_ctx = ClusterPermCtx(username=roles.ANONYMOUS_USER, project_id=project_id) + with pytest.raises(PermissionDeniedError) as exec: + cluster_permission_obj.can_create(perm_ctx) + assert exec.value.data['apply_url'] == generate_apply_url( + roles.ANONYMOUS_USER, + [ + ActionResourcesRequest( + ClusterAction.CREATE, + resource_type=ProjectPermission.resource_type, + resources=[project_id], + ), + ActionResourcesRequest( + ProjectAction.VIEW, resource_type=ProjectPermission.resource_type, resources=[project_id] + ), + ], + ) + + def test_can_view(self, cluster_permission_obj, project_id, cluster_id): + """测试场景:有集群查看权限(同时有项目查看权限)""" + perm_ctx = ClusterPermCtx(username=roles.ADMIN_USER, project_id=project_id, cluster_id=cluster_id) + assert cluster_permission_obj.can_view(perm_ctx) + + def test_can_not_view_but_project(self, cluster_permission_obj, project_id, cluster_id): + """测试场景:无集群查看权限(同时有项目查看权限)""" + self._test_can_not_view( + roles.PROJECT_NO_CLUSTER_USER, + cluster_permission_obj, + project_id, + cluster_id, + expected_action_list=[ + ActionResourcesRequest( + ClusterAction.VIEW, + resource_type=cluster_permission_obj.resource_type, + resources=[cluster_id], + parent_chain=[IAMResource(ResourceType.Project, project_id)], + ), + ActionResourcesRequest( + ProjectAction.VIEW, resource_type=ProjectPermission.resource_type, resources=[project_id] + ), + ], + ) + + def test_can_view_but_no_project(self, cluster_permission_obj, project_id, cluster_id): + """测试场景:有集群查看权限(同时无项目查看权限)""" + self._test_can_not_view( + roles.CLUSTER_NO_PROJECT_USER, + cluster_permission_obj, + project_id, + cluster_id, + expected_action_list=[ + ActionResourcesRequest( + ProjectAction.VIEW, + resource_type=ProjectPermission.resource_type, + resources=[project_id], + ) + ], + ) + + def test_can_not_view(self, cluster_permission_obj, project_id, cluster_id): + """测试场景:无集群查看权限(同时无项目查看权限)""" + self._test_can_not_view( + roles.ANONYMOUS_USER, + cluster_permission_obj, + project_id, + cluster_id, + expected_action_list=[ + ActionResourcesRequest( + ClusterAction.VIEW, + resource_type=cluster_permission_obj.resource_type, + resources=[cluster_id], + parent_chain=[IAMResource(ResourceType.Project, project_id)], + ), + ActionResourcesRequest( + ProjectAction.VIEW, + resource_type=ProjectPermission.resource_type, + resources=[project_id], + ), + ], + ) + + def _test_can_not_view(self, username, cluster_permission_obj, project_id, cluster_id, expected_action_list): + perm_ctx = ClusterPermCtx(username=username, project_id=project_id, cluster_id=cluster_id) + with pytest.raises(PermissionDeniedError) as exec: + cluster_permission_obj.can_view(perm_ctx) + assert exec.value.data['apply_url'] == generate_apply_url(username, expected_action_list) + + def test_can_not_manage(self, cluster_permission_obj, project_id, cluster_id): + """测试场景:无集群管理权限(同时无项目查看权限)""" + username = roles.ANONYMOUS_USER + perm_ctx = ClusterPermCtx(username=username, project_id=project_id, cluster_id=cluster_id) + with pytest.raises(PermissionDeniedError) as exec: + cluster_permission_obj.can_manage(perm_ctx) + assert exec.value.data['apply_url'] == generate_apply_url( + username, + [ + ActionResourcesRequest( + ClusterAction.MANAGE, + resource_type=ClusterPermission.resource_type, + resources=[cluster_id], + parent_chain=[IAMResource(ResourceType.Project, project_id)], + ), + ActionResourcesRequest( + ClusterAction.VIEW, + resource_type=ClusterPermission.resource_type, + resources=[cluster_id], + parent_chain=[IAMResource(ResourceType.Project, project_id)], + ), + ActionResourcesRequest( + ProjectAction.VIEW, resource_type=ProjectPermission.resource_type, resources=[project_id] + ), + ], + ) + + def test_can_manage_but_no_project(self, cluster_permission_obj, project_id, cluster_id): + """测试场景:有集群管理权限(同时无项目权限)""" + username = roles.CLUSTER_NO_PROJECT_USER + perm_ctx = ClusterPermCtx(username=username, project_id=project_id, cluster_id=cluster_id) + with pytest.raises(PermissionDeniedError) as exec: + cluster_permission_obj.can_manage(perm_ctx) + assert exec.value.data['apply_url'] == generate_apply_url( + username, + [ + ActionResourcesRequest( + ProjectAction.VIEW, resource_type=ProjectPermission.resource_type, resources=[project_id] + ) + ], + ) + + +@cluster_perm(method_name='can_manage') +def manage_cluster(perm_ctx: ClusterPermCtx): + """""" + + +class TestClusterPermDecorator: + def test_can_manage(self, cluster_permission_obj, project_id, cluster_id): + """测试场景:有集群管理权限(同时有项目查看权限)""" + perm_ctx = ClusterPermCtx(username=roles.ADMIN_USER, project_id=project_id, cluster_id=cluster_id) + manage_cluster(perm_ctx) + + def test_can_not_manage(self, cluster_permission_obj, project_id, cluster_id): + """测试场景:无集群管理权限(同时无项目查看权限)""" + username = roles.ANONYMOUS_USER + perm_ctx = ClusterPermCtx(username=username, project_id=project_id, cluster_id=cluster_id) + with pytest.raises(PermissionDeniedError) as exec: + manage_cluster(perm_ctx) + assert exec.value.data['apply_url'] == generate_apply_url( + username, + [ + ActionResourcesRequest( + ClusterAction.MANAGE, + resource_type=ClusterPermission.resource_type, + resources=[cluster_id], + parent_chain=[IAMResource(ResourceType.Project, project_id)], + ), + ActionResourcesRequest( + ClusterAction.VIEW, + resource_type=ClusterPermission.resource_type, + resources=[cluster_id], + parent_chain=[IAMResource(ResourceType.Project, project_id)], + ), + ActionResourcesRequest( + ProjectAction.VIEW, resource_type=ProjectPermission.resource_type, resources=[project_id] + ), + ], + ) diff --git a/bcs-app/backend/tests/iam/permissions/test_namespace.py b/bcs-app/backend/tests/iam/permissions/test_namespace.py new file mode 100644 index 000000000..d7a6f2085 --- /dev/null +++ b/bcs-app/backend/tests/iam/permissions/test_namespace.py @@ -0,0 +1,206 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community +Edition) 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. +""" +import pytest + +from backend.iam.permissions.exceptions import PermissionDeniedError +from backend.iam.permissions.request import ActionResourcesRequest, IAMResource +from backend.iam.permissions.resources.cluster import ClusterAction, ClusterPermission +from backend.iam.permissions.resources.constants import ResourceType +from backend.iam.permissions.resources.namespace import ( + NamespaceAction, + NamespacePermCtx, + NamespacePermission, + namespace_perm, +) +from backend.iam.permissions.resources.project import ProjectAction, ProjectPermission +from backend.tests.iam.conftest import generate_apply_url + +from . import roles + + +class TestNamespacePermission: + """ + 命名空间资源权限 + note: 仅测试 namespace_use 这一代表性的权限,其他操作权限逻辑重复 + """ + + def test_can_create_but_no_cluster_project(self, namespace_permission_obj, project_id, cluster_id): + """测试场景:有命名空间创建权限(同时无集群使用/查看权限、无项目查看权限)""" + username = roles.NAMESPACE_NO_CLUSTER_PROJECT_USER + perm_ctx = NamespacePermCtx(username=username, project_id=project_id, cluster_id=cluster_id) + with pytest.raises(PermissionDeniedError) as exec: + namespace_permission_obj.can_create(perm_ctx) + assert exec.value.data['apply_url'] == generate_apply_url( + username, + [ + ActionResourcesRequest( + ClusterAction.USE, + resource_type=ClusterPermission.resource_type, + resources=[cluster_id], + parent_chain=[IAMResource(ResourceType.Project, project_id)], + ), + ActionResourcesRequest( + ClusterAction.VIEW, + resource_type=ClusterPermission.resource_type, + resources=[cluster_id], + parent_chain=[IAMResource(ResourceType.Project, project_id)], + ), + ActionResourcesRequest( + ProjectAction.VIEW, resource_type=ProjectPermission.resource_type, resources=[project_id] + ), + ], + ) + + def test_can_not_use(self, namespace_permission_obj, project_id, cluster_id, namespace_name): + """测试场景:无命名空间使用/查看权限(同时无集群和项目权限)""" + username = roles.ANONYMOUS_USER + perm_ctx = NamespacePermCtx( + username=username, project_id=project_id, cluster_id=cluster_id, name=namespace_name + ) + with pytest.raises(PermissionDeniedError) as exec: + namespace_permission_obj.can_use(perm_ctx) + assert exec.value.data['apply_url'] == generate_apply_url( + username, + [ + ActionResourcesRequest( + NamespaceAction.USE, + resource_type=NamespacePermission.resource_type, + resources=[perm_ctx.iam_ns_id], + parent_chain=[ + IAMResource(ResourceType.Project, project_id), + IAMResource(ResourceType.Cluster, cluster_id), + ], + ), + ActionResourcesRequest( + NamespaceAction.VIEW, + resource_type=NamespacePermission.resource_type, + resources=[perm_ctx.iam_ns_id], + parent_chain=[ + IAMResource(ResourceType.Project, project_id), + IAMResource(ResourceType.Cluster, cluster_id), + ], + ), + ActionResourcesRequest( + ClusterAction.USE, + resource_type=ClusterPermission.resource_type, + resources=[cluster_id], + parent_chain=[IAMResource(ResourceType.Project, project_id)], + ), + ActionResourcesRequest( + ClusterAction.VIEW, + resource_type=ClusterPermission.resource_type, + resources=[cluster_id], + parent_chain=[IAMResource(ResourceType.Project, project_id)], + ), + ActionResourcesRequest( + ProjectAction.VIEW, resource_type=ProjectPermission.resource_type, resources=[project_id] + ), + ], + ) + + def test_can_use_but_no_cluster_project(self, namespace_permission_obj, project_id, cluster_id, namespace_name): + """测试场景: 有命名空间使用权限(同时无集群和项目权限)""" + username = roles.NAMESPACE_NO_CLUSTER_PROJECT_USER + perm_ctx = NamespacePermCtx( + username=username, project_id=project_id, cluster_id=cluster_id, name=namespace_name + ) + + # 不抛出异常 + assert not namespace_permission_obj.can_use(perm_ctx, raise_exception=False) + + # 抛出异常 + with pytest.raises(PermissionDeniedError) as exec: + namespace_permission_obj.can_use(perm_ctx) + assert exec.value.data['apply_url'] == generate_apply_url( + username, + [ + ActionResourcesRequest( + ClusterAction.USE, + resource_type=ClusterPermission.resource_type, + resources=[cluster_id], + parent_chain=[IAMResource(ResourceType.Project, project_id)], + ), + ActionResourcesRequest( + ClusterAction.VIEW, + resource_type=ClusterPermission.resource_type, + resources=[cluster_id], + parent_chain=[IAMResource(ResourceType.Project, project_id)], + ), + ActionResourcesRequest( + ProjectAction.VIEW, resource_type=ProjectPermission.resource_type, resources=[project_id] + ), + ], + ) + + +@namespace_perm(method_name='can_use') +def helm_install(perm_ctx: NamespacePermCtx): + """helm install 到某个命名空间""" + + +class TestNamespacePermDecorator: + def test_can_use(self, namespace_permission_obj, project_id, cluster_id, namespace_name): + """测试场景:有命名空间使用权限(同时有集群和项目权限)""" + perm_ctx = NamespacePermCtx( + username=roles.ADMIN_USER, project_id=project_id, cluster_id=cluster_id, name=namespace_name + ) + helm_install(perm_ctx) + + def test_can_not_use(self, namespace_permission_obj, project_id, cluster_id, namespace_name): + """测试场景:无命名空间使用权限(同时无集群和项目权限)""" + username = roles.ANONYMOUS_USER + perm_ctx = NamespacePermCtx( + username=username, project_id=project_id, cluster_id=cluster_id, name=namespace_name + ) + with pytest.raises(PermissionDeniedError) as exec: + helm_install(perm_ctx) + assert exec.value.data['apply_url'] == generate_apply_url( + username, + [ + ActionResourcesRequest( + NamespaceAction.USE, + resource_type=NamespacePermission.resource_type, + resources=[perm_ctx.iam_ns_id], + parent_chain=[ + IAMResource(ResourceType.Project, project_id), + IAMResource(ResourceType.Cluster, cluster_id), + ], + ), + ActionResourcesRequest( + NamespaceAction.VIEW, + resource_type=NamespacePermission.resource_type, + resources=[perm_ctx.iam_ns_id], + parent_chain=[ + IAMResource(ResourceType.Project, project_id), + IAMResource(ResourceType.Cluster, cluster_id), + ], + ), + ActionResourcesRequest( + ClusterAction.USE, + resource_type=ClusterPermission.resource_type, + resources=[cluster_id], + parent_chain=[IAMResource(ResourceType.Project, project_id)], + ), + ActionResourcesRequest( + ClusterAction.VIEW, + resource_type=ClusterPermission.resource_type, + resources=[cluster_id], + parent_chain=[IAMResource(ResourceType.Project, project_id)], + ), + ActionResourcesRequest( + ProjectAction.VIEW, resource_type=ProjectPermission.resource_type, resources=[project_id] + ), + ], + ) diff --git a/bcs-app/backend/tests/iam/permissions/test_project.py b/bcs-app/backend/tests/iam/permissions/test_project.py new file mode 100644 index 000000000..c834be0d2 --- /dev/null +++ b/bcs-app/backend/tests/iam/permissions/test_project.py @@ -0,0 +1,98 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community +Edition) 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. +""" +import pytest + +from backend.iam.permissions.exceptions import PermissionDeniedError +from backend.iam.permissions.perm import ActionResourcesRequest +from backend.iam.permissions.resources.project import ProjectAction, ProjectPermCtx +from backend.tests.iam.conftest import generate_apply_url + +from . import roles + + +class TestProjectPermission: + """ + 测试项目资源权限 + note: 仅测试 project_create 和 project_view 两种代表性的权限,其他操作权限逻辑重复 + """ + + def test_can_create(self, project_permission_obj): + """测试场景:有项目创建权限""" + perm_ctx = ProjectPermCtx(username=roles.ADMIN_USER) + assert project_permission_obj.can_create(perm_ctx) + + def test_can_not_create(self, project_permission_obj): + """测试场景:无项目创建权限""" + + # 无权限不抛出异常 + username = roles.NO_PROJECT_USER + perm_ctx = ProjectPermCtx(username=username) + assert not project_permission_obj.can_create(perm_ctx, raise_exception=False) + + # 无权限抛出异常 + with pytest.raises(PermissionDeniedError) as exec: + project_permission_obj.can_create(perm_ctx) + assert exec.value.code == PermissionDeniedError.code + assert exec.value.data['apply_url'] == generate_apply_url( + username, + action_request_list=[ + ActionResourcesRequest(ProjectAction.CREATE, resource_type=project_permission_obj.resource_type) + ], + ) + + def test_can_view(self, project_permission_obj, project_id): + """测试场景:有项目查看权限""" + perm_ctx = ProjectPermCtx(username=roles.ADMIN_USER, project_id=project_id) + assert project_permission_obj.can_view(perm_ctx) + + def test_can_not_view(self, project_permission_obj, project_id): + """测试场景:无项目查看权限""" + + # 无权限不抛出异常 + username = roles.NO_PROJECT_USER + perm_ctx = ProjectPermCtx(username=username, project_id=project_id) + assert not project_permission_obj.can_view(perm_ctx, raise_exception=False) + + # 无权限抛出异常 + with pytest.raises(PermissionDeniedError) as exec: + project_permission_obj.can_view(perm_ctx) + assert exec.value.code == PermissionDeniedError.code + assert exec.value.data['apply_url'] == generate_apply_url( + username, + [ + ActionResourcesRequest( + ProjectAction.VIEW, + resource_type=project_permission_obj.resource_type, + resources=[project_id], + ) + ], + ) + + def test_can_edit_not_view(self, project_permission_obj, project_id): + """测试场景:有项目编辑权限(同时无项目查看权限)""" + username = roles.PROJECT_NO_VIEW_USER + perm_ctx = ProjectPermCtx(username=username, project_id=project_id) + with pytest.raises(PermissionDeniedError) as exec: + project_permission_obj.can_edit(perm_ctx) + assert exec.value.data['apply_url'] == generate_apply_url( + username, + [ + ActionResourcesRequest( + ProjectAction.VIEW, + resource_type=project_permission_obj.resource_type, + resources=[project_id], + ) + ], + ) diff --git a/bcs-app/backend/tests/iam/permissions/test_templateset.py b/bcs-app/backend/tests/iam/permissions/test_templateset.py new file mode 100644 index 000000000..e3e86aeb0 --- /dev/null +++ b/bcs-app/backend/tests/iam/permissions/test_templateset.py @@ -0,0 +1,136 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community +Edition) 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. +""" +from unittest import mock + +import pytest + +from backend.iam.permissions.exceptions import PermissionDeniedError +from backend.iam.permissions.request import ActionResourcesRequest, IAMResource +from backend.iam.permissions.resources.constants import ResourceType +from backend.iam.permissions.resources.project import ProjectAction, ProjectPermission +from backend.iam.permissions.resources.templateset import ( + TemplatesetAction, + TemplatesetPermCtx, + TemplatesetPermission, + templateset_perm, +) +from backend.tests.iam.conftest import generate_apply_url + +from ..fake_iam import FakeProjectPermission, FakeTemplatesetPermission +from . import roles + + +@pytest.fixture +def templateset_permission_obj(): + templateset_patcher = mock.patch.object(TemplatesetPermission, '__bases__', (FakeTemplatesetPermission,)) + project_patcher = mock.patch.object(ProjectPermission, '__bases__', (FakeProjectPermission,)) + with templateset_patcher, project_patcher: + templateset_patcher.is_local = True # 标注为本地属性,__exit__ 的时候恢复成 patcher.temp_original + project_patcher.is_local = True + yield TemplatesetPermission() + + +class TestTemplatesetPermission: + """ + 模板集资源权限 + note: 仅测试 templateset_instantiate,其他操作权限逻辑重复(和TestClusterPermission类似) + """ + + def test_can_instantiate(self, templateset_permission_obj, project_id, template_id): + """测试场景:有模板集实例化权限""" + username = roles.ADMIN_USER + perm_ctx = TemplatesetPermCtx(username=username, project_id=project_id, template_id=template_id) + assert templateset_permission_obj.can_instantiate(perm_ctx) + + def test_can_instantiate_but_no_project(self, templateset_permission_obj, project_id, template_id): + """测试场景:有模板集实例化权限(同时无项目查看权限)""" + username = roles.TEMPLATESET_NO_PROJECT_USER + perm_ctx = TemplatesetPermCtx(username=username, project_id=project_id, template_id=template_id) + with pytest.raises(PermissionDeniedError) as exec: + templateset_permission_obj.can_instantiate(perm_ctx) + assert exec.value.data['apply_url'] == generate_apply_url( + username, + [ + ActionResourcesRequest( + ProjectAction.VIEW, resource_type=ProjectPermission.resource_type, resources=[project_id] + ) + ], + ) + + def test_can_not_instantiate(self, templateset_permission_obj, project_id, template_id): + """测试场景:无模板集实例化权限(同时无项目查看权限)""" + username = roles.ANONYMOUS_USER + perm_ctx = TemplatesetPermCtx(username=username, project_id=project_id, template_id=template_id) + with pytest.raises(PermissionDeniedError) as exec: + templateset_permission_obj.can_instantiate(perm_ctx) + assert exec.value.data['apply_url'] == generate_apply_url( + username, + [ + ActionResourcesRequest( + TemplatesetAction.INSTANTIATE, + resource_type=TemplatesetPermission.resource_type, + resources=[template_id], + parent_chain=[IAMResource(ResourceType.Project, project_id)], + ), + ActionResourcesRequest( + TemplatesetAction.VIEW, + resource_type=TemplatesetPermission.resource_type, + resources=[template_id], + parent_chain=[IAMResource(ResourceType.Project, project_id)], + ), + ActionResourcesRequest( + ProjectAction.VIEW, resource_type=ProjectPermission.resource_type, resources=[project_id] + ), + ], + ) + + +@templateset_perm(method_name='can_instantiate') +def instantiate_templateset(perm_ctx: TemplatesetPermCtx): + """""" + + +class TestTemplatesetPermDecorator: + def test_can_instantiate(self, templateset_permission_obj, project_id, template_id): + """测试场景:有模板集实例化权限""" + perm_ctx = TemplatesetPermCtx(username=roles.ADMIN_USER, project_id=project_id, template_id=template_id) + instantiate_templateset(perm_ctx) + + def test_can_not_instantiate(self, templateset_permission_obj, project_id, template_id): + """测试场景:无模板集实例化权限(同时无项目查看权限)""" + username = roles.ANONYMOUS_USER + perm_ctx = TemplatesetPermCtx(username=username, project_id=project_id, template_id=template_id) + with pytest.raises(PermissionDeniedError) as exec: + instantiate_templateset(perm_ctx) + assert exec.value.data['apply_url'] == generate_apply_url( + username, + [ + ActionResourcesRequest( + TemplatesetAction.INSTANTIATE, + resource_type=TemplatesetPermission.resource_type, + resources=[template_id], + parent_chain=[IAMResource(ResourceType.Project, project_id)], + ), + ActionResourcesRequest( + TemplatesetAction.VIEW, + resource_type=TemplatesetPermission.resource_type, + resources=[template_id], + parent_chain=[IAMResource(ResourceType.Project, project_id)], + ), + ActionResourcesRequest( + ProjectAction.VIEW, resource_type=ProjectPermission.resource_type, resources=[project_id] + ), + ], + ) diff --git a/bcs-app/backend/tests/resources/test_client.py b/bcs-app/backend/tests/resources/test_client.py index d2b48a2e4..3de58c99c 100644 --- a/bcs-app/backend/tests/resources/test_client.py +++ b/bcs-app/backend/tests/resources/test_client.py @@ -22,6 +22,8 @@ from backend.tests.testing_utils.mocks.paas_cc import StubPaaSCCClient from backend.utils.exceptions import ComponentError +pytestmark = pytest.mark.django_db + @pytest.fixture(autouse=True) def setup_settings(settings): diff --git a/bcs-app/backend/tests/testing_utils/mocks/paas_cc.py b/bcs-app/backend/tests/testing_utils/mocks/paas_cc.py index 3b908af02..1c7682df9 100644 --- a/bcs-app/backend/tests/testing_utils/mocks/paas_cc.py +++ b/bcs-app/backend/tests/testing_utils/mocks/paas_cc.py @@ -28,6 +28,10 @@ def __init__(self, *args, **kwargs): def get_cluster(self, project_id: str, cluster_id: str) -> Dict: return self.wrap_resp(self.make_cluster_data(project_id, cluster_id)) + @mockable_function + def get_cluster_by_id(self, cluster_id: str) -> Dict: + return self.make_cluster_data_by_id(cluster_id) + @mockable_function def get_project(self, project_id: str) -> Dict: return self.make_project_data(project_id) @@ -80,6 +84,40 @@ def make_cluster_data(project_id: str, cluster_id: str): 'updated_at': _stub_time, } + @staticmethod + def make_cluster_data_by_id(cluster_id: str): + _stub_time = '2021-01-01T00:00:00+08:00' + return { + 'area_id': 1, + 'artifactory': '', + 'capacity_updated_at': _stub_time, + 'cluster_id': cluster_id, + 'cluster_num': 1, + 'config_svr_count': 0, + 'created_at': _stub_time, + 'creator': 'unknown', + 'description': 'cluster description', + 'disabled': False, + 'environment': 'stag', + 'extra_cluster_id': '', + 'ip_resource_total': 0, + 'ip_resource_used': 0, + 'master_count': 0, + 'name': 'test-cluster', + 'need_nat': True, + 'node_count': 1, + 'project_id': uuid.uuid4().hex, + 'remain_cpu': 10, + 'remain_disk': 0, + 'remain_mem': 10, + 'status': 'normal', + 'total_cpu': 12, + 'total_disk': 0, + 'total_mem': 64, + 'type': 'k8s', + 'updated_at': _stub_time, + } + @staticmethod def make_project_data(project_id: str): _stub_time = '2021-01-01T00:00:00+08:00' diff --git a/bcs-app/backend/utils/async_run.py b/bcs-app/backend/utils/async_run.py index 86c82d858..d1aebcb23 100644 --- a/bcs-app/backend/utils/async_run.py +++ b/bcs-app/backend/utils/async_run.py @@ -47,6 +47,8 @@ def async_run(tasks, raise_exception: bool = True) -> List[AsyncResult]: run a group of tasks async(仅适用于IO密集型) Requires the tasks arg to be a list of functools.partial() """ + if not tasks: + return [] loop, created = get_or_create_loop() # https://github.com/python/asyncio/issues/258 diff --git a/bcs-app/backend/utils/error_codes.py b/bcs-app/backend/utils/error_codes.py index 433063203..76a600e57 100644 --- a/bcs-app/backend/utils/error_codes.py +++ b/bcs-app/backend/utils/error_codes.py @@ -48,6 +48,7 @@ class ErrorCodes: JSONParseError = ErrorCode(_("解析异常")) DBOperError = ErrorCode(_("DB操作异常")) + # TODO 禁用 APIError,该 ErrorCode 定义过于模糊,容易误用,考虑后续去除 APIError = ErrorCode(_('请求失败'), code_num=40001) NoBCSService = ErrorCode(_('该项目没有使用蓝鲸容器服务'), code_num=416) ValidateError = ErrorCode(_('参数不正确'), code_num=40002) @@ -69,10 +70,6 @@ class ErrorCodes: code_num=40101, status_code=status.HTTP_401_UNAUTHORIZED, ) - # 没有权限,最好使用drf permission class检查权限 - Forbidden = ErrorCode(_('没有使用权限'), code_num=40301, status_code=status.HTTP_403_FORBIDDEN) - # 权限中心错误码 - IAMCheckFailed = ErrorCode(_('权限校验失败'), code_num=40302, status_code=status.HTTP_403_FORBIDDEN) # 资源未找到 ResNotFoundError = ErrorCode(_('资源未找到'), code_num=40400, status_code=status.HTTP_404_NOT_FOUND) diff --git a/bcs-app/backend/utils/permissions.py b/bcs-app/backend/utils/permissions.py index c780a608b..21f3fc1bd 100644 --- a/bcs-app/backend/utils/permissions.py +++ b/bcs-app/backend/utils/permissions.py @@ -16,8 +16,8 @@ from rest_framework.permissions import BasePermission from backend.accounts import bcs_perm -from backend.bcs_web.iam import permissions from backend.components import paas_auth, paas_cc +from backend.iam import legacy_perms as permissions from backend.utils import FancyDict from backend.utils.cache import region from backend.utils.error_codes import error_codes diff --git a/bcs-app/backend/utils/views.py b/bcs-app/backend/utils/views.py index a8a06f700..7315a94af 100644 --- a/bcs-app/backend/utils/views.py +++ b/bcs-app/backend/utils/views.py @@ -394,5 +394,14 @@ def cached_project_kind(project_code): return Response(context, headers=headers) -class LoginSuccessView(VueTemplateView): +class LoginSuccessView(APIView): template_name = f"{settings.REGION}/login_success.html" + renderer_classes = [TemplateHTMLRenderer] + # 去掉权限控制 + permission_classes = () + + @xframe_options_exempt + def get(self, request): + # 去除开头的 . document.domain需要 + context = {"SESSION_COOKIE_DOMAIN": settings.SESSION_COOKIE_DOMAIN.lstrip(".")} + return Response(context) diff --git a/bcs-app/backend/utils/whitelist.py b/bcs-app/backend/utils/whitelist.py new file mode 100644 index 000000000..24e19974e --- /dev/null +++ b/bcs-app/backend/utils/whitelist.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community +Edition) 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. +""" +# TODO: apps/whitelist是否可以统一 +from backend.utils.func_controller import get_func_controller + + +def check_bcs_api_gateway_enabled(cluster_id: str) -> bool: + """校验是否通过 bcs-api-gateway 链路访问集群 apiserver""" + func_code = "BCS_API_GATEWAY_FOR_CLUSTER" + enabled, wlist = get_func_controller(func_code) + return enabled or cluster_id in wlist + + +def check_app_access_webconsole_enable(app_code: str, project_id_or_code: str) -> bool: + """APP是否可以访问webconsole接口 + NOTE:存储内容包含app_code和project信息(包含project_code和project_id),格式app_code:project_id_or_code + """ + func_code = "APP_ACCESS_WEBCONSOLE" + enabled, wlist = get_func_controller(func_code) + return enabled or f"{app_code}:{project_id_or_code}" in wlist diff --git a/bcs-app/backend/web_console/pod_life_cycle.py b/bcs-app/backend/web_console/pod_life_cycle.py index 015dc9c0a..332f0f1fb 100644 --- a/bcs-app/backend/web_console/pod_life_cycle.py +++ b/bcs-app/backend/web_console/pod_life_cycle.py @@ -18,6 +18,7 @@ import shlex import time from concurrent.futures import ThreadPoolExecutor +from typing import Optional import yaml from django.conf import settings @@ -250,7 +251,7 @@ def wait_user_pod_ready(ctx, name): raise PodLifeError(_("申请pod资源超时,请稍后再试{}").format(settings.COMMON_EXCEPTION_MSG)) -def get_service_account_token(k8s_client): +def get_service_account_token(k8s_client) -> Optional[str]: """获取web-console token""" if settings.REGION not in ["ee", "ce"]: return @@ -260,7 +261,7 @@ def get_service_account_token(k8s_client): if not item.metadata.name.startswith(token_prefix): continue - return base64.b64decode(item.data["token"]) + return smart_str(base64.b64decode(item.data["token"])) def create_service_account_rbac(k8s_client, ctx): diff --git a/bcs-app/backend/web_console/rest_api/views.py b/bcs-app/backend/web_console/rest_api/views.py index 3428b535c..ffbd0d66f 100644 --- a/bcs-app/backend/web_console/rest_api/views.py +++ b/bcs-app/backend/web_console/rest_api/views.py @@ -78,11 +78,11 @@ def get_k8s_cluster_context(self, request, project_id, cluster_id, client, bcs_c pod = pod_life_cycle.ensure_pod(ctx) logger.debug("get pod %s", pod) except pod_life_cycle.PodLifeError as error: - logger.error("kubetctl apply error: %s", error) + logger.error("apply error: %s", error) utils.activity_log(project_id, cluster_id, self.cluster_name, request.user.username, False, "%s" % error) raise error_codes.APIError("%s" % error) except Exception as error: - logger.exception("kubetctl apply error: %s", error) + logger.exception("apply error: %s", error) utils.activity_log(project_id, cluster_id, self.cluster_name, request.user.username, False, "申请pod资源失败") raise error_codes.APIError(_("申请pod资源失败,请稍后再试{}").format(settings.COMMON_EXCEPTION_MSG)) diff --git a/bcs-app/frontend/build/webpack.dev.conf.js b/bcs-app/frontend/build/webpack.dev.conf.js index 5fbf4a82f..d70bcb260 100644 --- a/bcs-app/frontend/build/webpack.dev.conf.js +++ b/bcs-app/frontend/build/webpack.dev.conf.js @@ -17,7 +17,7 @@ const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin') const MonacoEditorPlugin = require('monaco-editor-webpack-plugin') // const CircularDependencyPlugin = require('circular-dependency-plugin') const threadLoader = require('thread-loader') - +const CopyWebpackPlugin = require('copy-webpack-plugin') const config = require('./config') const baseWebpackConfig = require('./webpack.base.conf') const manifest = require('../static/lib-manifest.json') @@ -125,6 +125,13 @@ const webpackConfig = merge(baseWebpackConfig, { new webpack.HotModuleReplacementPlugin(), new webpack.NoEmitOnErrorsPlugin(), + new CopyWebpackPlugin([ + { + from: resolve(__dirname, '../login_success.html'), + to: config.dev.assetsSubDirectory, + ignore: ['.*'] + } + ]), new HtmlWebpackPlugin({ filename: 'index.html', diff --git a/bcs-app/frontend/login_success.html b/bcs-app/frontend/login_success.html index 5927860f9..872db6a34 100644 --- a/bcs-app/frontend/login_success.html +++ b/bcs-app/frontend/login_success.html @@ -85,12 +85,13 @@

登录成功

document.domain = '{{ SESSION_COOKIE_DOMAIN }}' document.addEventListener('DOMContentLoaded', function () { - var is_ajax = window.location.href.match(/is\_ajax\=(\d+)/) - if (is_ajax && is_ajax[1]) { - parent.bus.$emit('close-login-modal') - } else { - console.warn('#TODO 实现页面跳转') - } + parent.postMessage('closeLoginModal') + // var is_ajax = window.location.href.match(/is\_ajax\=(\d+)/) + // if (is_ajax && is_ajax[1]) { + // parent.bus.$emit('close-login-modal') + // } else { + // console.warn('#TODO 实现页面跳转') + // } }) diff --git a/bcs-app/frontend/src/App.vue b/bcs-app/frontend/src/App.vue index c295612d4..753b1d181 100644 --- a/bcs-app/frontend/src/App.vue +++ b/bcs-app/frontend/src/App.vue @@ -1,20 +1,21 @@ - diff --git a/bcs-app/frontend/src/views/cluster/info.vue b/bcs-app/frontend/src/views/cluster/info.vue index 2269d67b9..22f8139e8 100644 --- a/bcs-app/frontend/src/views/cluster/info.vue +++ b/bcs-app/frontend/src/views/cluster/info.vue @@ -660,7 +660,7 @@ clusterId: this.curCluster.cluster_id // 这里用 this.curCluster 来获取是为了使计算属性生效 }) const cpu = res.data.cpu_usage || {} - const mem = res.data.mem_usage || {} + const mem = res.data.memory_usage || {} const cpuTotal = cpu.total || 0 const memTotal = formatBytes(mem.total_bytes) || 0 diff --git a/bcs-app/frontend/src/views/cluster/node-overview.vue b/bcs-app/frontend/src/views/cluster/node-overview.vue index f10cac224..491bcd040 100644 --- a/bcs-app/frontend/src/views/cluster/node-overview.vue +++ b/bcs-app/frontend/src/views/cluster/node-overview.vue @@ -867,7 +867,7 @@ }, itemStyle: { normal: { - color: '#853cff' + color: '#3dda80' } }, data: item.values diff --git a/bcs-app/frontend/src/views/cluster/node.external.vue b/bcs-app/frontend/src/views/cluster/node.external.vue index 9ff1a89c8..f2bf9b788 100644 --- a/bcs-app/frontend/src/views/cluster/node.external.vue +++ b/bcs-app/frontend/src/views/cluster/node.external.vue @@ -131,7 +131,7 @@ {{$t('主机名/IP')}} - {{$t('状态')}} + {{$t('状态')}} {{$t('容器数量')}}