From 19ed8499eb686f7c0a3047dcf4c1ff3707132aab Mon Sep 17 00:00:00 2001 From: wklken Date: Sat, 3 Sep 2022 09:02:09 +0800 Subject: [PATCH 1/2] v2.3.6 (#620) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增: 账号过期时间 - 新增: 新增账户冻结周期任务 - 新增: 密码过期提醒 - bugfix: DDE注入漏洞解决,设置导入导出模板单元格为文本模式 Co-authored-by: Canway-shiisa <90179140+Canway-shiisa@users.noreply.github.com> Co-authored-by: v_yutyi Co-authored-by: Canway-jessonwang <101160580+Canway-jessonwang@users.noreply.github.com> Co-authored-by: nero --- deploy/helm/bk-user/Chart.yaml | 10 +- deploy/helm/bk-user/README.md | 41 +- deploy/helm/bk-user/charts/api/Chart.yaml | 2 +- .../bk-user/charts/api/templates/_helpers.tpl | 7 + .../api/templates/general-envs-configmap.yaml | 7 + deploy/helm/bk-user/charts/api/values.yaml | 19 +- deploy/helm/bk-user/charts/login/Chart.yaml | 2 +- .../charts/login/templates/_helpers.tpl | 7 + .../templates/general-envs-configmap.yaml | 7 + deploy/helm/bk-user/charts/login/values.yaml | 20 +- deploy/helm/bk-user/charts/saas/Chart.yaml | 2 +- .../charts/saas/templates/_helpers.tpl | 7 + .../templates/general-envs-configmap.yaml | 7 + deploy/helm/bk-user/charts/saas/values.yaml | 20 +- deploy/helm/bk-user/values.yaml | 11 + src/api/bin/sync_apigateway.sh | 2 +- src/api/bkuser_core/audit/constants.py | 2 + .../migrations/0005_auto_20220526_1048.py | 23 + src/api/bkuser_core/bkiam/constants.py | 6 +- .../make_crontab_sync_task_for_category.py | 2 + .../management/commands/test_category_sync.py | 3 + .../bkuser_core/categories/plugins/base.py | 2 +- .../categories/plugins/custom/helpers.py | 21 +- .../categories/plugins/ldap/__init__.py | 2 + .../categories/plugins/ldap/adaptor.py | 6 +- .../categories/plugins/ldap/client.py | 1 + .../categories/plugins/ldap/handlers.py | 9 +- .../categories/plugins/ldap/helper.py | 4 +- .../categories/plugins/ldap/login.py | 14 + .../categories/plugins/ldap/syncer.py | 23 +- .../categories/plugins/local/handlers.py | 1 + .../categories/plugins/local/syncer.py | 26 +- src/api/bkuser_core/categories/views.py | 24 +- src/api/bkuser_core/common/db_sync.py | 73 +- src/api/bkuser_core/common/error_codes.py | 1 + src/api/bkuser_core/common/notifier.py | 39 +- src/api/bkuser_core/config/common/system.py | 31 +- src/api/bkuser_core/departments/v2/views.py | 4 + src/api/bkuser_core/departments/v3/views.py | 9 +- src/api/bkuser_core/monitoring/apps.py | 1 + .../profiles/account_expiration_notifier.py | 55 + src/api/bkuser_core/profiles/constants.py | 21 +- src/api/bkuser_core/profiles/exceptions.py | 4 + .../migrations/0022_auto_20220520_1028.py | 35 + ...cal_profile_add_account_expiration_time.py | 61 + .../migrations/0024_expirationnoticerecord.py | 30 + ...namic_field_add_account_expiration_date.py | 73 + src/api/bkuser_core/profiles/models.py | 26 +- src/api/bkuser_core/profiles/notifier.py | 152 + .../profiles/password_expiration_notifier.py | 64 + src/api/bkuser_core/profiles/tasks.py | 196 +- src/api/bkuser_core/profiles/urls.py | 1 + src/api/bkuser_core/profiles/utils.py | 70 +- .../bkuser_core/profiles/v2/serializers.py | 16 +- src/api/bkuser_core/profiles/v2/views.py | 97 +- src/api/bkuser_core/profiles/v3/views.py | 9 +- .../tests/apis/v2/iam/test_field.py | 10 +- .../v2/user_settings/test_settings_list.py | 1 + .../bkuser_core/tests/common/test_db_sync.py | 2 +- .../bkuser_core/user_settings/constants.py | 6 + .../migrations/0002_auto_20191104_1600.py | 2 +- .../0015_alter_settingmeta_namespace.py | 18 + ...016_add_default_fields_account_settings.py | 142 + ...17_add_default_fields_password_settings.py | 155 + src/api/bkuser_core/user_settings/views.py | 46 + src/api/poetry.lock | 17 +- src/api/pyproject.toml | 4 +- src/bkuser_global/tracing/instrumentor.py | 19 +- src/bkuser_global/tracing/otel.py | 44 +- src/login/bin/start.sh | 3 +- src/login/bklogin/backends/bk.py | 5 +- src/login/bklogin/bkauth/views.py | 7 +- src/login/bklogin/common/exceptions.py | 11 + src/login/bklogin/config/common/system.py | 12 + src/login/bklogin/monitoring/apps.py | 2 + .../bklogin/templates/account/login_ce.html | 10 +- src/login/poetry.lock | 836 +- src/login/pyproject.toml | 14 +- src/login/static/css_ce/login.css | 43 + src/login/static/css_ce/login.min.css | 58 +- src/login/static/js_ce/login.js | 8 + src/login/static/js_ce/login.min.js | 2 +- src/pages/package.json | 6 +- src/pages/src/common/util.js | 34 + .../operation/NotifyEditorTemplate.vue | 325 + .../catalog/operation/SetAccount.vue | 366 + .../catalog/operation/SetPassword.vue | 819 +- .../catalog/operation/editorTemplate.vue | 96 + src/pages/src/language/lang/en.js | 30 +- src/pages/src/language/lang/zh.js | 30 +- src/pages/src/plugins/methods.js | 28 + src/pages/src/store/modules/catalog.js | 11 + .../src/views/catalog/operation/LocalAdd.vue | 35 +- .../src/views/catalog/operation/LocalSet.vue | 66 +- .../views/organization/details/DetailsBar.vue | 14 +- .../organization/details/InputComponents.vue | 3 +- .../views/organization/details/InputDate.vue | 19 + .../organization/details/InputSelect.vue | 2 +- .../organization/details/InputString.vue | 11 + .../organization/details/UserMaterial.vue | 91 +- src/pages/src/views/organization/index.vue | 18 +- .../views/organization/table/UserTable.vue | 14 +- .../organization/tree/OrganizationTree.vue | 24 +- src/pages/src/views/setting/FieldsSetting.vue | 3 +- src/pages/yarn.lock | 13655 ++++++++++++++++ src/saas/RELEASE.yaml | 11 +- src/saas/bkuser_shell/apis/viewset.py | 5 +- src/saas/bkuser_shell/audit/constants.py | 1 + src/saas/bkuser_shell/common/export.py | 26 +- .../bkuser_shell/config/common/platform.py | 8 +- src/saas/bkuser_shell/config/common/system.py | 8 +- src/saas/bkuser_shell/config_center/views.py | 15 +- .../bkuser_shell/organization/constants.py | 8 + .../organization/serializers/profiles.py | 7 + .../organization/views/departments.py | 4 +- .../bkuser_shell/organization/views/misc.py | 2 +- .../organization/views/profiles.py | 26 +- src/saas/media/excel/export_org_tmpl.xlsx | Bin 10631 -> 10709 bytes src/saas/poetry.lock | 13 +- src/saas/pyproject.toml | 2 +- src/sdk/bkuser_sdk/models/profile.py | 30 +- 121 files changed, 18111 insertions(+), 640 deletions(-) create mode 100644 src/api/bkuser_core/audit/migrations/0005_auto_20220526_1048.py create mode 100644 src/api/bkuser_core/profiles/account_expiration_notifier.py create mode 100644 src/api/bkuser_core/profiles/migrations/0022_auto_20220520_1028.py create mode 100644 src/api/bkuser_core/profiles/migrations/0023_local_profile_add_account_expiration_time.py create mode 100644 src/api/bkuser_core/profiles/migrations/0024_expirationnoticerecord.py create mode 100644 src/api/bkuser_core/profiles/migrations/0025_dynamic_field_add_account_expiration_date.py create mode 100644 src/api/bkuser_core/profiles/notifier.py create mode 100644 src/api/bkuser_core/profiles/password_expiration_notifier.py create mode 100644 src/api/bkuser_core/user_settings/migrations/0015_alter_settingmeta_namespace.py create mode 100644 src/api/bkuser_core/user_settings/migrations/0016_add_default_fields_account_settings.py create mode 100644 src/api/bkuser_core/user_settings/migrations/0017_add_default_fields_password_settings.py create mode 100644 src/pages/src/components/catalog/operation/NotifyEditorTemplate.vue create mode 100644 src/pages/src/components/catalog/operation/SetAccount.vue create mode 100644 src/pages/src/components/catalog/operation/editorTemplate.vue create mode 100644 src/pages/yarn.lock diff --git a/deploy/helm/bk-user/Chart.yaml b/deploy/helm/bk-user/Chart.yaml index fd445c51f..6958550cd 100644 --- a/deploy/helm/bk-user/Chart.yaml +++ b/deploy/helm/bk-user/Chart.yaml @@ -2,8 +2,8 @@ apiVersion: v2 name: bk-user description: A Helm chart for bk-user type: application -version: 1.2.20 -appVersion: "v2.3.5" +version: 1.2.21 +appVersion: "v2.3.6-beta.3" dependencies: @@ -18,13 +18,13 @@ dependencies: condition: redis.enabled - name: api - version: "1.0.0" + version: "1.0.1" condition: api.enabled - name: login - version: "1.0.0" + version: "1.0.1" condition: login.enabled - name: saas - version: "1.0.0" + version: "1.0.1" condition: saas.enabled diff --git a/deploy/helm/bk-user/README.md b/deploy/helm/bk-user/README.md index d900e9af3..523f937c4 100644 --- a/deploy/helm/bk-user/README.md +++ b/deploy/helm/bk-user/README.md @@ -211,7 +211,40 @@ global: enabled: true ``` -### 9. 配置sentry +### 9. 调用链 APM + +```yaml +global: + trace: + enabled: false + otlp: + host: 127.0.0.1 + port: 4317 + token: "" + type: grpc +api: + trace: + serviceName: "bk-user-api" + sampler: always_on + instrument: + dbApi: false + +saas: + trace: + serviceName: "bk-user-saas" + sampler: always_on + instrument: + dbApi: false + +login: + trace: + serviceName: "bk-login" + sampler: always_on + instrument: + dbApi: false +``` + +### 10. 配置sentry ```yaml global: @@ -219,7 +252,7 @@ global: sentryDsn: "http://12927b5f211046b575ee51fd8b1ac34f@{SENTRY_DOMAIN}/{PROJECT_ID}" ``` -### 10. 开启api auth +### 11. 开启api auth 默认值是true, 可以关闭, 关闭之后用户管理 API 将不受任何保护 @@ -239,7 +272,7 @@ login: bkComponentApiUrl: "http://bkapi.example.com" ``` -### 11. 环境变量注入 +### 12. 环境变量注入 ```yaml @@ -270,7 +303,7 @@ api: value: true ``` -### 12. 安装 +### 13. 安装 如果你已经准备好了 `values.yaml`,就可以直接进行安装操作了 diff --git a/deploy/helm/bk-user/charts/api/Chart.yaml b/deploy/helm/bk-user/charts/api/Chart.yaml index 61ff577b0..51665f2ed 100644 --- a/deploy/helm/bk-user/charts/api/Chart.yaml +++ b/deploy/helm/bk-user/charts/api/Chart.yaml @@ -3,4 +3,4 @@ name: api description: Api module for bk-user type: application version: 1.0.1 -appVersion: "v2.3.5" +appVersion: "v2.3.6-beta.3" diff --git a/deploy/helm/bk-user/charts/api/templates/_helpers.tpl b/deploy/helm/bk-user/charts/api/templates/_helpers.tpl index 645b58786..66dacf988 100644 --- a/deploy/helm/bk-user/charts/api/templates/_helpers.tpl +++ b/deploy/helm/bk-user/charts/api/templates/_helpers.tpl @@ -60,3 +60,10 @@ Create the name of the service account to use {{- default "default" .Values.serviceAccount.name }} {{- end }} {{- end }} + +{{/* +bk_monitor provides grpc url must add http protocol header +*/}} +{{- define "bk-user.trace.grpcUrl" -}} +"http://{{ .Values.global.trace.otlp.host }}:{{ .Values.global.trace.otlp.port }}" +{{- end -}} \ No newline at end of file diff --git a/deploy/helm/bk-user/charts/api/templates/general-envs-configmap.yaml b/deploy/helm/bk-user/charts/api/templates/general-envs-configmap.yaml index 081a2f98f..67dec9762 100644 --- a/deploy/helm/bk-user/charts/api/templates/general-envs-configmap.yaml +++ b/deploy/helm/bk-user/charts/api/templates/general-envs-configmap.yaml @@ -38,3 +38,10 @@ data: SENTRY_DSN: "{{ .Values.global.sentryDsn }}" # APIGateway url模板 BK_API_URL_TMPL: "{{ .Values.bkApiUrlTmpl }}" + # 蓝鲸调用链 + BKAPP_ENABLE_OTEL_TRACE: "{{ .Values.global.trace.enabled }}" + BKAPP_OTEL_SERVICE_NAME: "{{ .Values.trace.serviceName }}" + BKAPP_OTEL_SAMPLER: "{{ .Values.trace.sampler }}" + BKAPP_OTEL_GRPC_HOST: {{ include "bk-user.trace.grpcUrl" . }} + BKAPP_OTEL_DATA_TOKEN: "{{ .Values.global.trace.otlp.token }}" + BKAPP_OTEL_INSTRUMENT_DB_API: "{{ .Values.trace.instrument.dbApi }}" \ No newline at end of file diff --git a/deploy/helm/bk-user/charts/api/values.yaml b/deploy/helm/bk-user/charts/api/values.yaml index 344c02427..fa6ca2a1c 100644 --- a/deploy/helm/bk-user/charts/api/values.yaml +++ b/deploy/helm/bk-user/charts/api/values.yaml @@ -70,6 +70,17 @@ global: enabled: false dataId: 1 + ## -------------- + ## 蓝鲸调用链 + ## -------------- + trace: + enabled: false + otlp: + host: 127.0.0.1 + port: 4317 + token: "" + type: grpc + ## web deployment 副本数 replicaCount: 1 ## celery deployment 副本数 @@ -89,7 +100,7 @@ image: registry: hub.bktencent.com repository: blueking/bk-user-api pullPolicy: IfNotPresent - tag: "v2.3.5" + tag: "v2.3.6-beta.3" nameOverride: "" fullnameOverride: "" @@ -124,6 +135,12 @@ bkIamUrl: http://bkiam.example.com ## 蓝鲸权限中心后台 API 地址 bkIamApiUrl: http://bkiam-web +## 蓝鲸调用链 +trace: + serviceName: "bk-user-api" + sampler: always_on + instrument: + dbApi: false ## --------------- ## 环境变量 diff --git a/deploy/helm/bk-user/charts/login/Chart.yaml b/deploy/helm/bk-user/charts/login/Chart.yaml index 52722d72e..d7f9f3c01 100644 --- a/deploy/helm/bk-user/charts/login/Chart.yaml +++ b/deploy/helm/bk-user/charts/login/Chart.yaml @@ -3,4 +3,4 @@ name: login description: login module for blueking type: application version: 1.0.1 -appVersion: "v2.3.5" +appVersion: "v2.3.6-beta.3" diff --git a/deploy/helm/bk-user/charts/login/templates/_helpers.tpl b/deploy/helm/bk-user/charts/login/templates/_helpers.tpl index 645b58786..66dacf988 100644 --- a/deploy/helm/bk-user/charts/login/templates/_helpers.tpl +++ b/deploy/helm/bk-user/charts/login/templates/_helpers.tpl @@ -60,3 +60,10 @@ Create the name of the service account to use {{- default "default" .Values.serviceAccount.name }} {{- end }} {{- end }} + +{{/* +bk_monitor provides grpc url must add http protocol header +*/}} +{{- define "bk-user.trace.grpcUrl" -}} +"http://{{ .Values.global.trace.otlp.host }}:{{ .Values.global.trace.otlp.port }}" +{{- end -}} \ No newline at end of file diff --git a/deploy/helm/bk-user/charts/login/templates/general-envs-configmap.yaml b/deploy/helm/bk-user/charts/login/templates/general-envs-configmap.yaml index 9aaa3c79f..6e688c8f4 100644 --- a/deploy/helm/bk-user/charts/login/templates/general-envs-configmap.yaml +++ b/deploy/helm/bk-user/charts/login/templates/general-envs-configmap.yaml @@ -24,3 +24,10 @@ data: SENTRY_DSN: "{{ .Values.global.sentryDsn }}" # Login API Auth Enabled 登录是否开启了 API 认证 BK_LOGIN_API_AUTH_ENABLED: "{{ .Values.bkLoginApiAuthEnabled }}" + # 蓝鲸调用链 + BKAPP_ENABLE_OTEL_TRACE: "{{ .Values.global.trace.enabled }}" + BKAPP_OTEL_SERVICE_NAME: "{{ .Values.trace.serviceName }}" + BKAPP_OTEL_SAMPLER: "{{ .Values.trace.sampler }}" + BKAPP_OTEL_GRPC_HOST: {{ include "bk-user.trace.grpcUrl" . }} + BKAPP_OTEL_DATA_TOKEN: "{{ .Values.global.trace.otlp.token }}" + BKAPP_OTEL_INSTRUMENT_DB_API: "{{ .Values.trace.instrument.dbApi }}" diff --git a/deploy/helm/bk-user/charts/login/values.yaml b/deploy/helm/bk-user/charts/login/values.yaml index 7ccf29a05..c928013ab 100644 --- a/deploy/helm/bk-user/charts/login/values.yaml +++ b/deploy/helm/bk-user/charts/login/values.yaml @@ -65,6 +65,17 @@ global: enabled: false dataId: 1 + ## -------------- + ## 蓝鲸调用链 + ## -------------- + trace: + enabled: false + otlp: + host: 127.0.0.1 + port: 4317 + token: "" + type: grpc + ## web deployment 副本数 replicaCount: 1 ## celery deployment 副本数 @@ -83,7 +94,7 @@ image: registry: hub.bktencent.com repository: blueking/bk-login pullPolicy: IfNotPresent - tag: "v2.3.5" + tag: "v2.3.6-beta.3" nameOverride: "" fullnameOverride: "" @@ -110,6 +121,13 @@ bkUserAddr: bkuser.paas.example.com ## 蓝鲸用户管理后台 API 地址 bkUserApiUrl: http://bkuserapi-web +## 蓝鲸调用链 +trace: + serviceName: "bk-login" + sampler: always_on + instrument: + dbApi: false + ## --------------- ## 环境变量 ## --------------- diff --git a/deploy/helm/bk-user/charts/saas/Chart.yaml b/deploy/helm/bk-user/charts/saas/Chart.yaml index e56610bb1..b56e2a8ac 100644 --- a/deploy/helm/bk-user/charts/saas/Chart.yaml +++ b/deploy/helm/bk-user/charts/saas/Chart.yaml @@ -3,4 +3,4 @@ name: saas description: SaaS module for bk-user type: application version: 1.0.1 -appVersion: "v2.3.5" +appVersion: "v2.3.6-beta.3" diff --git a/deploy/helm/bk-user/charts/saas/templates/_helpers.tpl b/deploy/helm/bk-user/charts/saas/templates/_helpers.tpl index 645b58786..66dacf988 100644 --- a/deploy/helm/bk-user/charts/saas/templates/_helpers.tpl +++ b/deploy/helm/bk-user/charts/saas/templates/_helpers.tpl @@ -60,3 +60,10 @@ Create the name of the service account to use {{- default "default" .Values.serviceAccount.name }} {{- end }} {{- end }} + +{{/* +bk_monitor provides grpc url must add http protocol header +*/}} +{{- define "bk-user.trace.grpcUrl" -}} +"http://{{ .Values.global.trace.otlp.host }}:{{ .Values.global.trace.otlp.port }}" +{{- end -}} \ No newline at end of file diff --git a/deploy/helm/bk-user/charts/saas/templates/general-envs-configmap.yaml b/deploy/helm/bk-user/charts/saas/templates/general-envs-configmap.yaml index fa0db3b89..0f22652ee 100644 --- a/deploy/helm/bk-user/charts/saas/templates/general-envs-configmap.yaml +++ b/deploy/helm/bk-user/charts/saas/templates/general-envs-configmap.yaml @@ -30,3 +30,10 @@ data: ENABLE_IAM: "{{ .Values.global.enableIAM }}" # Sentry DSN配置, 非空则开启 SENTRY_DSN: "{{ .Values.global.sentryDsn }}" + # 蓝鲸调用链 + BKAPP_ENABLE_OTEL_TRACE: "{{ .Values.global.trace.enabled }}" + BKAPP_OTEL_SERVICE_NAME: "{{ .Values.trace.serviceName }}" + BKAPP_OTEL_SAMPLER: "{{ .Values.trace.sampler }}" + BKAPP_OTEL_GRPC_HOST: {{ include "bk-user.trace.grpcUrl" . }} + BKAPP_OTEL_DATA_TOKEN: "{{ .Values.global.trace.otlp.token }}" + BKAPP_OTEL_INSTRUMENT_DB_API: "{{ .Values.trace.instrument.dbApi }}" diff --git a/deploy/helm/bk-user/charts/saas/values.yaml b/deploy/helm/bk-user/charts/saas/values.yaml index c07fc27c0..e085605a6 100644 --- a/deploy/helm/bk-user/charts/saas/values.yaml +++ b/deploy/helm/bk-user/charts/saas/values.yaml @@ -69,6 +69,17 @@ global: enabled: false dataId: 1 + ## -------------- + ## 蓝鲸调用链 + ## -------------- + trace: + enabled: false + otlp: + host: 127.0.0.1 + port: 4317 + token: "" + type: grpc + ## web deployment 副本数 replicaCount: 1 ## celery deployment 副本数 @@ -81,7 +92,7 @@ image: registry: hub.bktencent.com repository: blueking/bk-user-saas pullPolicy: IfNotPresent - tag: "v2.3.5" + tag: "v2.3.6-beta.3" command: [] args: [] @@ -111,6 +122,13 @@ bkUserAddr: bkuser.example.com ## 蓝鲸用户管理后台 API 地址 bkUserApiUrl: http://bkuserapi-web +## 蓝鲸调用链 +trace: + serviceName: "bk-user-saas" + sampler: always_on + instrument: + dbApi: false + ## --------------- ## 环境变量 ## --------------- diff --git a/deploy/helm/bk-user/values.yaml b/deploy/helm/bk-user/values.yaml index f0ff08333..ce41aefda 100644 --- a/deploy/helm/bk-user/values.yaml +++ b/deploy/helm/bk-user/values.yaml @@ -35,6 +35,17 @@ global: enabled: false dataId: 1 + ## -------------- + ## 蓝鲸调用链 + ## -------------- + trace: + enabled: false + otlp: + host: 127.0.0.1 + port: 4317 + token: "" + type: grpc + api: enabled: true bkApiUrlTmpl: "http://bkapi.example.com/api/{api_name}" diff --git a/src/api/bin/sync_apigateway.sh b/src/api/bin/sync_apigateway.sh index d6ffb36e0..f03763856 100755 --- a/src/api/bin/sync_apigateway.sh +++ b/src/api/bin/sync_apigateway.sh @@ -38,7 +38,7 @@ if_error_then_exit $? "sync_resource_docs_by_archive fail" log_info "done sync_resource_docs_by_archive" log_info "do create_version_and_release_apigw..." -python manage.py create_version_and_release_apigw -f /app/resources/apigateway/definition.yaml +python manage.py create_version_and_release_apigw -f /app/resources/apigateway/definition.yaml --generate-sdks if_error_then_exit $? "create_version_and_release_apigw fail" log_info "done create_version_and_release_apigw" diff --git a/src/api/bkuser_core/audit/constants.py b/src/api/bkuser_core/audit/constants.py index 155f21417..272c2dfe7 100644 --- a/src/api/bkuser_core/audit/constants.py +++ b/src/api/bkuser_core/audit/constants.py @@ -21,6 +21,7 @@ class LogInFailReason(AutoLowerEnum): TOO_MANY_FAILURE = auto() LOCKED_USER = auto() DISABLED_USER = auto() + EXPIRED_USER = auto() SHOULD_CHANGE_INITIAL_PASSWORD = auto() _choices_labels = ( @@ -29,6 +30,7 @@ class LogInFailReason(AutoLowerEnum): (TOO_MANY_FAILURE, "密码错误次数过多"), (LOCKED_USER, "用户已锁定"), (DISABLED_USER, "用户已删除"), + (EXPIRED_USER, "用户账号已过期"), (SHOULD_CHANGE_INITIAL_PASSWORD, "需要修改初始密码"), ) diff --git a/src/api/bkuser_core/audit/migrations/0005_auto_20220526_1048.py b/src/api/bkuser_core/audit/migrations/0005_auto_20220526_1048.py new file mode 100644 index 000000000..a278ff305 --- /dev/null +++ b/src/api/bkuser_core/audit/migrations/0005_auto_20220526_1048.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.5 on 2022-05-26 02:48 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('audit', '0004_auto_20211021_1852'), + ] + + operations = [ + migrations.AlterField( + model_name='generallog', + name='status', + field=models.CharField(choices=[('succeed', '成功'), ('failed', '失败')], max_length=16, verbose_name='状态'), + ), + migrations.AlterField( + model_name='login', + name='reason', + field=models.CharField(blank=True, choices=[('bad_password', '密码错误'), ('expired_password', '密码过期'), ('too_many_failure', '密码错误次数过多'), ('locked_user', '用户已锁定'), ('disabled_user', '用户已删除'), ('expired_user', '用户账号已过期'), ('should_change_initial_password', '需要修改初始密码')], max_length=32, null=True, verbose_name='登陆失败原因'), + ), + ] diff --git a/src/api/bkuser_core/bkiam/constants.py b/src/api/bkuser_core/bkiam/constants.py index 2d11372d4..fac29816c 100644 --- a/src/api/bkuser_core/bkiam/constants.py +++ b/src/api/bkuser_core/bkiam/constants.py @@ -198,7 +198,9 @@ def parse_department_path(data): }, cls.CATEGORY: {"category.id": "id"}, cls.FIELD: {"field.id": "name"}, - cls.PROFILE: {}, + cls.PROFILE: { + "department._bk_iam_path_": parse_department_path, + }, cls.SYNCTASK: {"category.id": "category_id"}, } return _map[resource_type] @@ -211,6 +213,8 @@ def get_id_name_pair(cls, resource_type: "ResourceType") -> tuple: cls.CATEGORY: ("id", "display_name"), cls.FIELD: ("id", "display_name"), cls.PROFILE: ("id", "username"), + # FIXME: not sure + cls.SYNCTASK: ("id", "id"), } return _map[resource_type] diff --git a/src/api/bkuser_core/categories/management/commands/make_crontab_sync_task_for_category.py b/src/api/bkuser_core/categories/management/commands/make_crontab_sync_task_for_category.py index 8a86b02d8..c1888fe7e 100644 --- a/src/api/bkuser_core/categories/management/commands/make_crontab_sync_task_for_category.py +++ b/src/api/bkuser_core/categories/management/commands/make_crontab_sync_task_for_category.py @@ -9,6 +9,7 @@ specific language governing permissions and limitations under the License. """ import logging +import traceback from django.core.management.base import BaseCommand @@ -44,4 +45,5 @@ def handle(self, *args, **options): try: make_periodic_sync_task(int(category_id), operator, interval) except Exception: # pylint: disable=broad-except + self.stdout.write(traceback.format_exc()) self.stdout.write(f"Failed to add sync task for category {category_id}") diff --git a/src/api/bkuser_core/categories/management/commands/test_category_sync.py b/src/api/bkuser_core/categories/management/commands/test_category_sync.py index d190e81d7..44ce6ce2a 100644 --- a/src/api/bkuser_core/categories/management/commands/test_category_sync.py +++ b/src/api/bkuser_core/categories/management/commands/test_category_sync.py @@ -9,6 +9,7 @@ specific language governing permissions and limitations under the License. """ import logging +import traceback import uuid from django.core.management.base import BaseCommand @@ -42,10 +43,12 @@ def handle(self, *args, **options): raw_data_file=excel_file, ) except Exception: # pylint: disable=broad-except + self.stdout.write(traceback.format_exc()) logger.exception("can not find category by type<%s>", category_type) return try: adapter_sync(ProfileCategory.objects.filter(type=category_type)[0].pk, task_id=task_id) except Exception: # pylint: disable=broad-except + self.stdout.write(traceback.format_exc()) logger.exception("can not find category by type<%s>", category_type) diff --git a/src/api/bkuser_core/categories/plugins/base.py b/src/api/bkuser_core/categories/plugins/base.py index cfaff571e..d97036ed0 100644 --- a/src/api/bkuser_core/categories/plugins/base.py +++ b/src/api/bkuser_core/categories/plugins/base.py @@ -281,7 +281,7 @@ def try_to_add_profile_department_relation(self, profile: Profile, department: D profile=profile, department_id__in=exempt_department_ids ).exists() ): - logger.debug( + logger.info( "profile<%s> is in the exempted department<%s>, skip", profile, department, diff --git a/src/api/bkuser_core/categories/plugins/custom/helpers.py b/src/api/bkuser_core/categories/plugins/custom/helpers.py index 99fd0e0c5..b8e5a23d3 100644 --- a/src/api/bkuser_core/categories/plugins/custom/helpers.py +++ b/src/api/bkuser_core/categories/plugins/custom/helpers.py @@ -51,7 +51,7 @@ def _get_code(self, raw_key: Union[str, int]) -> str: # 添加 category_id ,code 可以在多目录中唯一 code = f"{self.category.pk}-{str(raw_key)}" sha = hashlib.sha256(force_bytes(code)).hexdigest() - logger.debug("transform code to sha: %s -> %s", code, sha) + logger.info("transform code to sha: %s -> %s", code, sha) return sha @@ -156,7 +156,7 @@ def _load_base_info(self): validate_username(value=info.username) except ValidationError as e: self.context.add_record(step=SyncStep.USERS, success=False, username=info.username, error=str(e)) - logger.warning("username<%s:%s> does not meet format", info.code, info.username) + logger.warning("username<%s:%s> does not meet format, will skip", info.code, info.username) continue # 1. 先更新 profile 本身 @@ -165,6 +165,7 @@ def _load_base_info(self): if info.extras: # note: the priority of extras from origin api is higher than `code=info.code` extras.update(info.extras) + profile_params = { "category_id": self.category.pk, "domain": self.category.domain, @@ -209,7 +210,9 @@ def _load_base_info(self): department=dep_id, error=_("部门不存在"), ) - logger.warning("the department<%s> of profile<%s:%s> is missing", dep_id, info.code, info.username) + logger.warning( + "the department<%s> of profile<%s:%s> is missing, will skip", dep_id, info.code, info.username + ) continue self.try_add_relation( @@ -231,7 +234,11 @@ def _load_leader_info(self): self.context.add_record( step=SyncStep.USERS_RELATIONSHIP, success=False, username=info.username, error=_("用户信息不存在") ) - logger.warning("profile<%s:%s> not exists, will not be synced, skip", info.code, info.username) + logger.warning( + "profile<%s:%s> not exists, the profile leaders will not be synced, will skip", + info.code, + info.username, + ) continue for leader_id in info.leaders: @@ -239,7 +246,7 @@ def _load_leader_info(self): self.context.add_record( step=SyncStep.USERS_RELATIONSHIP, success=False, username=info.username, error=_("无法设置自己为上级") ) - logger.warning("profile<%s:%s> can not regard self as leader, skip", info.code, info.username) + logger.warning("profile<%s:%s> can not regard self as leader, will skip", info.code, info.username) continue leader = self.db_sync_manager.magic_get(self._get_code(leader_id), CustomProfileMeta) @@ -250,7 +257,9 @@ def _load_leader_info(self): username=info.username, error=_("上级【{username}】不存在").format(username=leader_id), ) - logger.warning("the leader<%s> of profile<%s:%s> is missing", leader_id, info.code, info.username) + logger.warning( + "the leader<%s> of profile<%s:%s> is missing, will skip", leader_id, info.code, info.username + ) continue self.try_add_relation( diff --git a/src/api/bkuser_core/categories/plugins/ldap/__init__.py b/src/api/bkuser_core/categories/plugins/ldap/__init__.py index 614656f0a..24a17ae6a 100644 --- a/src/api/bkuser_core/categories/plugins/ldap/__init__.py +++ b/src/api/bkuser_core/categories/plugins/ldap/__init__.py @@ -24,3 +24,5 @@ category_type="ldap", settings_path=os.path.dirname(__file__) / Path("settings.yaml"), ).register() + +# NOTE: 策略-每一次排查, 都简化复杂度, 加相关的日志等, 为未来的排查降低成本 diff --git a/src/api/bkuser_core/categories/plugins/ldap/adaptor.py b/src/api/bkuser_core/categories/plugins/ldap/adaptor.py index f73afdcad..6025be652 100644 --- a/src/api/bkuser_core/categories/plugins/ldap/adaptor.py +++ b/src/api/bkuser_core/categories/plugins/ldap/adaptor.py @@ -49,19 +49,19 @@ def get_value( if dynamic_field: ldap_field_name = field_name if ldap_field_name not in self.dynamic_fields_mapping.values(): - logger.info("no config[%s] in configs of dynamic_fields_mapping", field_name) + logger.warning("no config[%s] in configs of dynamic_fields_mapping", field_name) return "" else: # 从目录配置中获取 字段名 ldap_field_name = self.config_loader.get(field_name) if not ldap_field_name: - logger.info("no config[%s] in configs of category", field_name) + logger.warning("no config[%s] in configs of category", field_name) return "" # 1. 通过字段名,获取具体值 if ldap_field_name not in user_meta or not user_meta[ldap_field_name]: - logger.info("field[%s] is missing in raw attributes of user data from ldap", field_name) + logger.warning("field[%s] is missing in raw attributes of user data from ldap", field_name) return "" # 2. 类似 memberOf 字段,将会返回原始列表 diff --git a/src/api/bkuser_core/categories/plugins/ldap/client.py b/src/api/bkuser_core/categories/plugins/ldap/client.py index e52d3ac2c..d8d1ff47b 100644 --- a/src/api/bkuser_core/categories/plugins/ldap/client.py +++ b/src/api/bkuser_core/categories/plugins/ldap/client.py @@ -114,6 +114,7 @@ def search( ) if not result and not self.con.result["result"] == 0 and not self.con.last_error: + logger.error("failed to search %s from %s, last_error: %s", search_filter, start_root, self.con.last_error) raise local_exceptions.SearchFailed return self.con.response diff --git a/src/api/bkuser_core/categories/plugins/ldap/handlers.py b/src/api/bkuser_core/categories/plugins/ldap/handlers.py index 5d6d8ab1b..fa1c38c99 100644 --- a/src/api/bkuser_core/categories/plugins/ldap/handlers.py +++ b/src/api/bkuser_core/categories/plugins/ldap/handlers.py @@ -71,6 +71,9 @@ def update_or_create_sync_tasks(instance: "Setting", operator: str): @receiver(post_category_delete) def delete_sync_tasks(sender, instance: "ProfileCategory", **kwargs): if instance.type not in [CategoryType.LDAP.value, CategoryType.MAD.value]: + logger.warning( + "category<%s> is %s, not a ldap or mad category, skip delete sync tasks", instance.id, instance.type + ) return logger.info("going to delete periodic task for Category<%s>, the category type is %s", instance.id, instance.type) @@ -81,14 +84,18 @@ def delete_sync_tasks(sender, instance: "ProfileCategory", **kwargs): @receiver(post_setting_create) def update_sync_tasks(sender, instance: "Setting", operator: str, **kwargs): if instance.category.type not in [CategoryType.LDAP.value, CategoryType.MAD.value]: + logger.warning( + "category<%s> is %s, not a ldap or mad category, skip update sync tasks", instance.id, instance.type + ) return # 针对 pull_cycle 配置更新同步任务 + logger.info("going to update periodic task for Category<%s>, the category type is %s", instance.id, instance.type) update_or_create_sync_tasks(instance, operator) @receiver(post_dynamic_field_delete) def update_dynamic_field_mapping(sender, instance: "DynamicFieldInfo", **kwargs): """尝试刷新自定义字段映射配置""" - delete_dynamic_filed(dynamic_field=instance.name) logger.info("going to delete <%s> from dynamic_field_mapping", instance.name) + delete_dynamic_filed(dynamic_field=instance.name) diff --git a/src/api/bkuser_core/categories/plugins/ldap/helper.py b/src/api/bkuser_core/categories/plugins/ldap/helper.py index 6ac20cb04..a8942bb9e 100644 --- a/src/api/bkuser_core/categories/plugins/ldap/helper.py +++ b/src/api/bkuser_core/categories/plugins/ldap/helper.py @@ -152,7 +152,7 @@ def _load_base_info(self): username=info.username, error=str(e), ) - logger.warning("username<%s> does not meet format", info.username) + logger.warning("username<%s> does not meet format, will skip", info.username) continue # 1. 先更新 profile 本身 @@ -201,7 +201,7 @@ def _load_base_info(self): error=_("部门不存在"), ) logger.warning( - "the department<%s> of profile<%s> is missing", + "the department<%s> of profile<%s> is missing, will skip", department_key, info.username, ) diff --git a/src/api/bkuser_core/categories/plugins/ldap/login.py b/src/api/bkuser_core/categories/plugins/ldap/login.py index 31d90278b..73069803b 100644 --- a/src/api/bkuser_core/categories/plugins/ldap/login.py +++ b/src/api/bkuser_core/categories/plugins/ldap/login.py @@ -8,6 +8,8 @@ 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 django.utils.encoding import force_str from bkuser_core.categories.plugins.ldap.adaptor import ProfileFieldMapper @@ -15,6 +17,8 @@ from bkuser_core.categories.plugins.ldap.exceptions import FetchUserMetaInfoFailed from bkuser_core.user_settings.loader import ConfigProvider +logger = logging.getLogger(__name__) + class LoginHandler: @staticmethod @@ -31,13 +35,22 @@ def check(self, profile, password): client = LDAPClient(config_loader) field_fetcher = ProfileFieldMapper(config_loader) + logger.debug( + "going to search users, object_class: %s, attributes: %s", + config_loader["user_class"], + field_fetcher.get_user_attributes(), + ) users = client.search( object_class=config_loader["user_class"], attributes=field_fetcher.get_user_attributes(), ) + logger.debug("search results users: %s", users) + + # NOTE: 1. 如果用户反馈登录一直不成功, 怎么排查? 2.target_dn可能被后面命中的覆盖? target_dn = None for user in users: if not user.get("raw_attributes"): + logger.debug("user %s has no raw_attributes, skip", user) continue if self.fetch_username(field_fetcher, user) == profile.username: @@ -47,4 +60,5 @@ def check(self, profile, password): raise FetchUserMetaInfoFailed("获取用户基本信息失败") # 检验 + logger.debug("going to check user, dn: %s", target_dn) client.check(username=target_dn, password=password) diff --git a/src/api/bkuser_core/categories/plugins/ldap/syncer.py b/src/api/bkuser_core/categories/plugins/ldap/syncer.py index bafe9f8af..38312801e 100644 --- a/src/api/bkuser_core/categories/plugins/ldap/syncer.py +++ b/src/api/bkuser_core/categories/plugins/ldap/syncer.py @@ -79,14 +79,22 @@ def _fetch_data( else: groups = [] except Exception as e: - logger.exception("failed to get groups from remote server") + logger.exception( + "failed to get groups from remote server. basic_pull_node: %s, user_group_filter: %s", + basic_pull_node, + user_group_filter, + ) error_detail = f" ({type(e).__module__}.{type(e).__name__}: {str(e)})" raise FetchDataFromRemoteFailed(_("无法获取用户组,请检查配置") + error_detail) try: departments = self.client.search(start_root=basic_pull_node, object_class=organization_class) except Exception as e: - logger.exception("failed to get departments from remote server") + logger.exception( + "failed to get departments from remote server. basic_pull_node: %s, organization_class: %s", + basic_pull_node, + organization_class, + ) error_detail = f" ({type(e).__module__}.{type(e).__name__}: {str(e)})" raise FetchDataFromRemoteFailed(_("无法获取组织部门,请检查配置") + error_detail) @@ -97,7 +105,12 @@ def _fetch_data( attributes=attributes or [], ) except Exception as e: - logger.exception("failed to get users from remote server") + logger.exception( + "failed to get users from remote server. basic_pull_node: %s, user_filter: %s, attributes: %s", + basic_pull_node, + user_filter, + attributes, + ) error_detail = f" ({type(e).__module__}.{type(e).__name__}: {str(e)})" raise FetchDataFromRemoteFailed(_("无法获取用户数据, 请检查配置") + error_detail) @@ -122,7 +135,7 @@ def fetch_profiles(self, restrict_types: List[str]): profiles = [] for user in users: if not user.get("dn"): - logger.info("no dn field, skipping for %s", user) + logger.warning("no dn field, skipping for profile: %s", user) continue profiles.append( @@ -141,7 +154,7 @@ def fetch_departments(self, restrict_types: List[str]): results = [] for is_group, dept_meta in chain.from_iterable(iter([product([False], departments), product([True], groups)])): if not dept_meta.get("dn"): - logger.info("no dn field, skipping for %s", dept_meta) + logger.warning("no dn field, skipping for %s:%s", ("group" if is_group else "department"), dept_meta) continue results.append( department_adapter( diff --git a/src/api/bkuser_core/categories/plugins/local/handlers.py b/src/api/bkuser_core/categories/plugins/local/handlers.py index 57018d04d..c33fcfec6 100644 --- a/src/api/bkuser_core/categories/plugins/local/handlers.py +++ b/src/api/bkuser_core/categories/plugins/local/handlers.py @@ -25,6 +25,7 @@ @receiver(post_category_create) def make_local_default_settings(sender, instance: "ProfileCategory", **kwargs): if instance.type not in [CategoryType.LOCAL.value]: + logger.info("category<%s> is not local, skip make_local_default_settings", instance.id) return logger.info("going to make default settings for Category<%s>", instance.id) diff --git a/src/api/bkuser_core/categories/plugins/local/syncer.py b/src/api/bkuser_core/categories/plugins/local/syncer.py index f232ce7c6..e5190db51 100644 --- a/src/api/bkuser_core/categories/plugins/local/syncer.py +++ b/src/api/bkuser_core/categories/plugins/local/syncer.py @@ -10,9 +10,10 @@ """ import logging from collections import OrderedDict -from dataclasses import dataclass +from dataclasses import dataclass, field from typing import Any, Dict, List, Optional, Sequence, Type +from django.contrib.auth.hashers import make_password from django.core.exceptions import ObjectDoesNotExist from django.db import transaction from django.utils.translation import ugettext_lazy as _ @@ -34,6 +35,7 @@ from bkuser_core.departments.models import Department, DepartmentThroughModel from bkuser_core.profiles.constants import DynamicFieldTypeEnum, ProfileStatus, StaffStatus from bkuser_core.profiles.models import DynamicFieldInfo, LeaderThroughModel, Profile +from bkuser_core.profiles.tasks import send_password_by_email from bkuser_core.profiles.utils import make_password_by_config from bkuser_core.user_settings.loader import ConfigProvider @@ -93,6 +95,7 @@ class ExcelSyncer(Syncer): """Excel 数据同步类""" fetcher_cls: Type[ExcelFetcher] = ExcelFetcher + notify_profile_init_password_dict: Dict[Profile, str] = field(default_factory=dict) def __post_init__(self): super().__post_init__() @@ -109,6 +112,8 @@ def sync(self, raw_data_file): self._sync_users(self.fetcher.parser_set, user_rows) self._sync_leaders(self.fetcher.parser_set, user_rows) + self._notify_init_passwords() + def _sync_departments(self, raw_department_groups): """由于部门之间存在比较多的父子关系,不适合使用批量插入,目前使用按照逻辑顺序插入""" @@ -204,7 +209,7 @@ def _sync_users(self, parser_set: "ParserSet", users: list): except ObjectDoesNotExist: profile_id = self.db_sync_manager.register_id(ProfileMeta) - password, _ = make_password_by_config(self.category_id) + raw_password, should_notify = make_password_by_config(self.category_id, return_raw=True) initial_params = { "id": profile_id, "status": ProfileStatus.NORMAL.name, @@ -214,13 +219,15 @@ def _sync_users(self, parser_set: "ParserSet", users: list): "category_id": self.category_id, "domain": self.category.domain, "password_valid_days": self._default_password_valid_days, - "password": password, + "password": make_password(raw_password), } initial_params.update(profile_params) adding_profile = Profile(**initial_params) self.db_sync_manager.magic_add(adding_profile) logger.debug("(%s/%s): adding profile %s", index, total, username) + if should_notify: + self.notify_profile_init_password_dict[adding_profile] = raw_password # 2 获取关联的部门DB实例,创建关联对象 progress(index, total, "adding profile & department relation") @@ -275,6 +282,19 @@ def _sync_leaders(self, parser_set: "ParserSet", users: list): # 单独批量插入 leaders self.db_sync_manager[LeaderThroughModel].sync_to_db() + def _notify_init_passwords(self) -> None: + # 是否发送初始化密码邮件 + if self.notify_profile_init_password_dict: + for instance, password in self.notify_profile_init_password_dict.items(): + try: + send_password_by_email.delay(instance.id, raw_password=password, init=True) + except Exception: # pylint: disable=broad-except + logger.exception( + "failed to send init password via email. [profile.id=%s, profile.username=%s", + instance.id, + instance.username, + ) + @dataclass class ParserSet: diff --git a/src/api/bkuser_core/categories/views.py b/src/api/bkuser_core/categories/views.py index d487601f3..9560eae26 100644 --- a/src/api/bkuser_core/categories/views.py +++ b/src/api/bkuser_core/categories/views.py @@ -199,7 +199,9 @@ def test_connection(self, request, lookup_value): **serializer.validated_data ) except Exception as e: - logger.exception("failed to test initialize category<%s>", instance.id) + logger.exception( + "failed to test initialize category<%s-%s-%s>", instance.type, instance.display_name, instance.id + ) raise error_codes.TEST_CONNECTION_FAILED.format(str(e), replace=True) return Response() @@ -233,12 +235,17 @@ def test_fetch_data(self, request, lookup_value): try: syncer = syncer_cls(instance.id) except Exception as e: - logger.exception("failed to test initialize category<%s>", instance.id) + logger.exception( + "failed to test initialize category<%s-%s-%s>", instance.type, instance.display_name, instance.id + ) raise error_codes.TEST_CONNECTION_FAILED.f(f"请确保连接设置正确 {str(e)}") try: syncer.fetcher.test_fetch_data(serializer.validated_data) except Exception as e: # pylint: disable=broad-except + logger.exception( + "failed to fetch data from category<%s-%s-%s>", instance.type, instance.display_name, instance.id + ) error_detail = f" ({type(e).__module__}.{type(e).__name__}: {str(e)})" raise error_codes.TEST_FETCH_DATA_FAILED.f(error_detail) @@ -260,6 +267,9 @@ def sync(self, request, lookup_value): category=instance, operator=request.operator, type_=SyncTaskType.MANUAL ).id except ExistsSyncingTaskError as e: + logger.exception( + "failed to register sync task. [instance.id=%s], operator=%s", instance.id, request.operator + ) raise error_codes.LOAD_DATA_FAILED.f(str(e)) try: @@ -279,7 +289,7 @@ def sync(self, request, lookup_value): raise except Exception as e: logger.exception( - "failed to sync data. " "[instance.id=%s, operator=%s, task_id=%s]", + "failed to sync data. [instance.id=%s, operator=%s, task_id=%s]", instance.id, request.operator, task_id, @@ -322,7 +332,7 @@ def import_data_file(self, request, lookup_value): adapter_sync(lookup_value, operator=request.operator, task_id=task_id, **params) except DataFormatError as e: logger.exception( - "failed to sync data, dataformat error. " "[instance_id=%s, operator=%s, task_id=%s, params=%s]", + "failed to sync data, dataformat error. [instance_id=%s, operator=%s, task_id=%s, params=%s]", lookup_value, request.operator, task_id, @@ -358,6 +368,12 @@ class SyncTaskViewSet(AdvancedModelViewSet, AdvancedListAPIView): @swagger_auto_schema(responses={200: SyncTaskProcessSerializer(many=True)}) def show_logs(self, request, lookup_value): task: SyncTask = self.get_object() + + # NOTE: 必须有manage_category权限才能查看/变更settings => SaaS已经传递了 ACTION_ID = IAMAction.VIEW_CATEGORY.value + # request.META[dj_settings.NEED_IAM_HEADER] = "True" + # request.META[dj_settings.ACTION_ID_HEADER] = IAMAction.MANAGE_CATEGORY.value + self.check_object_permissions(request, task.category) + processes = task.progresses.order_by("-create_time") slz = SyncTaskProcessSerializer(processes, many=True) diff --git a/src/api/bkuser_core/common/db_sync.py b/src/api/bkuser_core/common/db_sync.py index 24d9b1267..c1bb27956 100644 --- a/src/api/bkuser_core/common/db_sync.py +++ b/src/api/bkuser_core/common/db_sync.py @@ -44,7 +44,7 @@ class SyncModelMeta: use_bulk: bool = True # TODO: support unique_together unique_key_field: ClassVar[str] = "" - sharding_size: int = 5000 + sharding_size: int = 1000 @classmethod def has_unique_key(cls) -> bool: @@ -204,29 +204,66 @@ def _sync_updating(self): self._sync(items, manager, method, extra_params) def _sync(self, items: List, manager: str, method: str, extra_params: dict): + target_model_name = self.meta.target_model.__name__ if not items: - logger.info( - "======== %s is empty to %s 📭 =========", - self.meta.target_model.__name__, - method, - ) + logger.info("======== %s is empty to %s 📭 =========", target_model_name, method) return - logger.info( - "======== Going to %s(count: %s) for %s =========", - method, - len(items), - self.meta.target_model.__name__, - ) + logger.info("======== Going to %s(count: %s) for %s =========", method, len(items), target_model_name) + + current_count = 0 + total_fail_count = 0 + total_fail_records = [] slices = self.make_slices(items) for idx, part in enumerate(slices): - logger.debug( - "======== Syncing part of %s(%s/%s) =========", self.meta.target_model.__name__, idx + 1, len(slices) + logger.info( + "======== Syncing part of %s(%s/%s) current: %d + %d =========", + target_model_name, + idx + 1, + len(slices), + current_count, + len(part), ) + current_count = current_count + len(part) + # NOTE: 批量插入失败, 会导致整体同步任务失败 + # - 优化: 批量插入失败, 切换成单条插入 + # - 优化: 单条插入失败, continue (会打详细日志) try: getattr(getattr(self.meta.target_model, manager), method)(part, **extra_params) except Exception: - logger.exception("%s %s failed", self.meta.target_model.__name__, method) - raise - - logger.info("======== %s synced. ✅ ========", self.meta.target_model.__name__) + logger.warning( + "%s %s failed, count=%d, extra_params=%s, will try to sync one by one", + target_model_name, + method, + len(part), + extra_params, + ) + for one in part: + try: + one.save() + continue + except Exception: + total_fail_count += 1 + logger.exception( + "%s %s: save one by one fail, item=%s, will not be updated, detail=%s", + target_model_name, + method, + one, + vars(one), + ) + total_fail_records.append(one) + continue + # 原先的逻辑: raise + # raise + if total_fail_count > 0: + logger.error( + "%s %s failed, total_fail_count=%d, total_fail_records=%s", + target_model_name, + method, + total_fail_count, + total_fail_records, + ) + logger.info("======== %s synced. and got %d fail ✅ ========", target_model_name, total_fail_count) + # TODO: should do something to let the admin know some record fail! + else: + logger.info("======== %s synced. ✅ ========", target_model_name) diff --git a/src/api/bkuser_core/common/error_codes.py b/src/api/bkuser_core/common/error_codes.py index f0916a404..a0fa5ca26 100644 --- a/src/api/bkuser_core/common/error_codes.py +++ b/src/api/bkuser_core/common/error_codes.py @@ -117,6 +117,7 @@ def __getattr__(self, code_name): ErrorCode("SHOULD_CHANGE_INITIAL_PASSWORD", _("平台分配的初始密码未修改"), 3210021), ErrorCode("USER_IS_DELETED", _("账号已被删除,请联系管理员"), 3210022), ErrorCode("CATEGORY_PLUGIN_LOAD_FAIL", _("目录登录插件加载失败"), 3210023), + ErrorCode("USER_IS_EXPIRED", _("该用户账号已过期"), 3210024), # 用户相关 ErrorCode("PASSWORD_DUPLICATED", _("新密码不能与最近{max_password_history}次密码相同")), ErrorCode("EMAIL_NOT_PROVIDED", _("该用户没有提供邮箱,发送邮件失败")), diff --git a/src/api/bkuser_core/common/notifier.py b/src/api/bkuser_core/common/notifier.py index 4e0d0143d..ac05bc41e 100644 --- a/src/api/bkuser_core/common/notifier.py +++ b/src/api/bkuser_core/common/notifier.py @@ -21,6 +21,7 @@ logger = logging.getLogger(__name__) DEFAULT_EMAIL_SENDER = "bk-user-core-api" +DEFAULT_SMS_SENDER = "bk-user-core-api" class ReceiversCouldNotBeEmpty(Exception): @@ -31,6 +32,10 @@ class SendMailFailed(Exception): """发送邮件失败""" +class SendSmsFailed(Exception): + """发送短信失败""" + + def send_mail(receivers: List[str], message: str, sender: str = None, title: str = None): """发邮件""" if not receivers: @@ -43,7 +48,7 @@ def send_mail(receivers: List[str], message: str, sender: str = None, title: str client = get_client_by_raw_username(user=sender or DEFAULT_EMAIL_SENDER) message_encoded = force_text(base64.b64encode(message.encode("utf-8"))) - logger.info( + logger.debug( "going to send email to %s, title: %s, via %s", receivers_str, title, @@ -71,3 +76,35 @@ def send_mail(receivers: List[str], message: str, sender: str = None, title: str ret.get("message", "unknown error"), ) raise SendMailFailed(ret.get("message", "unknown error")) + + +def send_sms(receivers: List[str], message: str, sender: str = None): + """发短信""" + if not receivers: + raise ReceiversCouldNotBeEmpty(_("收件人不能为空")) + + receivers_str = ",".join(receivers) + + client = get_client_by_raw_username(user=sender or DEFAULT_SMS_SENDER) + + message_encoded = force_text(base64.b64encode(message.encode("utf-8"))) + logger.debug( + "going to send sms to %s, via %s", + receivers_str, + DEFAULT_EMAIL_SENDER, + ) + + send_sms_params = { + "content": message_encoded, + "receiver": receivers_str, + "is_content_base64": True, + } + ret = client.cmsi.send_sms(**send_sms_params) + + if not ret.get("result", False): + logger.error( + "Failed to send sms notification %s for %s", + receivers_str, + ret.get("message", "unknown error"), + ) + raise SendSmsFailed(ret.get("message", "unknown error")) diff --git a/src/api/bkuser_core/config/common/system.py b/src/api/bkuser_core/config/common/system.py index d0ba866d3..7a37c0c79 100644 --- a/src/api/bkuser_core/config/common/system.py +++ b/src/api/bkuser_core/config/common/system.py @@ -46,11 +46,11 @@ # tracing: otel 相关配置 # if enable, default false ENABLE_OTEL_TRACE = env.bool("BKAPP_ENABLE_OTEL_TRACE", default=False) -BKAPP_OTEL_INSTRUMENT_DB_API = env.bool("BKAPP_OTEL_INSTRUMENT_DB_API", default=True) -BKAPP_OTEL_SERVICE_NAME = env("BKAPP_OTEL_SERVICE_NAME", default="bk-user") -BKAPP_OTEL_SAMPLER = env("BKAPP_OTEL_SAMPLER", default="parentbased_always_off") -BKAPP_OTEL_BK_DATA_ID = env.int("BKAPP_OTEL_BK_DATA_ID", default=-1) +BKAPP_OTEL_INSTRUMENT_DB_API = env.bool("BKAPP_OTEL_INSTRUMENT_DB_API", default=False) +BKAPP_OTEL_SERVICE_NAME = env("BKAPP_OTEL_SERVICE_NAME", default="bk-user-api") +BKAPP_OTEL_SAMPLER = env("BKAPP_OTEL_SAMPLER", default="always_on") BKAPP_OTEL_GRPC_HOST = env("BKAPP_OTEL_GRPC_HOST", default="") +BKAPP_OTEL_DATA_TOKEN = env("BKAPP_OTEL_DATA_TOKEN", default="") # ============================================================================== # 全局应用配置 @@ -94,6 +94,29 @@ # 复用 API, 接口参数中存在 SYNC_API_PARAM 时, 以sync的接口协议返回 SYNC_API_PARAM = "for_sync" +# 通知发送时间间隔 +NOTICE_INTERVAL_SECONDS = env.int("NOTICE_INTERVAL_SECONDS", default=3) + + +# ============================================================================== +# 黑白名单/禁用等 +# ============================================================================== + +# 全局开关 +ENABLE_PROFILE_SENSITIVE_FILTER = env.bool("ENABLE_PROFILE_SENSITIVE_FILTER", default=False) + +# profile中敏感字段, 默认接口不返回, 只有加白的app_code才允许访问 +PROFILE_SENSITIVE_FIELDS = tuple(env.list("PROFILE_SENSITIVE_FIELDS", default=[])) +PROFILE_SENSITIVE_FIELDS_WHITELIST_APP_CODES = tuple( + env.list("PROFILE_SENSITIVE_FIELDS_WHITELIST_APP_CODES", default=[]) +) + +# extras中的敏感字段, 以及只有白名单中的 TOKEN 请求才能获取到这批字段; 安全考虑 +PROFILE_EXTRAS_SENSITIVE_FIELDS = tuple(env.list("PROFILE_EXTRAS_SENSITIVE_FIELDS", default=[])) +PROFILE_EXTRAS_SENSITIVE_FIELDS_WHITELIST_APP_CODES = tuple( + env.list("PROFILE_EXTRAS_SENSITIVE_FIELDS_WHITELIST_APP_CODES", default=[]) +) + # ============================================================================== # 开发调试 diff --git a/src/api/bkuser_core/departments/v2/views.py b/src/api/bkuser_core/departments/v2/views.py index 6683311a5..0e06fe3c9 100644 --- a/src/api/bkuser_core/departments/v2/views.py +++ b/src/api/bkuser_core/departments/v2/views.py @@ -241,6 +241,10 @@ def create(self, request, *args, **kwargs): responses={200: local_serializers.DepartmentSerializer()}, ) def partial_update(self, request, *args, **kwargs): + # check permission first, call check_object_permission + instance = self.get_object() + self.check_object_permissions(request, obj=instance) + return super().partial_update(request, *args, **kwargs) @swagger_auto_schema( diff --git a/src/api/bkuser_core/departments/v3/views.py b/src/api/bkuser_core/departments/v3/views.py index 65af0e035..00433c30a 100644 --- a/src/api/bkuser_core/departments/v3/views.py +++ b/src/api/bkuser_core/departments/v3/views.py @@ -10,10 +10,11 @@ """ import logging -from rest_framework import filters, viewsets +from rest_framework import filters from rest_framework.generics import ListAPIView from .serializers import PaginatedDeptSerializer, QueryDeptSerializer +from bkuser_core.apis.v2.viewset import AdvancedModelViewSet from bkuser_core.apis.v3.exceptions import QueryTooLong from bkuser_core.apis.v3.filters import MultipleFieldFilter from bkuser_core.apis.v3.serializers import AdvancedPagination @@ -26,7 +27,8 @@ logger = logging.getLogger(__name__) -class DepartmentViewSet(viewsets.ModelViewSet, ListAPIView): +# class DepartmentViewSet(viewsets.ModelViewSet, ListAPIView): +class DepartmentViewSet(AdvancedModelViewSet, ListAPIView): queryset = Department.objects.all() permission_classes = [IAMPermission] filter_backends = [filters.OrderingFilter] @@ -35,6 +37,9 @@ class DepartmentViewSet(viewsets.ModelViewSet, ListAPIView): foreign_fields = ["parent", "children"] + # 使用 filter 进行过滤的操作 + iam_filter_actions: tuple = ("list",) + @inject_serializer(query_in=QueryDeptSerializer, out=PaginatedDeptSerializer) def list(self, request, validated_data: dict, *args, **kwargs): """获取用户列表""" diff --git a/src/api/bkuser_core/monitoring/apps.py b/src/api/bkuser_core/monitoring/apps.py index a096b8c40..b5c0f92c3 100644 --- a/src/api/bkuser_core/monitoring/apps.py +++ b/src/api/bkuser_core/monitoring/apps.py @@ -19,6 +19,7 @@ class MonitoringConfig(AppConfig): name = "bkuser_core.monitoring" def ready(self): + setup_by_settings() init_sentry_sdk("bk-user-api", django_integrated=True, redis_integrated=True, celery_integrated=True) diff --git a/src/api/bkuser_core/profiles/account_expiration_notifier.py b/src/api/bkuser_core/profiles/account_expiration_notifier.py new file mode 100644 index 000000000..64f52b46b --- /dev/null +++ b/src/api/bkuser_core/profiles/account_expiration_notifier.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +""" +TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-用户管理(Bk-User) 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 datetime +import logging + +from bkuser_core.categories.constants import CategoryType +from bkuser_core.categories.models import ProfileCategory +from bkuser_core.profiles.notifier import get_expiration_dates, get_logined_profiles +from bkuser_core.user_settings.constants import ACCOUNT_EXPIRATION_NOTICE_INTERVAL_META_KEY, SettingsEnableNamespaces +from bkuser_core.user_settings.models import Setting + +logger = logging.getLogger(__name__) + + +def get_profiles_for_account_expiration(): + """ + 获取 需要进行账号过期相关通知的用户 + """ + expiring_profile_list = [] + expired_profile_list = [] + category_ids = ProfileCategory.objects.filter(type=CategoryType.LOCAL.value).values_list("id", flat=True) + + for category_id in category_ids: + notice_interval = ( + Setting.objects.filter( + category_id=category_id, + meta__key=ACCOUNT_EXPIRATION_NOTICE_INTERVAL_META_KEY, + meta__namespace=SettingsEnableNamespaces.ACCOUNT.value, + ) + .first() + .value + ) + + expiration_dates = get_expiration_dates(notice_interval) + logined_profiles = get_logined_profiles() + + expiring_profiles = logined_profiles.filter( + account_expiration_date__in=expiration_dates, category_id=category_id + ).values("id", "username", "category_id", "email", "telephone", "account_expiration_date") + expiring_profile_list.extend(expiring_profiles) + + expired_profiles = logined_profiles.filter( + account_expiration_date__lt=datetime.date.today(), category_id=category_id + ).values("id", "username", "category_id", "email", "telephone", "account_expiration_date") + expired_profile_list.extend(expired_profiles) + + return expiring_profile_list, expired_profile_list diff --git a/src/api/bkuser_core/profiles/constants.py b/src/api/bkuser_core/profiles/constants.py index 2831e949f..28ec1d38d 100644 --- a/src/api/bkuser_core/profiles/constants.py +++ b/src/api/bkuser_core/profiles/constants.py @@ -20,13 +20,9 @@ class ProfileStatus(AutoNameEnum): LOCKED = auto() DELETED = auto() DISABLED = auto() + EXPIRED = auto() - _choices_labels = ( - (NORMAL, "正常"), - (LOCKED, "被冻结"), - (DELETED, "被删除"), - (DISABLED, "被禁用"), - ) + _choices_labels = ((NORMAL, "正常"), (LOCKED, "被冻结"), (DELETED, "被删除"), (DISABLED, "被禁用"), (EXPIRED, "已过期")) class StaffStatus(AutoNameEnum): @@ -117,4 +113,17 @@ class FieldMapMethod(AutoLowerEnum): ) +class TypeOfExpiration(AutoLowerEnum): + ACCOUNT_EXPIRATION = auto() + PASSWORD_EXPIRATION = auto() + + _choices_labels = ( + (ACCOUNT_EXPIRATION, "账号过期"), + (PASSWORD_EXPIRATION, "密码过期"), + ) + + PASSWD_RESET_VIA_SAAS_EMAIL_TMPL = "您的蓝鲸账号【{username}】的密码已被重置,若非本人操作,请及时修改" + +NOTICE_METHOD_EMAIL = "send_email" +NOTICE_METHOD_SMS = "send_sms" diff --git a/src/api/bkuser_core/profiles/exceptions.py b/src/api/bkuser_core/profiles/exceptions.py index 3085e796f..f6464b83e 100644 --- a/src/api/bkuser_core/profiles/exceptions.py +++ b/src/api/bkuser_core/profiles/exceptions.py @@ -18,5 +18,9 @@ class ProfileEmailEmpty(Exception): """用户邮箱为空""" +class ProfileTelephoneEmpty(Exception): + """用户手机号码为空""" + + class CountryISOCodeNotMatch(Exception): """Country Code 不匹配""" diff --git a/src/api/bkuser_core/profiles/migrations/0022_auto_20220520_1028.py b/src/api/bkuser_core/profiles/migrations/0022_auto_20220520_1028.py new file mode 100644 index 000000000..364d42400 --- /dev/null +++ b/src/api/bkuser_core/profiles/migrations/0022_auto_20220520_1028.py @@ -0,0 +1,35 @@ +# Generated by Django 3.2.5 on 2022-05-20 02:28 + +import datetime +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('profiles', '0021_auto_20210326_1640'), + ] + + operations = [ + migrations.AddField( + model_name='profile', + name='account_expiration_date', + field=models.DateField(blank=True, default=datetime.date(2100, 1, 1), null=True, verbose_name='账号过期时间'), + ), + migrations.AlterField( + model_name='profile', + name='category_id', + field=models.IntegerField(blank=True, null=True, verbose_name='用户目录ID'), + ), + migrations.AlterField( + model_name='profile', + name='status', + field=models.CharField( + choices=[('NORMAL', '正常'), ('LOCKED', '被冻结'), ('DELETED', '被删除'), ('DISABLED', '被禁用'), + ('EXPIRED', '已过期')], default='NORMAL', max_length=64, verbose_name='账户状态'), + ), + migrations.AlterIndexTogether( + name='profile', + index_together={('category_id', 'account_expiration_date')}, + ), + ] diff --git a/src/api/bkuser_core/profiles/migrations/0023_local_profile_add_account_expiration_time.py b/src/api/bkuser_core/profiles/migrations/0023_local_profile_add_account_expiration_time.py new file mode 100644 index 000000000..819b6a8b8 --- /dev/null +++ b/src/api/bkuser_core/profiles/migrations/0023_local_profile_add_account_expiration_time.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +""" +TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-用户管理(Bk-User) 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 __future__ import unicode_literals + +from bkuser_core.categories.constants import CategoryType +from bkuser_core.user_settings.constants import SettingsEnableNamespaces +from django.db import migrations +from django.db.models import F + +import datetime + + +def forwards_func(apps, schema_editor): + """ + 本地目录用户增加 账号有效期 字段 + """ + + SettingMeta = apps.get_model("user_settings", "SettingMeta") + Setting = apps.get_model("user_settings", "Setting") + ProfileCategory = apps.get_model("categories", "ProfileCategory") + Profile = apps.get_model("profiles", "Profile") + + # 保证已存在的本地目录下用户拥有过期时间 + for c in ProfileCategory.objects.filter(type=CategoryType.LOCAL.value): + # 获取对应目录下的 账号有效期配置 + meta = SettingMeta.objects.filter( + key="expired_after_days", + namespace=SettingsEnableNamespaces.ACCOUNT.value + ).first() + + expired_after_days = Setting.objects.filter( + category_id=c.id, + meta=meta, + ).first().value + + if expired_after_days == -1: + Profile.objects.filter(category_id=c.id).update( + account_expiration_date=datetime.date(year=2100, month=1, day=1)) + + # 过期时间=用户创建时间+账号有效期 + else: + Profile.objects.filter(category_id=c.id).update( + account_expiration_date=F("create_time")+datetime.timedelta(days=expired_after_days)) + + +class Migration(migrations.Migration): + + dependencies = [ + ("profiles", "0022_auto_20220520_1028"), + ("user_settings", "0016_add_default_fields_account_settings") + ] + + operations = [migrations.RunPython(forwards_func)] diff --git a/src/api/bkuser_core/profiles/migrations/0024_expirationnoticerecord.py b/src/api/bkuser_core/profiles/migrations/0024_expirationnoticerecord.py new file mode 100644 index 000000000..39275a7d8 --- /dev/null +++ b/src/api/bkuser_core/profiles/migrations/0024_expirationnoticerecord.py @@ -0,0 +1,30 @@ +# Generated by Django 3.2.5 on 2022-05-23 12:39 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('profiles', '0023_local_profile_add_account_expiration_time'), + ] + + operations = [ + migrations.CreateModel( + name='ExpirationNoticeRecord', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('create_time', models.DateTimeField(auto_now_add=True)), + ('update_time', models.DateTimeField(auto_now=True)), + ('notice_date', models.DateField(verbose_name='过期通知时间')), + ('type', models.CharField(choices=[('account_expiration', '账号过期'), ('password_expiration', '密码过期')], db_index=True, max_length=64, verbose_name='过期类型')), + ('profile', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='profiles.profile', verbose_name='用户')), + ], + options={ + 'verbose_name': '过期通知记录', + 'verbose_name_plural': '过期通知记录', + 'ordering': ['id'], + }, + ), + ] diff --git a/src/api/bkuser_core/profiles/migrations/0025_dynamic_field_add_account_expiration_date.py b/src/api/bkuser_core/profiles/migrations/0025_dynamic_field_add_account_expiration_date.py new file mode 100644 index 000000000..6d877b0a7 --- /dev/null +++ b/src/api/bkuser_core/profiles/migrations/0025_dynamic_field_add_account_expiration_date.py @@ -0,0 +1,73 @@ +# -*- coding: utf-8 -*- +""" +TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-用户管理(Bk-User) 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. +""" +# Generated by Django 1.11.23 on 2020-07-29 07:49 +from __future__ import unicode_literals + +from django.db import migrations + + +def forwards_func(apps, schema_editor): + """添加内建字段信息""" + DynamicFieldInfo = apps.get_model("profiles", "DynamicFieldInfo") + + max_order = max(DynamicFieldInfo.objects.all().values_list("order", flat=True)) + + DynamicFieldInfo.objects.create( + name='account_expiration_date', + display_name='账号过期时间', + type='string', + require=False, + unique=False, + editable=True, + builtin=True, + order=max_order+1, + display_name_en='account_expiration_date', + display_name_zh_hans='账号过期时间', + visible=True + ) + DynamicFieldInfo.objects.create( + name='last_login_time', + display_name='最近登陆时间', + type='string', + require=False, + unique=False, + editable=False, + builtin=True, + order=max_order+2, + display_name_en='last_login_time', + display_name_zh_hans='最近登录时间', + configurable=False, + visible=False) + DynamicFieldInfo.objects.create( + name='create_time', + display_name='创建时间', + type='string', + require=False, + unique=False, + editable=False, + builtin=True, + order=max_order+3, + display_name_en='create_time', + display_name_zh_hans='创建时间', + configurable=False, + visible=True) + + DynamicFieldInfo.objects.filter(name='qq').update(visible=False) + DynamicFieldInfo.objects.filter(name='wx_userid').update(visible=False) + + +class Migration(migrations.Migration): + + dependencies = [ + ("profiles", "0024_expirationnoticerecord"), + ] + + operations = [migrations.RunPython(forwards_func)] diff --git a/src/api/bkuser_core/profiles/models.py b/src/api/bkuser_core/profiles/models.py index 0b9f2636f..b7dbb7805 100644 --- a/src/api/bkuser_core/profiles/models.py +++ b/src/api/bkuser_core/profiles/models.py @@ -26,6 +26,7 @@ ProfileStatus, RoleCodeEnum, StaffStatus, + TypeOfExpiration, ) from .managers import DynamicFieldInfoManager, ProfileAllManager, ProfileManager, ProfileTokenManager from .validators import validate_domain, validate_dynamic_field_name, validate_extras_value_unique, validate_username @@ -61,7 +62,7 @@ class Profile(TimestampedModel): # ----------------------- 目录相关 ----------------------- # 由写入时保证 domain & category_id 对应性 domain = models.CharField(verbose_name=_("域"), max_length=64, null=True, blank=True, db_index=True) - category_id = models.IntegerField(verbose_name=_("用户目录ID"), null=True, blank=True, db_index=True) + category_id = models.IntegerField(verbose_name=_("用户目录ID"), null=True, blank=True) # ----------------------- 目录相关 ----------------------- display_name = models.CharField(verbose_name=_("全名"), null=True, blank=True, default="", max_length=255) @@ -103,6 +104,12 @@ class Profile(TimestampedModel): ) # ----------------------- 职位相关 ----------------------- + # ----------------------- 账号相关 ----------------------- + account_expiration_date = models.DateField( + verbose_name=_("账号过期时间"), null=True, blank=True, default=datetime.date(year=2100, month=1, day=1) + ) + # ----------------------- 账号相关 ----------------------- + # ----------------------- 国际化相关 ----------------------- time_zone = models.CharField( verbose_name=_("时区"), @@ -147,6 +154,7 @@ class Meta: verbose_name = "用户信息" verbose_name_plural = "用户信息" unique_together = ("username", "category_id") + index_together = ("category_id", "account_expiration_date") def custom_validate(self): validate_domain(self.domain) @@ -285,3 +293,19 @@ class ProfileTokenHolder(TimestampedModel): def expired(self): """是否过期""" return now() > self.expire_time + + +class ExpirationNoticeRecord(TimestampedModel): + notice_date = models.DateField(verbose_name=_("过期通知时间"), null=False, blank=False) + type = models.CharField( + verbose_name=_("过期类型"), + choices=TypeOfExpiration.get_choices(), + db_index=True, + max_length=64, + ) + profile = models.ForeignKey("profiles.Profile", verbose_name="用户", on_delete=models.CASCADE) + + class Meta: + ordering = ["id"] + verbose_name = "过期通知记录" + verbose_name_plural = "过期通知记录" diff --git a/src/api/bkuser_core/profiles/notifier.py b/src/api/bkuser_core/profiles/notifier.py new file mode 100644 index 000000000..1e3ad4fbc --- /dev/null +++ b/src/api/bkuser_core/profiles/notifier.py @@ -0,0 +1,152 @@ +# -*- coding: utf-8 -*- +""" +TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-用户管理(Bk-User) 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 datetime +import logging + +from django.db.models import Exists, OuterRef + +from bkuser_core.audit.models import LogIn +from bkuser_core.categories.constants import CategoryType +from bkuser_core.categories.models import ProfileCategory +from bkuser_core.common.notifier import send_mail, send_sms +from bkuser_core.profiles.constants import NOTICE_METHOD_EMAIL, NOTICE_METHOD_SMS, TypeOfExpiration +from bkuser_core.profiles.models import Profile +from bkuser_core.user_settings.loader import ConfigProvider + +logger = logging.getLogger(__name__) + + +class ExpirationNotifier: + def handler(self, notice_config): + + notice_method_map = { + "send_email": self._notice_by_email, + "send_sms": self._notice_by_sms, + } + + for notice_method in notice_config: + notice_method_map[notice_method](notice_config[notice_method]) + + def _notice_by_email(self, email_config): + send_mail( + sender=email_config["sender"], + receivers=email_config["receivers"], + message=email_config["message"], + title=email_config["title"], + ) + + def _notice_by_sms(self, sms_config): + send_sms(sender=sms_config["sender"], receivers=sms_config["receivers"], message=sms_config["message"]) + + +def get_logined_profiles(): + """ + 获取在平台登录过的所有用户 + """ + subquery = LogIn.objects.filter(profile=OuterRef('pk')).values_list('id') + logined_profile_ids = ( + Profile.objects.annotate(temp=Exists(subquery)).filter(temp=True).values_list('id', flat=True) + ) + logined_profiles = Profile.objects.filter(id__in=logined_profile_ids) + + return logined_profiles + + +def get_expiration_dates(notice_interval): + """ + 获取需要进行通知的 过期时间列表 + """ + expiration_dates = [] + for day in notice_interval: + expiration_date = datetime.date.today() + datetime.timedelta(days=day) + expiration_dates.append(expiration_date) + + return expiration_dates + + +def get_config_from_all_local_categories(): + """一次性拉取所有目录的ConfigProvider""" + category_config_map = {} + category_ids = ProfileCategory.objects.filter(type=CategoryType.LOCAL.value).values_list("id", flat=True) + + for category_id in category_ids: + config_loader = ConfigProvider(category_id) + category_config_map[category_id] = config_loader + + return category_config_map + + +def get_notice_config_for_expiration(expiration_type, profile, config_loader): + """ + 整合 过期 通知内容 + """ + notice_config = {} + + if expiration_type == TypeOfExpiration.ACCOUNT_EXPIRATION.value: + logger.info("--------- get notice config for account expiration ----------") + notice_methods = config_loader["account_expiration_notice_methods"] + expired_at = profile["account_expiration_date"] - datetime.date.today() + expired_email_config = config_loader["expired_account_email_config"] + expiring_email_config = config_loader["expiring_account_email_config"] + expired_sms_config = config_loader["expired_account_sms_config"] + expiring_sms_config = config_loader["expiring_account_sms_config"] + + elif expiration_type == TypeOfExpiration.PASSWORD_EXPIRATION.value: + logger.info("--------- get notice config for password expiration ----------") + notice_methods = config_loader["password_expiration_notice_methods"] + expired_at = ( + (profile["password_update_time"].date() or profile["create_time"].date()) + + datetime.timedelta(days=profile["password_valid_days"]) + - datetime.date.today() + ) + expired_email_config = config_loader["expired_password_email_config"] + expiring_email_config = config_loader["expiring_password_email_config"] + expired_sms_config = config_loader["expired_password_sms_config"] + expiring_sms_config = config_loader["expiring_password_sms_config"] + + if not notice_methods: + return + + if NOTICE_METHOD_EMAIL in notice_methods: + email_config = expired_email_config if expired_at.days < 0 else expiring_email_config + + message = ( + email_config["content"].format(username=profile["username"]) + if expired_at.days < 0 + else email_config["content"].format(username=profile["username"], expired_at=expired_at.days) + ) + + notice_config.update( + { + "send_email": { + "sender": email_config["sender"], + "receivers": [profile["email"]], + "message": message, + "title": email_config["title"], + } + } + ) + + if NOTICE_METHOD_SMS in notice_methods: + sms_config = expired_sms_config if expired_at.days < 0 else expiring_sms_config + + message = ( + sms_config["content"].format(username=profile["username"]) + if expired_at.days < 0 + else sms_config["content"].format(username=profile["username"], expired_at=expired_at.days) + ) + + notice_config.update( + {"send_sms": {"sender": sms_config["sender"], "receivers": [profile["telephone"]], "message": message}} + ) + logger.debug("--------- notice_config(%s) of profile(%s) ----------", notice_config, profile) + + return notice_config diff --git a/src/api/bkuser_core/profiles/password_expiration_notifier.py b/src/api/bkuser_core/profiles/password_expiration_notifier.py new file mode 100644 index 000000000..8b8df22c8 --- /dev/null +++ b/src/api/bkuser_core/profiles/password_expiration_notifier.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- +""" +TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-用户管理(Bk-User) 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 datetime +import logging + +from bkuser_core.categories.constants import CategoryType +from bkuser_core.categories.models import ProfileCategory +from bkuser_core.profiles.notifier import get_expiration_dates, get_logined_profiles +from bkuser_core.user_settings.constants import PASSWORD_EXPIRATION_NOTICE_INTERVAL_META_KEY, SettingsEnableNamespaces +from bkuser_core.user_settings.models import Setting + +logger = logging.getLogger(__name__) + + +def get_profiles_for_password_expiration(): + """ + 获取 需要进行密码过期相关通知的用户 + """ + expiring_profile_list = [] + expired_profile_list = [] + category_ids = ProfileCategory.objects.filter(type=CategoryType.LOCAL.value).values_list("id", flat=True) + logined_profiles = get_logined_profiles() + + for category_id in category_ids: + notice_interval = ( + Setting.objects.filter( + category_id=category_id, + meta__key=PASSWORD_EXPIRATION_NOTICE_INTERVAL_META_KEY, + meta__namespace=SettingsEnableNamespaces.PASSWORD.value, + ) + .first() + .value + ) + + expiration_dates = get_expiration_dates(notice_interval) + profiles = logined_profiles.filter(category_id=category_id, password_valid_days__gt=0).values( + "id", + "username", + "category_id", + "email", + "telephone", + "password_valid_days", + "password_update_time", + "create_time", + ) + for profile in profiles: + valid_period = datetime.timedelta(days=profile["password_valid_days"]) + expired_at = (profile["password_update_time"] or profile["create_time"]) + valid_period + + if expired_at.date() in expiration_dates: + expiring_profile_list.append(profile) + + if expired_at.date() < datetime.date.today(): + expired_profile_list.append(profile) + + return expiring_profile_list, expired_profile_list diff --git a/src/api/bkuser_core/profiles/tasks.py b/src/api/bkuser_core/profiles/tasks.py index ce9ba9eeb..e457027bd 100644 --- a/src/api/bkuser_core/profiles/tasks.py +++ b/src/api/bkuser_core/profiles/tasks.py @@ -8,17 +8,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. """ +import datetime import logging +import time import urllib.parse +from celery.schedules import crontab +from celery.task import periodic_task from django.conf import settings +from django.utils.timezone import now +from .account_expiration_notifier import get_profiles_for_account_expiration +from .constants import TypeOfExpiration +from .notifier import ExpirationNotifier, get_config_from_all_local_categories, get_notice_config_for_expiration +from .password_expiration_notifier import get_profiles_for_password_expiration +from bkuser_core.categories.constants import CategoryType +from bkuser_core.categories.models import ProfileCategory from bkuser_core.celery import app from bkuser_core.common.notifier import send_mail from bkuser_core.profiles import exceptions -from bkuser_core.profiles.constants import PASSWD_RESET_VIA_SAAS_EMAIL_TMPL -from bkuser_core.profiles.models import Profile +from bkuser_core.profiles.constants import PASSWD_RESET_VIA_SAAS_EMAIL_TMPL, ProfileStatus +from bkuser_core.profiles.models import ExpirationNoticeRecord, Profile from bkuser_core.profiles.utils import make_passwd_reset_url_by_token +from bkuser_core.user_settings.exceptions import SettingHasBeenDisabledError from bkuser_core.user_settings.loader import ConfigProvider logger = logging.getLogger(__name__) @@ -66,3 +78,183 @@ def send_password_by_email(profile_id: int, raw_password: str = None, init: bool message=message, title=email_config["title"], ) + + +@periodic_task(run_every=crontab(minute='0', hour='2')) +def notice_for_account_expiration(): + """ + 用户账号过期通知 + #TODO:存在大数量级用户的情况下,当天的任务可能无法当天执行完毕,新一天的任务又同步开启,这里考虑做优化 + """ + expiring_profile_list, expired_profile_list = get_profiles_for_account_expiration() + category_config_map = get_config_from_all_local_categories() + + logger.info( + "--------- going to notice expiring_profiles count:(%s) for account expiration ----------", + len(expiring_profile_list), + ) + for profile in expiring_profile_list: + notice_config = get_notice_config_for_expiration( + expiration_type=TypeOfExpiration.ACCOUNT_EXPIRATION.value, + profile=profile, + config_loader=category_config_map[profile["category_id"]], + ) + if not notice_config: + continue + + ExpirationNotifier().handler(notice_config) + time.sleep(settings.NOTICE_INTERVAL_SECONDS) + + logger.info( + "--------- going to notice expired_profiles count:(%s) for account expiration ----------", + len(expired_profile_list), + ) + for profile in expired_profile_list: + notice_config = get_notice_config_for_expiration( + expiration_type=TypeOfExpiration.ACCOUNT_EXPIRATION.value, + profile=profile, + config_loader=category_config_map[profile["category_id"]], + ) + if not notice_config: + continue + + notice_record = ExpirationNoticeRecord.objects.filter( + type=TypeOfExpiration.ACCOUNT_EXPIRATION.value, profile_id=profile["id"] + ).first() + if not notice_record: + ExpirationNotifier().handler(notice_config) + ExpirationNoticeRecord.objects.create( + type=TypeOfExpiration.ACCOUNT_EXPIRATION.value, + notice_date=datetime.date.today(), + profile_id=profile["id"], + ) + time.sleep(settings.NOTICE_INTERVAL_SECONDS) + continue + + # 上一次过期通知的时间距离现在超过一个月则进行通知 + if notice_record.notice_date < datetime.date.today() - datetime.timedelta(days=30): + ExpirationNotifier().handler(notice_config) + notice_record.notice_date = datetime.date.today() + notice_record.save() + time.sleep(settings.NOTICE_INTERVAL_SECONDS) + + +@periodic_task(run_every=crontab(minute='0', hour='3')) +def notice_for_password_expiration(): + """ + 用户密码过期通知 + """ + expiring_profile_list, expired_profile_list = get_profiles_for_password_expiration() + category_config_map = get_config_from_all_local_categories() + + logger.info( + "--------- going to notice expiring_profiles count:(%s) for password expiration ----------", + len(expiring_profile_list), + ) + for profile in expiring_profile_list: + notice_config = get_notice_config_for_expiration( + expiration_type=TypeOfExpiration.PASSWORD_EXPIRATION.value, + profile=profile, + config_loader=category_config_map[profile["category_id"]], + ) + if not notice_config: + continue + + ExpirationNotifier().handler(notice_config) + time.sleep(settings.NOTICE_INTERVAL_SECONDS) + + logger.info( + "--------- going to notice expired_profiles count:(%s) for password expiration ----------", + len(expired_profile_list), + ) + for profile in expired_profile_list: + notice_config = get_notice_config_for_expiration( + expiration_type=TypeOfExpiration.PASSWORD_EXPIRATION.value, + profile=profile, + config_loader=category_config_map[profile["category_id"]], + ) + if not notice_config: + continue + + notice_record = ExpirationNoticeRecord.objects.filter( + type=TypeOfExpiration.PASSWORD_EXPIRATION.value, profile_id=profile["id"] + ).first() + if not notice_record: + ExpirationNotifier().handler(notice_config) + ExpirationNoticeRecord.objects.create( + type=TypeOfExpiration.PASSWORD_EXPIRATION.value, + notice_date=datetime.date.today(), + profile_id=profile["id"], + ) + time.sleep(settings.NOTICE_INTERVAL_SECONDS) + continue + + if notice_record.notice_date < datetime.date.today() - datetime.timedelta(days=30): + ExpirationNotifier().handler(notice_config) + notice_record.notice_date = datetime.date.today() + notice_record.save() + time.sleep(settings.NOTICE_INTERVAL_SECONDS) + + +@periodic_task(run_every=crontab(minute='0', hour='4')) +def change_profile_status_for_account_expiration(): + """ + 对账号过期的用户进行状态变更 + """ + category_ids = ProfileCategory.objects.filter(type=CategoryType.LOCAL.value).values_list("id") + expired_profiles = Profile.objects.filter( + category_id__in=category_ids, + account_expiration_date__lt=datetime.date.today(), + status__in=[ProfileStatus.NORMAL.value, ProfileStatus.DISABLED.value], + ) + expired_profiles.update(status=ProfileStatus.EXPIRED.value) + + +@periodic_task(run_every=crontab(minute='0', hour='5')) +def change_profile_status_for_account_locking(): + """ + 对长时间未登录的用户进行状态冻结 + """ + category_ids = ProfileCategory.objects.filter(type=CategoryType.LOCAL.value).values_list("id", flat=True) + frozen_profile_ids = [] + # 获取用户目录设置 + for category_id in category_ids: + config_loader = ConfigProvider(category_id=category_id) + try: + enable_auto_freeze = config_loader.get("enable_auto_freeze") + logger.info("category<%s> enable_auto_freeze = %s", category_id, enable_auto_freeze) + if not enable_auto_freeze: + continue + except SettingHasBeenDisabledError: + logger.info("category<%s> has disabled enable_auto_freeze", category_id) + continue + + try: + freeze_after_days = config_loader.get("freeze_after_days") + if int(freeze_after_days) <= 0: + logger.error("account_expired_to_locked: freeze_after_days should be more than 0") + continue + except SettingHasBeenDisabledError: + logger.info("category<%s> has disabled freeze_after_days", category_id) + continue + + profiles = Profile.objects.filter( + category_id=category_id, + status=ProfileStatus.NORMAL.value, + ) + + for profile in profiles: + # 最后登录时间 + # 当用户从未登录过 + if not profile.last_login_time: + # 场景考虑:该用户被管理员关注 + profile_last_operate_time = profile.update_time + else: + # 登录过,长久未登录,但是被管理员关注 + profile_last_operate_time = max(profile.last_login_time, profile.update_time) + # 被冻结用户集合添加 + if profile_last_operate_time + datetime.timedelta(days=int(freeze_after_days)) < now(): + frozen_profile_ids.append(profile.id) + # 批量冻结 + if frozen_profile_ids: + Profile.objects.filter(id__in=frozen_profile_ids).update(status=ProfileStatus.LOCKED.value) diff --git a/src/api/bkuser_core/profiles/urls.py b/src/api/bkuser_core/profiles/urls.py index 9291a0dff..a9ced0644 100644 --- a/src/api/bkuser_core/profiles/urls.py +++ b/src/api/bkuser_core/profiles/urls.py @@ -12,4 +12,5 @@ from bkuser_core.profiles.v2.urls import urlpatterns as v2_urlpatterns from bkuser_core.profiles.v3.urls import urlpatterns as v3_urlpatterns +# NOTE: can't delete it now, the pages used /api/v3/profiles and /api/v3/departments for search urlpatterns = v2_urlpatterns + v3_urlpatterns diff --git a/src/api/bkuser_core/profiles/utils.py b/src/api/bkuser_core/profiles/utils.py index 3da014a90..a67956945 100644 --- a/src/api/bkuser_core/profiles/utils.py +++ b/src/api/bkuser_core/profiles/utils.py @@ -13,7 +13,7 @@ import re import string import urllib.parse -from typing import TYPE_CHECKING, Tuple +from typing import TYPE_CHECKING, Dict, Tuple from django.conf import settings from django.contrib.auth.hashers import make_password @@ -25,6 +25,7 @@ from bkuser_core.profiles.validators import DOMAIN_PART_REGEX, USERNAME_REGEX from bkuser_core.user_settings.constants import InitPasswordMethod from bkuser_core.user_settings.loader import ConfigProvider +from bkuser_global.local import local from bkuser_global.utils import force_str_2_bool if TYPE_CHECKING: @@ -34,8 +35,16 @@ def gen_password(length): + # 必须包含至少一个数字 chars = string.ascii_letters + string.digits - return "".join([random.choice(chars) for _ in range(length)]) + + random_chars = [random.choice(chars) for _ in range(length)] + if any([c.isdigit() for c in random_chars]): + return "".join(random_chars) + + random_chars[0] = random.choice(string.digits) + random.shuffle(random_chars) + return "".join(random_chars) USERNAME_DOMAIN_REGEX = re.compile(f"(?P{USERNAME_REGEX})(@(?P{DOMAIN_PART_REGEX}))?$") @@ -98,7 +107,7 @@ def make_password_by_config(category_id, return_raw: bool = False) -> Tuple[str, raw_password = config_loader["init_password"] else: # 当且仅当自动生成密码时发送邮件 - raw_password = gen_password(8) + raw_password = gen_password(12) should_notify = True if return_raw: @@ -191,3 +200,58 @@ def check_former_passwords( def make_passwd_reset_url_by_token(token: str): """make reset""" return urllib.parse.urljoin(settings.SAAS_URL, f"set_password?token={token}") + + +def _get_bk_app_code_from_request(request) -> str: + """if the requests are from apigateway""" + return getattr(request, "bk_app_code", "") + + +def _get_bk_app_code_from_request_param(request) -> str: + """currently, some env can't get the bk_app_code from request, so we use the param to get it + FIXME: will remove later after the inner env esb change from token to jwt + """ + if not request: + return "" + return request.GET.get("app_id", "") + + +def _is_saas_request(request) -> bool: + """if the request is from saas""" + if not request: + return False + + return local.request_username == "SAAS" + + +def remove_sensitive_fields_for_profile(request, data: Dict) -> Dict: + """remove sensitive fields for profile""" + if not settings.ENABLE_PROFILE_SENSITIVE_FILTER: + return data + + # if no request or no data, return + if not (request and data): + return data + + # if from saas, return + if _is_saas_request(request): + return data + + # FIXME: currently get from request_param, + # will change to get from request after the inner env esb change from token to jwt + bk_app_code = _get_bk_app_code_from_request_param(request) + + # remove sensitive fields, except the app_code in whitelist + for key in settings.PROFILE_SENSITIVE_FIELDS: + if key in data and bk_app_code not in settings.PROFILE_SENSITIVE_FIELDS_WHITELIST_APP_CODES: + data[key] = "" + # data.pop(key) + + # remove sensitive extras fields, except the app_code in whitelist + if "extras" in data: + extras = data["extras"] + for key in settings.PROFILE_EXTRAS_SENSITIVE_FIELDS: + if key in extras and bk_app_code not in settings.PROFILE_EXTRAS_SENSITIVE_FIELDS_WHITELIST_APP_CODES: + extras.pop(key) + + return data diff --git a/src/api/bkuser_core/profiles/v2/serializers.py b/src/api/bkuser_core/profiles/v2/serializers.py index c433a7e88..7cad8f2ef 100644 --- a/src/api/bkuser_core/profiles/v2/serializers.py +++ b/src/api/bkuser_core/profiles/v2/serializers.py @@ -18,7 +18,12 @@ from bkuser_core.departments.v2.serializers import ForSyncDepartmentSerializer, SimpleDepartmentSerializer from bkuser_core.profiles.constants import TIME_ZONE_CHOICES, LanguageEnum, RoleCodeEnum from bkuser_core.profiles.models import DynamicFieldInfo, Profile -from bkuser_core.profiles.utils import force_use_raw_username, get_username, parse_username_domain +from bkuser_core.profiles.utils import ( + force_use_raw_username, + get_username, + parse_username_domain, + remove_sensitive_fields_for_profile, +) from bkuser_core.profiles.validators import validate_domain, validate_username # =============================================================================== @@ -85,6 +90,10 @@ def get_username(self, data): data.domain, ) + def to_representation(self, obj): + data = super().to_representation(obj) + return remove_sensitive_fields_for_profile(self.context.get("request", {}), data) + class Meta: model = Profile exclude = ["password"] @@ -99,6 +108,7 @@ class RapidProfileSerializer(CustomFieldsMixin, serializers.Serializer): departments = SimpleDepartmentSerializer(many=True, required=False) leader = LeaderSerializer(many=True, required=False) last_login_time = serializers.DateTimeField(required=False, read_only=True) + account_expiration_date = serializers.CharField(required=False) create_time = serializers.DateTimeField(required=False, read_only=True) update_time = serializers.DateTimeField(required=False, read_only=True) @@ -126,6 +136,10 @@ def get_extras(self, obj: "Profile") -> dict: """尝试从 context 中获取默认字段值""" return get_extras(obj.extras, self.context.get("extra_defaults", {}).copy()) + def to_representation(self, obj): + data = super().to_representation(obj) + return remove_sensitive_fields_for_profile(self.context.get("request", {}), data) + class ForSyncRapidProfileSerializer(RapidProfileSerializer): """this serializer is for sync data from one bk-user to another diff --git a/src/api/bkuser_core/profiles/v2/views.py b/src/api/bkuser_core/profiles/v2/views.py index c2926e831..b2e5d6add 100644 --- a/src/api/bkuser_core/profiles/v2/views.py +++ b/src/api/bkuser_core/profiles/v2/views.py @@ -16,7 +16,7 @@ from django.conf import settings from django.contrib.auth.hashers import make_password -from django.core.exceptions import FieldError, MultipleObjectsReturned +from django.core.exceptions import FieldError, MultipleObjectsReturned, ObjectDoesNotExist from django.db.models import F, Q from django.utils.decorators import method_decorator from django.utils.timezone import now @@ -40,6 +40,9 @@ from bkuser_core.apis.v2.viewset import AdvancedBatchOperateViewSet, AdvancedListAPIView, AdvancedModelViewSet from bkuser_core.audit.constants import LogInFailReason, OperationType from bkuser_core.audit.utils import audit_general_log, create_general_log, create_profile_log +from bkuser_core.bkiam.constants import IAMAction +from bkuser_core.bkiam.exceptions import IAMPermissionDenied +from bkuser_core.bkiam.permissions import IAMPermission, IAMPermissionExtraInfo from bkuser_core.categories.constants import CategoryType from bkuser_core.categories.loader import get_plugin_by_category from bkuser_core.categories.models import ProfileCategory @@ -59,6 +62,7 @@ make_passwd_reset_url_by_token, make_password_by_config, parse_username_domain, + remove_sensitive_fields_for_profile, ) from bkuser_core.profiles.v2.filters import ProfileSearchFilter from bkuser_core.profiles.validators import validate_username @@ -76,6 +80,8 @@ class ProfileViewSet(AdvancedModelViewSet, AdvancedListAPIView): filter_backends = [ProfileSearchFilter, filters.OrderingFilter] relation_fields = ["departments", "leader", "login_set"] + iam_filter_actions: tuple = ("list",) + def get_object(self): _default_lookup_field = self.lookup_field @@ -180,6 +186,7 @@ def list(self, request, *args, **kwargs): if fields: self._check_fields(fields) else: + # 这里没传fields默认使用slz.fields是有问题的, 但是先保持接口行为一致, 不动fields声明(新版接口解决) fields = serializer_class().fields self._ensure_enabled_field(request, fields=fields) @@ -191,13 +198,16 @@ def list(self, request, *args, **kwargs): raise error_codes.QUERY_PARAMS_ERROR # 提前将关系表拿出来 - queryset = queryset.prefetch_related(*self.relation_fields) + # BUG: 这里需要去掉 login_set(百万级的表), 大表会导致prefetch就失败 + # queryset = queryset.prefetch_related(*self.relation_fields) + queryset = queryset.prefetch_related("departments", "leader") # 当用户请求数据时,判断其是否强制输出原始 username if not force_use_raw_username(request): # 直接在 DB 中拼接 username & domain,比在 serializer 中快很多 if "username" in fields: default_domain = ProfileCategory.objects.get_default().domain + # 这里拼装的 username@domain, 没有走到serializer中的get_username queryset = queryset.extra( select={"username": "if(`domain`= %s, username, CONCAT(username, '@', domain))"}, select_params=(default_domain,), @@ -206,14 +216,23 @@ def list(self, request, *args, **kwargs): page = self.paginate_queryset(queryset) # page may be empty list if page is not None: - serializer = serializer_class(page, fields=fields, many=True) + # BUG: slz 中的 last_login_time 会导致放大查询, 需要剔除(即, 这个接口将不再支持last_login_time) + # another two property not in slz fields are: latest_check_time bad_check_cnt + if "last_login_time" in fields: + del fields["last_login_time"] + + # BUG: 这里必须显式传递 context给到slz, 下层self.context.get("request") 用到, 判断拼接 username@domain + # 坑, 修改或重构需要注意; 不要通过这种方式来决定字段格式, 非常容易遗漏 + serializer = serializer_class(page, fields=fields, many=True, context=self.get_serializer_context()) return self.get_paginated_response(serializer.data) fields = [x for x in fields if x in self._get_model_field_names()] # 全量数据太大,使用 serializer 效率非常低 # 由于存在多对多字段,所以返回列表会平铺展示,同一个 username 会多次展示 # https://docs.djangoproject.com/en/1.11/ref/models/querysets/#values - return Response(data=list(queryset.values(*fields))) + data = list(queryset.values(*fields)) + data = [remove_sensitive_fields_for_profile(request, d) for d in data] + return Response(data=data) @method_decorator(clear_cache_if_succeed) @swagger_auto_schema( @@ -228,6 +247,7 @@ def create(self, request, *args, **kwargs): from bkuser_core.departments.models import Department + # departments为空, 则绕过了第一次权限控制 deps = Department.objects.filter(id__in=validated_data.get("departments", [])) for dep in deps: self.check_object_permissions(request, obj=dep) @@ -246,6 +266,11 @@ def create(self, request, *args, **kwargs): if not ProfileCategory.objects.get(pk=validated_data["category_id"]).enabled: raise error_codes.CATEGORY_NOT_ENABLED + # 必须要有这个category的管理权限, 才能添加用户到这个目录下 + # 注意这里 saas 传的 action_id = manage_department, 必须先改成manage_category才能检查category权限 + request.META[settings.ACTION_ID_HEADER] = IAMAction.MANAGE_CATEGORY.value + self.check_object_permissions(request, obj=ProfileCategory.objects.get(pk=validated_data["category_id"])) + try: existed = Profile.objects.get( username=serializer.validated_data["username"], @@ -510,6 +535,8 @@ class BatchProfileViewSet(AdvancedBatchOperateViewSet): serializer_class = local_serializers.ProfileSerializer queryset = Profile.objects.filter(enabled=True) + permission_classes = [IAMPermission] + def get_serializer_class(self): """Serializer 路由""" if self.action in ("multiple_update", "multiple_delete"): @@ -524,12 +551,53 @@ def multiple_retrieve(self, request): """批量获取用户""" return super().multiple_retrieve(request) + def permission_denied(self, request, message=None, obj=None, **kwargs): + """针对 IAM 注入相关信息""" + raise IAMPermissionDenied( + detail=message, + extra_info=IAMPermissionExtraInfo.from_request(request, obj=obj).to_dict(), + ) + + def clean_iam_header(self, request): + if settings.ACTION_ID_HEADER in request.META: + request.META.pop(settings.ACTION_ID_HEADER) + if settings.NEED_IAM_HEADER in request.META: + request.META.pop(settings.NEED_IAM_HEADER) + + def check_category_permission(self, request, category): + # NOTE: 必须有manage_category权限才能查看/变更settings + request.META[settings.NEED_IAM_HEADER] = "True" + request.META[settings.ACTION_ID_HEADER] = IAMAction.MANAGE_CATEGORY.value + self.check_object_permissions(request, category) + + def check_permission(self, request): + self.clean_iam_header(request) + serializer_class = self.get_serializer_class() + serializer = serializer_class(data=request.data, many=True) + serializer.is_valid(raise_exception=True) + query_objs = serializer.validated_data + + for obj in query_objs: + try: + instance = self.queryset.get(pk=obj["id"]) + except ObjectDoesNotExist: + logger.warning( + "obj <%s-%s> not found or already been deleted.", + self.queryset.model, + obj, + ) + continue + else: + # NOTE: poor performance, but it's ok for now + self.check_category_permission(request, ProfileCategory.objects.get(pk=instance.category_id)) + @swagger_auto_schema( request_body=local_serializers.UpdateProfileSerializer(many=True), responses={"200": local_serializers.ProfileSerializer(many=True)}, ) def multiple_update(self, request): """批量更新用户""" + self.check_permission(request) return super().multiple_update(request) @swagger_auto_schema( @@ -538,6 +606,7 @@ def multiple_update(self, request): ) def multiple_delete(self, request): """批量删除用户""" + self.check_permission(request) return super().multiple_delete(request) @@ -682,6 +751,7 @@ def login(self, request): raise error_codes.PASSWORD_ERROR self._check_password_status(request, profile, config_loader, time_aware_now) + self._check_account_status(request, profile) create_profile_log(profile=profile, operation="LogIn", request=request, params={"is_success": True}) return Response(data=local_serializers.ProfileSerializer(profile, context={"request": request}).data) @@ -723,6 +793,20 @@ def _check_password_status( raise error_codes.PASSWORD_EXPIRED.format(data=self._generate_reset_passwd_url_with_token(profile)) + def _check_account_status(self, request, profile: Profile): + """ + 校验登录账号状态 + """ + expired_at = profile.account_expiration_date - datetime.date.today() + if expired_at.days < 0: + create_profile_log( + profile=profile, + operation="LogIn", + request=request, + params={"is_success": False, "reason": LogInFailReason.EXPIRED_USER.value}, + ) + raise error_codes.USER_IS_EXPIRED + @staticmethod def _generate_reset_passwd_url_with_token(profile: Profile) -> dict: data = {} @@ -819,6 +903,11 @@ class DynamicFieldsViewSet(AdvancedModelViewSet, AdvancedListAPIView): lookup_field: str = "name" cache_name = "profiles" + # FIXME: 这里不能开启权限, SaaS 一大堆地方查, 并且dynamic_fields是底层的服务, 查看部门等都会调用, 加上权限由于用户未申请会直接报错 + # NOTE: 当前正在重构的地方会去除这种权限控制方式 + # 先注释, 非敏感数据 + # iam_filter_actions = ("list",) + def get_serializer(self, *args, **kwargs): if self.action in ["create"]: return local_serializers.CreateFieldsSerializer(*args, **kwargs) diff --git a/src/api/bkuser_core/profiles/v3/views.py b/src/api/bkuser_core/profiles/v3/views.py index d7a278771..1817fb7eb 100644 --- a/src/api/bkuser_core/profiles/v3/views.py +++ b/src/api/bkuser_core/profiles/v3/views.py @@ -10,9 +10,10 @@ """ import logging -from rest_framework import filters, viewsets +from rest_framework import filters from rest_framework.generics import ListAPIView +from bkuser_core.apis.v2.viewset import AdvancedModelViewSet from bkuser_core.apis.v3.exceptions import QueryTooLong from bkuser_core.apis.v3.filters import MultipleFieldFilter from bkuser_core.apis.v3.serializers import AdvancedPagination @@ -26,7 +27,8 @@ logger = logging.getLogger(__name__) -class ProfileViewSet(viewsets.ModelViewSet, ListAPIView): +# class ProfileViewSet(viewsets.ModelViewSet, ListAPIView): +class ProfileViewSet(AdvancedModelViewSet, ListAPIView): """获取用户数据""" queryset = Profile.objects.all() @@ -38,6 +40,9 @@ class ProfileViewSet(viewsets.ModelViewSet, ListAPIView): foreign_fields = ["departments", "leader"] in_fields = ["username__in", "staff_status__in", "status__in"] + # 使用 filter 进行过滤的操作 + iam_filter_actions: tuple = ("list",) + @inject_serializer(query_in=QueryProfileSerializer, out=PaginatedProfileSerializer) def list(self, request, validated_data: dict, *args, **kwargs): """获取用户列表""" diff --git a/src/api/bkuser_core/tests/apis/v2/iam/test_field.py b/src/api/bkuser_core/tests/apis/v2/iam/test_field.py index bc960d1b8..daf98282e 100644 --- a/src/api/bkuser_core/tests/apis/v2/iam/test_field.py +++ b/src/api/bkuser_core/tests/apis/v2/iam/test_field.py @@ -71,7 +71,7 @@ def test_list_attr_value(self, factory, view): } request = factory.post("/api/iam/v1/fields/", body, format="json") response = view(request=request) - assert response.data["count"] == 11 + assert response.data["count"] == 14 assert response.status_code == 200 def test_list_keyword_attr_value(self, factory, view): @@ -80,7 +80,7 @@ def test_list_keyword_attr_value(self, factory, view): body = { "type": "field", "method": "list_attr_value", - "filter": {"attr": "name", "keyword": "usernam"}, + "filter": {"attr": "name", "keyword": "username"}, "page": {"offset": 0, "limit": 20}, } request = factory.post("/api/iam/v1/fields/", body, format="json") @@ -202,7 +202,7 @@ def test_offset_limit_page(self, factory, view): request = factory.post("/api/iam/v1/fields/", body, format="json") response = view(request=request) - assert response.data["count"] == 11 + assert response.data["count"] == 14 assert len(response.data["results"]) == 5 assert response.status_code == 200 @@ -216,7 +216,7 @@ def test_offset_limit_page(self, factory, view): request = factory.post("/api/iam/v1/fields/", body, format="json") response = view(request=request) - assert response.data["count"] == 11 + assert response.data["count"] == 14 assert len(response.data["results"]) == 1 assert response.status_code == 200 @@ -229,7 +229,7 @@ def test_list_instances(self, factory, view): body = {"type": "field", "method": "list_instance", "page": {"offset": 0, "limit": 20}} request = factory.post("/api/iam/v1/fields/", body, format="json") response = view(request=request) - assert response.data["count"] == 11 + assert response.data["count"] == 14 def test_list_parent_instances(self): """测试拉取实例列表, parent 过滤""" diff --git a/src/api/bkuser_core/tests/apis/v2/user_settings/test_settings_list.py b/src/api/bkuser_core/tests/apis/v2/user_settings/test_settings_list.py index 9ed2fe7ea..1d39303ce 100644 --- a/src/api/bkuser_core/tests/apis/v2/user_settings/test_settings_list.py +++ b/src/api/bkuser_core/tests/apis/v2/user_settings/test_settings_list.py @@ -42,6 +42,7 @@ def local_category(self): # --------------- List --------------- def test_category_id_list(self, factory, view, local_category): """测试拉取配置列表""" + SettingViewSet.permission_classes = [] request = factory.get(f"/api/v2/settings/?category_id={local_category.pk}") response = view(request=request) assert len(response.data) == SettingMeta.objects.filter(category_type="local").count() diff --git a/src/api/bkuser_core/tests/common/test_db_sync.py b/src/api/bkuser_core/tests/common/test_db_sync.py index bd9cb7184..7976438d6 100644 --- a/src/api/bkuser_core/tests/common/test_db_sync.py +++ b/src/api/bkuser_core/tests/common/test_db_sync.py @@ -36,7 +36,7 @@ def test_long_slice(self, model_manager): """测试长列表切片""" long_list = range(120002) slices = model_manager.make_slices(list(long_list)) - assert len(slices) == 25 + assert len(slices) == 121 assert len(slices[0]) == model_manager.meta.sharding_size assert len(slices[1]) == model_manager.meta.sharding_size assert len(slices[-1]) == 2 diff --git a/src/api/bkuser_core/user_settings/constants.py b/src/api/bkuser_core/user_settings/constants.py index 137473515..188926043 100644 --- a/src/api/bkuser_core/user_settings/constants.py +++ b/src/api/bkuser_core/user_settings/constants.py @@ -18,15 +18,21 @@ class SettingsEnableNamespaces(AutoLowerEnum): PASSWORD = auto() CONNECTION = auto() FIELDS = auto() + ACCOUNT = auto() _choices_labels = ( (GENERAL, "通用"), (PASSWORD, "密码"), (CONNECTION, "连接"), (FIELDS, "字段"), + (ACCOUNT, "账号"), ) class InitPasswordMethod(AutoLowerEnum): FIXED_PRESET = auto() RANDOM_VIA_MAIL = auto() + + +ACCOUNT_EXPIRATION_NOTICE_INTERVAL_META_KEY = "account_expiration_notice_interval" +PASSWORD_EXPIRATION_NOTICE_INTERVAL_META_KEY = "password_expiration_notice_interval" diff --git a/src/api/bkuser_core/user_settings/migrations/0002_auto_20191104_1600.py b/src/api/bkuser_core/user_settings/migrations/0002_auto_20191104_1600.py index aef78fefb..65f5cac25 100644 --- a/src/api/bkuser_core/user_settings/migrations/0002_auto_20191104_1600.py +++ b/src/api/bkuser_core/user_settings/migrations/0002_auto_20191104_1600.py @@ -61,7 +61,7 @@ def forwards_func(apps, schema_editor): }, ), dict(key="force_reset_first_login", example=True, default=True), - dict(key="enable_auto_freeze", example=True, default=True), + dict(key="enable_auto_freeze", example=False, default=False), dict(key="freeze_after_days", example=180, default=180), ] diff --git a/src/api/bkuser_core/user_settings/migrations/0015_alter_settingmeta_namespace.py b/src/api/bkuser_core/user_settings/migrations/0015_alter_settingmeta_namespace.py new file mode 100644 index 000000000..611abf9bf --- /dev/null +++ b/src/api/bkuser_core/user_settings/migrations/0015_alter_settingmeta_namespace.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.5 on 2022-05-20 02:25 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('user_settings', '0014_alter_local_password_settings'), + ] + + operations = [ + migrations.AlterField( + model_name='settingmeta', + name='namespace', + field=models.CharField(choices=[('general', '通用'), ('password', '密码'), ('connection', '连接'), ('fields', '字段'), ('account', '账号')], db_index=True, default='general', max_length=32, verbose_name='命名空间'), + ), + ] diff --git a/src/api/bkuser_core/user_settings/migrations/0016_add_default_fields_account_settings.py b/src/api/bkuser_core/user_settings/migrations/0016_add_default_fields_account_settings.py new file mode 100644 index 000000000..e066632ae --- /dev/null +++ b/src/api/bkuser_core/user_settings/migrations/0016_add_default_fields_account_settings.py @@ -0,0 +1,142 @@ +# -*- coding: utf-8 -*- +""" +TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-用户管理(Bk-User) 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 __future__ import unicode_literals + +from bkuser_core.categories.constants import CategoryType +from bkuser_core.user_settings.constants import SettingsEnableNamespaces +from django.db import migrations + + +def forwards_func(apps, schema_editor): + """添加本地用户目录账号设置""" + + SettingMeta = apps.get_model("user_settings", "SettingMeta") + Setting = apps.get_model("user_settings", "Setting") + ProfileCategory = apps.get_model("categories", "ProfileCategory") + + local_account_settings = [ + dict( + key="expired_after_days", + example=-1, + default=-1, + ), + + dict( + key="account_expiration_notice_methods", + example=["send_email"], + default=["send_email"], + ), + dict( + key="account_expiration_notice_interval", + example=[1, 7, 15], + default=[1, 7, 15] + ), + dict( + key="expiring_account_email_config", + example={ + "title": "蓝鲸智云 - 账号到期提醒", + "sender": "蓝鲸智云", + "content": "{username},您好:您的蓝鲸智云平台账号将于{expired_at}天后到期," + "为避免影响使用,请尽快联系平台管理员进行续期。蓝鲸智云平台用户管理处", + "content_html": '

{username},您好:

        您的蓝鲸智云' + '平台账号将于{expired_at}天后到期,为避免影响使用,请尽快联系平台管' + '理员进行续期。

蓝鲸智云平台用户管理处

', + }, + default={ + "title": "蓝鲸智云 - 账号到期提醒", + "sender": "蓝鲸智云", + "content": "{username},您好:您的蓝鲸智云平台账号将于{expired_at}天后到期," + "为避免影响使用,请尽快联系平台管理员进行续期。蓝鲸智云平台用户管理处", + "content_html": '

{username},您好:

        您的蓝鲸智云' + '平台账号将于{expired_at}天后到期,为避免影响使用,请尽快联系平台管' + '理员进行续期。

蓝鲸智云平台用户管理处

', + }, + ), + dict( + key="expired_account_email_config", + example={ + "title": "蓝鲸智云 - 账号到期提醒", + "sender": "蓝鲸智云", + "content": "{username},您好:您的蓝鲸智云平台账号已过期,为避免影响使用,请尽快联系平台管理员进行续期。蓝鲸智云平台用户管理处", + "content_html": '

{username},您好:

        您的蓝鲸智云平台账号已过期,为避免影响使用,请尽快联系平台管理员进行续期。

蓝鲸智云平台用户管理处

' + }, + default={ + "title": "蓝鲸智云 - 账号到期提醒", + "sender": "蓝鲸智云", + "content": "{username},您好:您的蓝鲸智云平台账号已过期,为避免影响使用,请尽快联系平台管理员进行续期。蓝鲸智云平台用户管理处", + "content_html": '

{username},您好:

        您的蓝鲸智云平台账号已过期,为避免影响使用,请尽快联系平台管理员进行续期。

蓝鲸智云平台用户管理处

' + }, + ), + dict( + key="expiring_account_sms_config", + example={ + "sender": "蓝鲸智云", + "content": "【蓝鲸智云】账号到期提醒!{username},您好,您的蓝鲸平台账号将于{expired_at}天后到期,为避免影响使用,请尽快联系平台管理员进行续期。", + "content_html": '

【蓝鲸智云】账号到期提醒!{username}您好,您的蓝鲸平台账号将于{expired_at}天后到期,为避免' + '影响使用,请尽快联系平台管理员进行续期。

', + }, + default={ + "sender": "蓝鲸智云", + "content": "【蓝鲸智云】账号到期提醒!{username},您好,您的蓝鲸平台账号将于{expired_at}天后到期,为避免影响使用,请尽快联系平台管理员进行续期。", + "content_html": '

【蓝鲸智云】账号到期提醒!{username}您好,您的蓝鲸平台账号将于{expired_at}天后到期,为避免' + '影响使用,请尽快联系平台管理员进行续期。

', + }, + ), + dict( + key="expired_account_sms_config", + example={ + "sender": "蓝鲸智云", + "content": "【蓝鲸智云】账号到期提醒!{username},您好,您的蓝鲸智云平台账号已过期," + "为避免影响使用,请尽快联系平台管理员进行续期。", + "content_html": '

【蓝鲸智云】账号到期提醒!{username}您好,您的蓝鲸平台账号已到期,为避免影响使用,请尽快联系' + '平台管理员进行续期。

', + }, + default={ + "sender": "蓝鲸智云", + "content": "【蓝鲸智云】账号到期提醒!{username},您好,您的蓝鲸智云平台账号已过期," + "为避免影响使用,请尽快联系平台管理员进行续期。", + "content_html": '

【蓝鲸智云】账号到期提醒!{username}您好,您的蓝鲸平台账号已到期,为避免影响使用,请尽快联系' + '平台管理员进行续期。

', + }, + ), + + ] + + for x in local_account_settings: + meta, _ = SettingMeta.objects.get_or_create( + namespace=SettingsEnableNamespaces.ACCOUNT.value, + category_type=CategoryType.LOCAL.value, + required=True, + **x + ) + # 保证已存在的目录拥有默认配置 + for c in ProfileCategory.objects.filter(type=CategoryType.LOCAL.value): + Setting.objects.get_or_create(meta=meta, category_id=c.id, value=meta.default) + + +def backwards_func(apps, schema_editor): + SettingMeta = apps.get_model("user_settings", "SettingMeta") + meta = SettingMeta.objects.get( + namespace=SettingsEnableNamespaces.ACCOUNT.value, + category_type=CategoryType.LOCAL.value + ) + Setting = apps.get_model("user_settings", "Setting") + Setting.objects.filter(category__type=CategoryType.LOCAL.value, meta=meta).delete() + meta.delete() + + +class Migration(migrations.Migration): + + dependencies = [ + ("user_settings", "0015_alter_settingmeta_namespace"), + ] + + operations = [migrations.RunPython(forwards_func, backwards_func)] diff --git a/src/api/bkuser_core/user_settings/migrations/0017_add_default_fields_password_settings.py b/src/api/bkuser_core/user_settings/migrations/0017_add_default_fields_password_settings.py new file mode 100644 index 000000000..0369a4ec6 --- /dev/null +++ b/src/api/bkuser_core/user_settings/migrations/0017_add_default_fields_password_settings.py @@ -0,0 +1,155 @@ +# -*- coding: utf-8 -*- +""" +TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-用户管理(Bk-User) 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 __future__ import unicode_literals + +from bkuser_core.categories.constants import CategoryType +from bkuser_core.user_settings.constants import SettingsEnableNamespaces +from django.db import migrations + + +def forwards_func(apps, schema_editor): + """添加本地用户目录密码设置""" + + SettingMeta = apps.get_model("user_settings", "SettingMeta") + Setting = apps.get_model("user_settings", "Setting") + ProfileCategory = apps.get_model("categories", "ProfileCategory") + + local_password_settings = [ + + dict( + key="password_expiration_notice_methods", + example=["send_email"], + default=["send_email"], + ), + dict( + key="password_expiration_notice_interval", + example=[1, 7, 15], + default=[1, 7, 15] + ), + dict( + key="expiring_password_email_config", + example={ + "title": "【蓝鲸智云】密码到期提醒", + "sender": "蓝鲸智云", + "content": "{username},您好:您的蓝鲸智云平台账号将于{expired_at}天后到期," + "为避免影响使用,请尽快登陆平台修改密码。蓝鲸智云平台用户管理处", + "content_html": '

{username},您好:

 ' + '       您的蓝鲸智云平台账号将于' + '{expired_at}天后到期,为避免影响使用,请尽快登陆平台修改密码。' + '

蓝鲸智云平台用户管理处

', + }, + default={ + "title": "【蓝鲸智云】密码到期提醒", + "sender": "蓝鲸智云", + "content": "{username},您好:您的蓝鲸智云平台账号将于{expired_at}天后到期," + "为避免影响使用,请尽快登陆平台修改密码。蓝鲸智云平台用户管理处", + "content_html": '

{username},您好:

 ' + '       您的蓝鲸智云平台账号将于' + '{expired_at}天后到期,为避免影响使用,请尽快登陆平台修改密码。' + '

蓝鲸智云平台用户管理处

', + }, + ), + dict( + key="expired_password_email_config", + example={ + "title": "【蓝鲸智云】密码到期提醒", + "sender": "蓝鲸智云", + "content": "{username},您好:您的蓝鲸智云平台密码已过期,为避免影响使用,请尽快登陆平台修改密码。蓝鲸智云平台用户管理处", + "content_html": '

{username},您好:

 ' + '       您的蓝鲸智云平台密码已过期,为避免影响使用,请尽快登陆平台修改密码。

' + '

蓝鲸智云平台用户管理处

', + }, + default={ + "title": "【蓝鲸智云】密码到期提醒", + "sender": "蓝鲸智云", + "content": "{username},您好:您的蓝鲸智云平台密码已过期,为避免影响使用,请尽快登陆平台修改密码。蓝鲸智云平台用户管理处", + "content_html": '

{username},您好:

 ' + '       您的蓝鲸智云平台密码已过期,为避免影响使用,请尽快登陆平台修改密码。

' + '

蓝鲸智云平台用户管理处

', + }, + ), + dict( + key="expiring_password_sms_config", + example={ + "sender": "蓝鲸智云", + "content": "【蓝鲸智云】密码到期提醒!{username},您好,您的蓝鲸平台密码将于{expired_at}天后到期,为避免影响使用,请尽快" + "登陆平台修改密码。", + "content_html": '

【蓝鲸智云】密码到期提醒!{username},您好,您的蓝鲸平台密码将于 {expired_at} 天后到期,' + '为避免影响使用,请尽快登陆平台修改密码。

', + }, + default={ + "sender": "蓝鲸智云", + "content": "【蓝鲸智云】密码到期提醒!{username},您好,您的蓝鲸平台密码将于{expired_at}天后到期,为避免影响使用,请尽快" + "登陆平台修改密码。", + "content_html": '

【蓝鲸智云】密码到期提醒!{username},您好,您的蓝鲸平台密码将于 {expired_at} 天后到期,' + '为避免影响使用,请尽快登陆平台修改密码。

', + }, + ), + dict( + key="expired_password_sms_config", + example={ + "sender": "蓝鲸智云", + "content": "【蓝鲸智云】密码到期提醒!{username}您好!您的蓝鲸智云平台密码已过期,为避免影响使用,请尽快登陆平台修改密码。", + "content_html": '

【蓝鲸智云】密码到期提醒!{username}您好!您的蓝鲸智云平台密码已过期,为避免影响使用,请尽快登陆平台' + '修改密码。

', + }, + default={ + "sender": "蓝鲸智云", + "content": "【蓝鲸智云】密码到期提醒!{username}您好!您的蓝鲸智云平台密码已过期,为避免影响使用,请尽快登陆平台修改密码。", + "content_html": '

【蓝鲸智云】密码到期提醒!{username}您好!您的蓝鲸智云平台密码已过期,为避免影响使用,请尽快登陆平台' + '修改密码。

', + }, + ), + + ] + + for x in local_password_settings: + meta, _ = SettingMeta.objects.get_or_create( + namespace=SettingsEnableNamespaces.PASSWORD.value, + category_type=CategoryType.LOCAL.value, + required=True, + **x + ) + # 保证已存在的目录拥有默认配置 + for c in ProfileCategory.objects.filter(type=CategoryType.LOCAL.value): + Setting.objects.get_or_create(meta=meta, category_id=c.id, value=meta.default) + + +def backwards_func(apps, schema_editor): + SettingMeta = apps.get_model("user_settings", "SettingMeta") + Setting = apps.get_model("user_settings", "Setting") + + meta_keys = [ + "password_expiration_notice_methods", + "password_expiration_notice_interval", + "expiring_password_email_config", + "expired_password_email_config", + "expiring_password_sms_config", + "expired_password_sms_config" + ] + + for meta_key in meta_keys: + meta = SettingMeta.objects.get( + namespace=SettingsEnableNamespaces.PASSWORD.value, + category_type=CategoryType.LOCAL.value, + key=meta_key + ) + + Setting.objects.filter(category__type=CategoryType.LOCAL.value, meta=meta).delete() + meta.delete() + + +class Migration(migrations.Migration): + dependencies = [ + ("user_settings", "0016_add_default_fields_account_settings"), + ] + + operations = [migrations.RunPython(forwards_func, backwards_func)] diff --git a/src/api/bkuser_core/user_settings/views.py b/src/api/bkuser_core/user_settings/views.py index 9d870631d..deb3bb979 100644 --- a/src/api/bkuser_core/user_settings/views.py +++ b/src/api/bkuser_core/user_settings/views.py @@ -10,12 +10,14 @@ """ import logging +from django.conf import settings as dj_settings from django.utils.decorators import method_decorator from drf_yasg.utils import swagger_auto_schema from rest_framework import status from rest_framework.response import Response from bkuser_core.apis.v2.viewset import AdvancedListAPIView, AdvancedModelViewSet +from bkuser_core.bkiam.constants import IAMAction from bkuser_core.categories.models import ProfileCategory from bkuser_core.common.cache import clear_cache_if_succeed from bkuser_core.common.error_codes import error_codes @@ -36,6 +38,18 @@ class SettingViewSet(AdvancedModelViewSet): serializer_class = serializers.SettingSerializer lookup_field: str = "id" + def clean_iam_header(self, request): + if dj_settings.ACTION_ID_HEADER in request.META: + request.META.pop(dj_settings.ACTION_ID_HEADER) + if dj_settings.NEED_IAM_HEADER in request.META: + request.META.pop(dj_settings.NEED_IAM_HEADER) + + def check_category_permission(self, request, category): + # NOTE: 必须有manage_category权限才能查看/变更settings + request.META[dj_settings.NEED_IAM_HEADER] = "True" + request.META[dj_settings.ACTION_ID_HEADER] = IAMAction.MANAGE_CATEGORY.value + self.check_object_permissions(request, category) + @staticmethod def _get_category(category_id: int): # 目前只匹配 category_id @@ -67,6 +81,8 @@ def list(self, request, *args, **kwargs): category_id = validated_data.pop("category_id") category = self._get_category(category_id=category_id) + self.check_category_permission(request, category) + metas = self._get_metas(category, validated_data) settings = Setting.objects.filter(meta__in=metas, category_id=category_id) return Response(data=serializers.SettingSerializer(settings, many=True).data) @@ -83,6 +99,8 @@ def create(self, request, *args, **kwargs): category = self._get_category(category_id=validated_data.pop("category_id")) value = validated_data.pop("value") + self.check_category_permission(request, category) + metas = self._get_metas(category, validated_data) if metas.count() != 1: raise error_codes.CANNOT_FIND_SETTING_META @@ -100,7 +118,11 @@ def create(self, request, *args, **kwargs): return Response(serializers.SettingSerializer(setting).data, status=status.HTTP_201_CREATED) def _update(self, request, validated_data): + self.clean_iam_header(request) instance = self.get_object() + + self.check_category_permission(request, instance.category) + try: need_update = is_obj_needed_update(instance, validated_data) except ValueError: @@ -132,10 +154,34 @@ def update(self, request, validated_data, *args, **kwargs): def partial_update(self, request, validated_data, *args, **kwargs): return self._update(request, validated_data) + # NOTE: saas没有用到, 暂时不需要封装check permission + # def retrieve(self, request, *args, **kwargs): + # fields = self._get_fields() + # self._check_fields(fields) + + # self.clean_iam_header(request) + # instance = self.get_object() + # self.check_category_permission(request, instance.category) + + # return Response( + # data=self.get_serializer(instance, fields=fields).data, + # status=status.HTTP_200_OK, + # ) + + def destroy(self, request, *args, **kwargs): + self.clean_iam_header(request) + instance = self.get_object() + + self.check_category_permission(request, instance.category) + instance.delete() + return Response(status=status.HTTP_204_NO_CONTENT) + class SettingMetaViewSet(AdvancedModelViewSet, AdvancedListAPIView): """配置信息""" + # FIXME: 没有任何权限管控 + queryset = SettingMeta.objects.all() serializer_class = serializers.SettingMetaSerializer lookup_field = "id" diff --git a/src/api/poetry.lock b/src/api/poetry.lock index 191ec59e7..6d4fa9220 100644 --- a/src/api/poetry.lock +++ b/src/api/poetry.lock @@ -45,7 +45,7 @@ reference = "tencent-mirrors" [[package]] name = "apigw-manager" -version = "1.0.10" +version = "1.1.5" description = "" category = "main" optional = false @@ -55,12 +55,13 @@ python-versions = ">=3.6.1,<4.0.0" bkapi-bk-apigateway = ">=1.0.6,<2.0.0" bkapi-client-core = ">=1.1.3" future = "0.18.2" +packaging = ">=21.0" pyyaml = ">=5.4.1" urllib3 = ">=1.25.3" [package.extras] -django = ["Django (>=1.11.1)", "packaging (>=21.0)", "pyjwt (>=1.6.4)"] -demo = ["Django (>=1.11.1)", "PyMySQL (>=1.0.2,<2.0.0)", "django-environ (>=0.8.1)", "packaging (>=21.0)", "pyjwt (>=1.6.4)"] +django = ["Django (>=1.11.1)", "pyjwt (>=1.6.4)"] +demo = ["Django (>=1.11.1)", "PyMySQL (>=1.0.2,<2.0.0)", "django-environ (>=0.8.1)", "pyjwt (>=1.6.4)"] cryptography = ["cryptography (>=3.1.1)", "pyjwt (>=1.6.4)"] [package.source] @@ -555,7 +556,7 @@ reference = "tencent-mirrors" [[package]] name = "django" -version = "3.2.13" +version = "3.2.15" description = "A high-level Python Web framework that encourages rapid development and clean, pragmatic design." category = "main" optional = false @@ -2663,7 +2664,7 @@ reference = "tencent-mirrors" [metadata] lock-version = "1.1" python-versions = "3.6.14" -content-hash = "2328289d38120923fc9eea44d3a94abbee6efdc3b098144f3f8b24a55cf84f74" +content-hash = "a5df43ed5650d6e3aa47d75a0ec94bea78d86ecf964646bb92734b3fa84308e9" [metadata.files] aenum = [ @@ -2680,7 +2681,7 @@ amqp = [ {file = "amqp-2.6.1.tar.gz", hash = "sha256:70cdb10628468ff14e57ec2f751c7aa9e48e7e3651cfd62d431213c0c4e58f21"}, ] apigw-manager = [ - {file = "apigw_manager-1.0.10-py3-none-any.whl", hash = "md5:ec5bb61bfae9e55d5cf37a59022473dc"}, + {file = "apigw_manager-1.1.5-py3-none-any.whl", hash = "md5:cc2df9c0dc88460737b338213b7cc511"}, ] appnope = [ {file = "appnope-0.1.3-py2.py3-none-any.whl", hash = "sha256:265a455292d0bd8a72453494fa24df5a11eb18373a60c7c0430889f22548605e"}, @@ -2897,8 +2898,8 @@ deprecated = [ {file = "Deprecated-1.2.13.tar.gz", hash = "sha256:43ac5335da90c31c24ba028af536a91d41d53f9e6901ddb021bcc572ce44e38d"}, ] django = [ - {file = "Django-3.2.13-py3-none-any.whl", hash = "sha256:b896ca61edc079eb6bbaa15cf6071eb69d6aac08cce5211583cfb41515644fdf"}, - {file = "Django-3.2.13.tar.gz", hash = "sha256:6d93497a0a9bf6ba0e0b1a29cccdc40efbfc76297255b1309b3a884a688ec4b6"}, + {file = "Django-3.2.15-py3-none-any.whl", hash = "sha256:115baf5049d5cf4163e43492cdc7139c306ed6d451e7d3571fe9612903903713"}, + {file = "Django-3.2.15.tar.gz", hash = "sha256:f71934b1a822f14a86c9ac9634053689279cd04ae69cb6ade4a59471b886582b"}, ] django-celery-beat = [ {file = "django-celery-beat-2.2.0.tar.gz", hash = "sha256:b8a13afb15e7c53fc04f4f847ac71a6d32088959aba701eb7c4a59f0c28ba543"}, diff --git a/src/api/pyproject.toml b/src/api/pyproject.toml index 82fce2b46..b48791d0b 100644 --- a/src/api/pyproject.toml +++ b/src/api/pyproject.toml @@ -7,7 +7,7 @@ authors = ["IMBlues "] [tool.poetry.dependencies] python = "3.6.14" aenum = "2.2.1" -Django = "3.2.13" +Django = "3.2.15" django-celery-beat = "2.2.0" celery = "4.4.7" drf-yasg = "1.20.0" @@ -40,7 +40,7 @@ django-prometheus = "^2.1.0" pyyaml = "^6.0" sentry-sdk = "1.5.6" pyjwt = "^2.3.0" -apigw-manager = "^1.0.3" +apigw-manager = "1.1.5" django-celery-results = "2.0.1" werkzeug = "2.0.3" packaging = "21.0" diff --git a/src/bkuser_global/tracing/instrumentor.py b/src/bkuser_global/tracing/instrumentor.py index 0d036bec3..22d03e8e9 100644 --- a/src/bkuser_global/tracing/instrumentor.py +++ b/src/bkuser_global/tracing/instrumentor.py @@ -18,7 +18,7 @@ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.instrumentation.logging import LoggingInstrumentor from opentelemetry.instrumentation.requests import RequestsInstrumentor -from opentelemetry.trace import Span, Status, StatusCode +from opentelemetry.trace import Span, Status, StatusCode, format_trace_id def requests_callback(span: Span, response): @@ -79,6 +79,14 @@ def requests_callback(span: Span, response): span.set_attribute("request_id", request_id) +def django_request_hook(span, request): + """ + 在request注入trace_id,方便获取 + """ + trace_id = span.get_span_context().trace_id + request.otel_trace_id = format_trace_id(trace_id) + + def django_response_hook(span, request, response): """ 处理蓝鲸标准协议 Django 响应 @@ -121,12 +129,16 @@ def instrumentation_dependencies(self) -> Collection[str]: def _instrument(self, **kwargs): LoggingInstrumentor().instrument() + print("otel instructment: logging") RequestsInstrumentor().instrument(span_callback=requests_callback) - DjangoInstrumentor().instrument(response_hook=django_response_hook) + print("otel instructment: requests") + DjangoInstrumentor().instrument(request_hook=django_request_hook, response_hook=django_response_hook) + print("otel instructment: django") try: from opentelemetry.instrumentation.redis import RedisInstrumentor RedisInstrumentor().instrument() + print("otel instructment: redis") except Exception: # pylint: disable=broad-except # ignore redis instrumentor if it's not installed pass @@ -135,6 +147,7 @@ def _instrument(self, **kwargs): from opentelemetry.instrumentation.celery import CeleryInstrumentor CeleryInstrumentor().instrument() + print("otel instructment: celery") if getattr(settings, "BKAPP_OTEL_INSTRUMENT_DB_API", False): import MySQLdb # noqa @@ -146,7 +159,9 @@ def _instrument(self, **kwargs): "mysql", {"database": "db", "port": "port", "host": "host", "user": "user"}, ) + print("otel instructment: database api") def _uninstrument(self, **kwargs): for instrumentor in self.instrumentors: + print("otel uninstrument", instrumentor) instrumentor.uninstrument() diff --git a/src/bkuser_global/tracing/otel.py b/src/bkuser_global/tracing/otel.py index b05f0f418..32f9aff5e 100644 --- a/src/bkuser_global/tracing/otel.py +++ b/src/bkuser_global/tracing/otel.py @@ -9,19 +9,47 @@ specific language governing permissions and limitations under the License. """ import os +import threading from django.conf import settings from opentelemetry import trace from opentelemetry.exporter.jaeger.thrift import JaegerExporter from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter from opentelemetry.sdk.resources import SERVICE_NAME, Resource -from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace import ReadableSpan, TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor from opentelemetry.sdk.trace.sampling import _KNOWN_SAMPLERS from .instrumentor import BKAppInstrumentor +class LazyBatchSpanProcessor(BatchSpanProcessor): + def __init__(self, *args, **kwargs): + super(LazyBatchSpanProcessor, self).__init__(*args, **kwargs) + # 停止默认线程 + self.done = True + with self.condition: + self.condition.notify_all() + self.worker_thread.join() + self.done = False + self.worker_thread = None + + def on_end(self, span: ReadableSpan) -> None: + if self.worker_thread is None: + self.worker_thread = threading.Thread(name=self.__class__.__name__, target=self.worker, daemon=True) + self.worker_thread.start() + super(LazyBatchSpanProcessor, self).on_end(span) + + def shutdown(self) -> None: + # signal the worker thread to finish and then wait for it + self.done = True + with self.condition: + self.condition.notify_all() + if self.worker_thread: + self.worker_thread.join() + self.span_exporter.shutdown() + + def setup_trace_config(): is_environment_dev = os.getenv("DJANGO_SETTINGS_MODULE", "").endswith(".dev") if is_environment_dev: @@ -31,7 +59,9 @@ def setup_trace_config(): TracerProvider(resource=Resource.create({SERVICE_NAME: settings.BKAPP_OTEL_SERVICE_NAME})) ) jaeger_exporter = JaegerExporter( - agent_host_name="localhost", agent_port=6831, udp_split_oversized_batches=True + agent_host_name="localhost", + agent_port=6831, + udp_split_oversized_batches=True, ) trace.get_tracer_provider().add_span_processor(BatchSpanProcessor(jaeger_exporter)) else: @@ -40,18 +70,22 @@ def setup_trace_config(): resource=Resource.create( { "service.name": settings.BKAPP_OTEL_SERVICE_NAME, - "bk_data_id": settings.BKAPP_OTEL_BK_DATA_ID, + "bk.data.token": settings.BKAPP_OTEL_DATA_TOKEN, }, ), sampler=_KNOWN_SAMPLERS[settings.BKAPP_OTEL_SAMPLER], ) ) - otlp_exporter = OTLPSpanExporter(endpoint=settings.BKAPP_OTEL_GRPC_HOST) - span_processor = BatchSpanProcessor(otlp_exporter) + otlp_exporter = OTLPSpanExporter(endpoint=settings.BKAPP_OTEL_GRPC_HOST, insecure=True) + # span_processor = BatchSpanProcessor(otlp_exporter) + span_processor = LazyBatchSpanProcessor(otlp_exporter) trace.get_tracer_provider().add_span_processor(span_processor) def setup_by_settings(): if getattr(settings, "ENABLE_OTEL_TRACE", False): + print("ENABLE_OTEL_TRACE = True") setup_trace_config() BKAppInstrumentor().instrument() + else: + print("ENABLE_OTEL_TRACE = False") diff --git a/src/login/bin/start.sh b/src/login/bin/start.sh index 9287bb70a..fac4f8c64 100755 --- a/src/login/bin/start.sh +++ b/src/login/bin/start.sh @@ -3,7 +3,8 @@ python manage.py collectstatic python manage.py compilemessages + command="gunicorn wsgi -w 16 --timeout 150 -b [::]:5000 -k gevent --max-requests 1024 --access-logfile '-' --access-logformat '%(h)s %(l)s %(u)s %(t)s \"%(r)s\" %(s)s %(b)s \"%(f)s\" \"%(a)s\" in %(L)s seconds' --log-level INFO --log-file=- --env prometheus_multiproc_dir=/tmp/" ## Run! -exec bash -c "$command" +exec bash -c "$command" \ No newline at end of file diff --git a/src/login/bklogin/backends/bk.py b/src/login/bklogin/backends/bk.py index 1d65ca854..50841100c 100755 --- a/src/login/bklogin/backends/bk.py +++ b/src/login/bklogin/backends/bk.py @@ -13,7 +13,7 @@ from django.contrib.auth import get_user_model from django.contrib.auth.backends import ModelBackend -from bklogin.common.exceptions import AuthenticationError, PasswordNeedReset +from bklogin.common.exceptions import AuthenticationError, PasswordNeedReset, UserExpiredException from bklogin.common.log import logger from bklogin.common.usermgr import get_categories_str from bklogin.components import usermgr_api @@ -50,6 +50,7 @@ class BkUserCheckCode(int, StructuredEnum): CATEGORY_NOT_ENABLED = 3210019 ERROR_FORMAT = 3210020 SHOULD_CHANGE_INITIAL_PASSWORD = 3210021 + USER_IS_EXPIRED = 3210024 class BkUserBackend(ModelBackend): @@ -91,6 +92,8 @@ def authenticate(self, request, username=None, password=None, **kwargs): if not ok: if code in [BkUserCheckCode.SHOULD_CHANGE_INITIAL_PASSWORD, BkUserCheckCode.PASSWORD_EXPIRED]: raise PasswordNeedReset(message=message, reset_password_url=extra_values.get("reset_password_url")) + elif code == BkUserCheckCode.USER_IS_EXPIRED: + raise UserExpiredException raise AuthenticationError(message=message, redirect_to=extra_values.get("redirect_to")) # set the username to real username diff --git a/src/login/bklogin/bkauth/views.py b/src/login/bklogin/bkauth/views.py index 957ffaea1..b811014a4 100755 --- a/src/login/bklogin/bkauth/views.py +++ b/src/login/bklogin/bkauth/views.py @@ -26,7 +26,7 @@ from bklogin.bkauth.constants import REDIRECT_FIELD_NAME from bklogin.bkauth.forms import BkAuthenticationForm from bklogin.bkauth.utils import is_safe_url, set_bk_token_invalid -from bklogin.common.exceptions import AuthenticationError, PasswordNeedReset +from bklogin.common.exceptions import AuthenticationError, PasswordNeedReset, UserExpiredException from bklogin.common.log import logger from bklogin.common.mixins.exempt import LoginExemptMixin from bklogin.common.usermgr import get_categories_str @@ -113,6 +113,7 @@ def _bk_login(request): error_message = "" login_redirect_to = "" + user_expired = False # POST if request.method == "POST" and is_license_ok: @@ -126,6 +127,9 @@ def _bk_login(request): except PasswordNeedReset as e: token_set_password_url = e.reset_password_url error_message = e.message + except UserExpiredException as e: + login_redirect_to = e.redirect_to + user_expired = e.user_expired else: error_message = _("账户或者密码错误,请重新输入") # GET @@ -139,6 +143,7 @@ def _bk_login(request): context = { "form": form, "error_message": error_message, + "user_expired": user_expired, REDIRECT_FIELD_NAME: redirect_to, "site": current_site, "site_name": current_site.name, diff --git a/src/login/bklogin/common/exceptions.py b/src/login/bklogin/common/exceptions.py index ac4fa35a3..890006202 100755 --- a/src/login/bklogin/common/exceptions.py +++ b/src/login/bklogin/common/exceptions.py @@ -45,3 +45,14 @@ class PasswordNeedReset(Exception): def __init__(self, reset_password_url: str, message: Optional[str] = None): self.reset_password_url = reset_password_url self.message = message or _("登录校验失败,请重置密码") + + +class UserExpiredException(Exception): + """Auth failure due to user had expired""" + + redirect_to = "" + + def __init__(self, redirect_to=None): + self.user_expired = True + if redirect_to: + self.redirect_to = redirect_to diff --git a/src/login/bklogin/config/common/system.py b/src/login/bklogin/config/common/system.py index 4575de1ee..b00da8dcb 100644 --- a/src/login/bklogin/config/common/system.py +++ b/src/login/bklogin/config/common/system.py @@ -15,6 +15,18 @@ # ============================================================================== SENTRY_DSN = env("SENTRY_DSN", default="") +# ============================================================================== +# OTEL +# ============================================================================== +# tracing: otel 相关配置 +# if enable, default false +ENABLE_OTEL_TRACE = env.bool("BKAPP_ENABLE_OTEL_TRACE", default=False) +BKAPP_OTEL_INSTRUMENT_DB_API = env.bool("BKAPP_OTEL_INSTRUMENT_DB_API", default=False) +BKAPP_OTEL_SERVICE_NAME = env("BKAPP_OTEL_SERVICE_NAME", default="bk-login") +BKAPP_OTEL_SAMPLER = env("BKAPP_OTEL_SAMPLER", default="always_on") +BKAPP_OTEL_GRPC_HOST = env("BKAPP_OTEL_GRPC_HOST", default="") +BKAPP_OTEL_DATA_TOKEN = env("BKAPP_OTEL_DATA_TOKEN", default="") + # ============================================================================== # HTTP CONNECTIONS diff --git a/src/login/bklogin/monitoring/apps.py b/src/login/bklogin/monitoring/apps.py index dc4276b96..285c3ad8f 100644 --- a/src/login/bklogin/monitoring/apps.py +++ b/src/login/bklogin/monitoring/apps.py @@ -11,6 +11,7 @@ """ from django.apps import AppConfig +from bkuser_global.tracing.otel import setup_by_settings from bkuser_global.tracing.sentry import init_sentry_sdk @@ -18,4 +19,5 @@ class MonitoringConfig(AppConfig): name = "bklogin.monitoring" def ready(self): + setup_by_settings() init_sentry_sdk("bk-login", django_integrated=True) diff --git a/src/login/bklogin/templates/account/login_ce.html b/src/login/bklogin/templates/account/login_ce.html index 6007031bb..ef50a3db9 100755 --- a/src/login/bklogin/templates/account/login_ce.html +++ b/src/login/bklogin/templates/account/login_ce.html @@ -43,6 +43,14 @@
diff --git a/src/login/poetry.lock b/src/login/poetry.lock index 1239dac19..b32dc6e1c 100644 --- a/src/login/poetry.lock +++ b/src/login/poetry.lock @@ -1,3 +1,19 @@ +[[package]] +name = "aiocontextvars" +version = "0.2.2" +description = "Asyncio support for PEP-567 contextvars backport." +category = "main" +optional = false +python-versions = ">=3.5" + +[package.dependencies] +contextvars = {version = "2.4", markers = "python_version < \"3.7\""} + +[package.source] +type = "legacy" +url = "https://mirrors.tencent.com/pypi/simple" +reference = "tencent-mirrors" + [[package]] name = "appnope" version = "0.1.3" @@ -75,6 +91,19 @@ type = "legacy" url = "https://mirrors.tencent.com/pypi/simple" reference = "tencent-mirrors" +[[package]] +name = "backoff" +version = "1.10.0" +description = "Function decoration for backoff and retry" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.source] +type = "legacy" +url = "https://mirrors.tencent.com/pypi/simple" +reference = "tencent-mirrors" + [[package]] name = "black" version = "21.12b0" @@ -217,6 +246,22 @@ type = "legacy" url = "https://mirrors.tencent.com/pypi/simple" reference = "tencent-mirrors" +[[package]] +name = "contextvars" +version = "2.4" +description = "PEP 567 Backport" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +immutables = ">=0.9" + +[package.source] +type = "legacy" +url = "https://mirrors.tencent.com/pypi/simple" +reference = "tencent-mirrors" + [[package]] name = "coverage" version = "6.2" @@ -299,6 +344,25 @@ type = "legacy" url = "https://mirrors.tencent.com/pypi/simple" reference = "tencent-mirrors" +[[package]] +name = "deprecated" +version = "1.2.13" +description = "Python @deprecated decorator to deprecate old python classes, functions or methods." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.dependencies] +wrapt = ">=1.10,<2" + +[package.extras] +dev = ["tox", "bump2version (<1)", "sphinx (<2)", "importlib-metadata (<3)", "importlib-resources (<4)", "configparser (<5)", "sphinxcontrib-websupport (<2)", "zipp (<2)", "PyTest (<5)", "PyTest-Cov (<2.6)", "pytest", "pytest-cov"] + +[package.source] +type = "legacy" +url = "https://mirrors.tencent.com/pypi/simple" +reference = "tencent-mirrors" + [[package]] name = "dj-static" version = "0.0.6" @@ -317,7 +381,7 @@ reference = "tencent-mirrors" [[package]] name = "django" -version = "3.2.13" +version = "3.2.15" description = "A high-level Python Web framework that encourages rapid development and clean, pragmatic design." category = "main" optional = false @@ -384,14 +448,14 @@ reference = "tencent-mirrors" [[package]] name = "django-prometheus" -version = "1.0.15" +version = "2.1.0" description = "Django middlewares to monitor your application with Prometheus.io." category = "main" optional = false python-versions = "*" [package.dependencies] -prometheus-client = ">=0.0.21" +prometheus-client = ">=0.7" [package.source] type = "legacy" @@ -460,6 +524,25 @@ type = "legacy" url = "https://mirrors.tencent.com/pypi/simple" reference = "tencent-mirrors" +[[package]] +name = "googleapis-common-protos" +version = "1.56.3" +description = "Common protobufs used in Google APIs" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +protobuf = ">=3.15.0,<5.0.0dev" + +[package.extras] +grpc = ["grpcio (>=1.0.0,<2.0.0dev)"] + +[package.source] +type = "legacy" +url = "https://mirrors.tencent.com/pypi/simple" +reference = "tencent-mirrors" + [[package]] name = "greenlet" version = "1.1.2" @@ -476,6 +559,20 @@ type = "legacy" url = "https://mirrors.tencent.com/pypi/simple" reference = "tencent-mirrors" +[[package]] +name = "grpcio" +version = "1.48.0" +description = "HTTP/2-based RPC framework" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +six = ">=1.5.2" + +[package.extras] +protobuf = ["grpcio-tools (>=1.48.0)"] + [[package]] name = "gunicorn" version = "19.9.0" @@ -507,6 +604,25 @@ type = "legacy" url = "https://mirrors.tencent.com/pypi/simple" reference = "tencent-mirrors" +[[package]] +name = "immutables" +version = "0.18" +description = "Immutable Collections" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +typing-extensions = {version = ">=3.7.4.3", markers = "python_version < \"3.8\""} + +[package.extras] +test = ["flake8 (>=3.8.4,<3.9.0)", "pycodestyle (>=2.6.0,<2.7.0)", "mypy (==0.942)", "pytest (>=6.2.4,<6.3.0)"] + +[package.source] +type = "legacy" +url = "https://mirrors.tencent.com/pypi/simple" +reference = "tencent-mirrors" + [[package]] name = "importlib-metadata" version = "4.2.0" @@ -678,6 +794,372 @@ type = "legacy" url = "https://mirrors.tencent.com/pypi/simple" reference = "tencent-mirrors" +[[package]] +name = "opentelemetry-api" +version = "1.7.1" +description = "OpenTelemetry Python API" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +aiocontextvars = {version = "*", markers = "python_version < \"3.7\""} +Deprecated = ">=1.2.6" + +[package.source] +type = "legacy" +url = "https://mirrors.tencent.com/pypi/simple" +reference = "tencent-mirrors" + +[[package]] +name = "opentelemetry-exporter-jaeger" +version = "1.7.1" +description = "Jaeger Exporters for OpenTelemetry" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +opentelemetry-exporter-jaeger-proto-grpc = "1.7.1" +opentelemetry-exporter-jaeger-thrift = "1.7.1" + +[package.source] +type = "legacy" +url = "https://mirrors.tencent.com/pypi/simple" +reference = "tencent-mirrors" + +[[package]] +name = "opentelemetry-exporter-jaeger-proto-grpc" +version = "1.7.1" +description = "Jaeger Protobuf Exporter for OpenTelemetry" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +googleapis-common-protos = ">=1.52,<2.0" +grpcio = ">=1.0.0,<2.0.0" +opentelemetry-api = ">=1.3,<2.0" +opentelemetry-sdk = ">=1.3,<2.0" + +[package.source] +type = "legacy" +url = "https://mirrors.tencent.com/pypi/simple" +reference = "tencent-mirrors" + +[[package]] +name = "opentelemetry-exporter-jaeger-thrift" +version = "1.7.1" +description = "Jaeger Thrift Exporter for OpenTelemetry" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +opentelemetry-api = ">=1.3,<2.0" +opentelemetry-sdk = ">=1.3,<2.0" +thrift = ">=0.10.0" + +[package.source] +type = "legacy" +url = "https://mirrors.tencent.com/pypi/simple" +reference = "tencent-mirrors" + +[[package]] +name = "opentelemetry-exporter-otlp" +version = "1.7.1" +description = "OpenTelemetry Collector Exporters" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +opentelemetry-exporter-otlp-proto-grpc = "1.7.1" +opentelemetry-exporter-otlp-proto-http = "1.7.1" + +[package.source] +type = "legacy" +url = "https://mirrors.tencent.com/pypi/simple" +reference = "tencent-mirrors" + +[[package]] +name = "opentelemetry-exporter-otlp-proto-grpc" +version = "1.7.1" +description = "OpenTelemetry Collector Protobuf over gRPC Exporter" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +backoff = ">=1.10.0,<1.11.0" +googleapis-common-protos = ">=1.52,<2.0" +grpcio = ">=1.0.0,<2.0.0" +opentelemetry-api = ">=1.3,<2.0" +opentelemetry-proto = "1.7.1" +opentelemetry-sdk = ">=1.3,<2.0" + +[package.extras] +test = ["pytest-grpc"] + +[package.source] +type = "legacy" +url = "https://mirrors.tencent.com/pypi/simple" +reference = "tencent-mirrors" + +[[package]] +name = "opentelemetry-exporter-otlp-proto-http" +version = "1.7.1" +description = "OpenTelemetry Collector Protobuf over HTTP Exporter" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +backoff = ">=1.10.0,<1.11.0" +googleapis-common-protos = ">=1.52,<2.0" +opentelemetry-api = ">=1.3,<2.0" +opentelemetry-proto = "1.7.1" +opentelemetry-sdk = ">=1.3,<2.0" +requests = ">=2.7,<3.0" + +[package.source] +type = "legacy" +url = "https://mirrors.tencent.com/pypi/simple" +reference = "tencent-mirrors" + +[[package]] +name = "opentelemetry-instrumentation" +version = "0.26b1" +description = "Instrumentation Tools & Auto Instrumentation for OpenTelemetry Python" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +opentelemetry-api = ">=1.4,<2.0" +wrapt = ">=1.0.0,<2.0.0" + +[package.source] +type = "legacy" +url = "https://mirrors.tencent.com/pypi/simple" +reference = "tencent-mirrors" + +[[package]] +name = "opentelemetry-instrumentation-celery" +version = "0.26b1" +description = "OpenTelemetry Celery Instrumentation" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +opentelemetry-api = ">=1.3,<2.0" +opentelemetry-instrumentation = "0.26b1" +opentelemetry-semantic-conventions = "0.26b1" + +[package.extras] +instruments = ["celery (>=4.0,<6.0)"] +test = ["pytest", "opentelemetry-test-utils (==0.26b1)", "celery (>=4.0,<6.0)"] + +[package.source] +type = "legacy" +url = "https://mirrors.tencent.com/pypi/simple" +reference = "tencent-mirrors" + +[[package]] +name = "opentelemetry-instrumentation-dbapi" +version = "0.26b1" +description = "OpenTelemetry Database API instrumentation" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +opentelemetry-api = ">=1.3,<2.0" +opentelemetry-instrumentation = "0.26b1" +opentelemetry-semantic-conventions = "0.26b1" +wrapt = ">=1.0.0,<2.0.0" + +[package.extras] +test = ["opentelemetry-test-utils (==0.26b1)"] + +[package.source] +type = "legacy" +url = "https://mirrors.tencent.com/pypi/simple" +reference = "tencent-mirrors" + +[[package]] +name = "opentelemetry-instrumentation-django" +version = "0.26b1" +description = "OpenTelemetry Instrumentation for Django" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +opentelemetry-api = ">=1.3,<2.0" +opentelemetry-instrumentation = "0.26b1" +opentelemetry-instrumentation-wsgi = "0.26b1" +opentelemetry-semantic-conventions = "0.26b1" +opentelemetry-util-http = "0.26b1" + +[package.extras] +asgi = ["opentelemetry-instrumentation-asgi (==0.26b1)"] +instruments = ["django (>=1.10)"] +test = ["opentelemetry-test-utils (==0.26b1)", "django (>=1.10)"] + +[package.source] +type = "legacy" +url = "https://mirrors.tencent.com/pypi/simple" +reference = "tencent-mirrors" + +[[package]] +name = "opentelemetry-instrumentation-logging" +version = "0.26b1" +description = "OpenTelemetry Logging instrumentation" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +opentelemetry-api = ">=1.3,<2.0" +opentelemetry-instrumentation = "0.26b1" + +[package.extras] +test = ["opentelemetry-test-utils (==0.26b1)"] + +[package.source] +type = "legacy" +url = "https://mirrors.tencent.com/pypi/simple" +reference = "tencent-mirrors" + +[[package]] +name = "opentelemetry-instrumentation-redis" +version = "0.26b1" +description = "OpenTelemetry Redis instrumentation" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +opentelemetry-api = ">=1.3,<2.0" +opentelemetry-instrumentation = "0.26b1" +opentelemetry-semantic-conventions = "0.26b1" +wrapt = ">=1.12.1" + +[package.extras] +instruments = ["redis (>=2.6)"] +test = ["opentelemetry-test-utils (==0.26b1)", "opentelemetry-sdk (>=1.3,<2.0)", "redis (>=2.6)"] + +[package.source] +type = "legacy" +url = "https://mirrors.tencent.com/pypi/simple" +reference = "tencent-mirrors" + +[[package]] +name = "opentelemetry-instrumentation-requests" +version = "0.26b1" +description = "OpenTelemetry requests instrumentation" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +opentelemetry-api = ">=1.3,<2.0" +opentelemetry-instrumentation = "0.26b1" +opentelemetry-semantic-conventions = "0.26b1" +opentelemetry-util-http = "0.26b1" + +[package.extras] +instruments = ["requests (>=2.0,<3.0)"] +test = ["opentelemetry-test-utils (==0.26b1)", "httpretty (>=1.0,<2.0)", "requests (>=2.0,<3.0)"] + +[package.source] +type = "legacy" +url = "https://mirrors.tencent.com/pypi/simple" +reference = "tencent-mirrors" + +[[package]] +name = "opentelemetry-instrumentation-wsgi" +version = "0.26b1" +description = "WSGI Middleware for OpenTelemetry" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +opentelemetry-api = ">=1.3,<2.0" +opentelemetry-instrumentation = "0.26b1" +opentelemetry-semantic-conventions = "0.26b1" +opentelemetry-util-http = "0.26b1" + +[package.extras] +test = ["opentelemetry-test-utils (==0.26b1)"] + +[package.source] +type = "legacy" +url = "https://mirrors.tencent.com/pypi/simple" +reference = "tencent-mirrors" + +[[package]] +name = "opentelemetry-proto" +version = "1.7.1" +description = "OpenTelemetry Python Proto" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +protobuf = ">=3.13.0" + +[package.source] +type = "legacy" +url = "https://mirrors.tencent.com/pypi/simple" +reference = "tencent-mirrors" + +[[package]] +name = "opentelemetry-sdk" +version = "1.7.1" +description = "OpenTelemetry Python SDK" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +opentelemetry-api = "1.7.1" +opentelemetry-semantic-conventions = "0.26b1" + +[package.source] +type = "legacy" +url = "https://mirrors.tencent.com/pypi/simple" +reference = "tencent-mirrors" + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.26b1" +description = "OpenTelemetry Semantic Conventions" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.source] +type = "legacy" +url = "https://mirrors.tencent.com/pypi/simple" +reference = "tencent-mirrors" + +[[package]] +name = "opentelemetry-util-http" +version = "0.26b1" +description = "Web util for OpenTelemetry" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.source] +type = "legacy" +url = "https://mirrors.tencent.com/pypi/simple" +reference = "tencent-mirrors" + [[package]] name = "packaging" version = "21.3" @@ -821,6 +1303,19 @@ type = "legacy" url = "https://mirrors.tencent.com/pypi/simple" reference = "tencent-mirrors" +[[package]] +name = "protobuf" +version = "3.19.4" +description = "Protocol Buffers" +category = "main" +optional = false +python-versions = ">=3.5" + +[package.source] +type = "legacy" +url = "https://mirrors.tencent.com/pypi/simple" +reference = "tencent-mirrors" + [[package]] name = "ptyprocess" version = "0.7.0" @@ -1225,6 +1720,27 @@ type = "legacy" url = "https://mirrors.tencent.com/pypi/simple" reference = "tencent-mirrors" +[[package]] +name = "thrift" +version = "0.16.0" +description = "Python bindings for the Apache Thrift RPC system" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +six = ">=1.7.2" + +[package.extras] +all = ["tornado (>=4.0)", "twisted"] +tornado = ["tornado (>=4.0)"] +twisted = ["twisted"] + +[package.source] +type = "legacy" +url = "https://mirrors.tencent.com/pypi/simple" +reference = "tencent-mirrors" + [[package]] name = "toml" version = "0.10.2" @@ -1363,6 +1879,19 @@ type = "legacy" url = "https://mirrors.tencent.com/pypi/simple" reference = "tencent-mirrors" +[[package]] +name = "wrapt" +version = "1.14.1" +description = "Module for decorators, wrappers and monkey patching." +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" + +[package.source] +type = "legacy" +url = "https://mirrors.tencent.com/pypi/simple" +reference = "tencent-mirrors" + [[package]] name = "xlrd" version = "1.0.0" @@ -1444,9 +1973,13 @@ reference = "tencent-mirrors" [metadata] lock-version = "1.1" python-versions = "3.6.14" -content-hash = "bee2dfd1cd594de6e23e029362b86d1c05c7edbd6a64bc1af608092e456e9de5" +content-hash = "b3651ac572422ac5a03fda1aad26e9bb68f1b248c81bf27e05f6743bdeb38332" [metadata.files] +aiocontextvars = [ + {file = "aiocontextvars-0.2.2-py2.py3-none-any.whl", hash = "sha256:885daf8261818767d8f7cbd79f9d4482d118f024b6586ef6e67980236a27bfa3"}, + {file = "aiocontextvars-0.2.2.tar.gz", hash = "sha256:f027372dc48641f683c559f247bd84962becaacdc9ba711d583c3871fb5652aa"}, +] appnope = [ {file = "appnope-0.1.3-py2.py3-none-any.whl", hash = "sha256:265a455292d0bd8a72453494fa24df5a11eb18373a60c7c0430889f22548605e"}, {file = "appnope-0.1.3.tar.gz", hash = "sha256:02bd91c4de869fbb1e1c50aafc4098827a7a54ab2f39d9dcba6c9547ed920e24"}, @@ -1467,6 +2000,10 @@ backcall = [ {file = "backcall-0.2.0-py2.py3-none-any.whl", hash = "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"}, {file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"}, ] +backoff = [ + {file = "backoff-1.10.0-py2.py3-none-any.whl", hash = "sha256:5e73e2cbe780e1915a204799dba0a01896f45f4385e636bcca7a0614d879d0cd"}, + {file = "backoff-1.10.0.tar.gz", hash = "sha256:b8fba021fac74055ac05eb7c7bfce4723aedde6cd0a504e5326bcb0bdd6d19a4"}, +] black = [ {file = "black-21.12b0-py3-none-any.whl", hash = "sha256:a615e69ae185e08fdd73e4715e260e2479c861b5740057fde6e8b4e3b7dd589f"}, {file = "black-21.12b0.tar.gz", hash = "sha256:77b80f693a569e2e527958459634f18df9b0ba2625ba4e0c2d5da5be42e6f2b3"}, @@ -1547,6 +2084,9 @@ colorama = [ {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, ] +contextvars = [ + {file = "contextvars-2.4.tar.gz", hash = "sha256:f38c908aaa59c14335eeea12abea5f443646216c4e29380d7bf34d2018e2c39e"}, +] coverage = [ {file = "coverage-6.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6dbc1536e105adda7a6312c778f15aaabe583b0e9a0b0a324990334fd458c94b"}, {file = "coverage-6.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:174cf9b4bef0db2e8244f82059a5a72bd47e1d40e71c68ab055425172b16b7d0"}, @@ -1628,12 +2168,16 @@ decorator = [ {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"}, {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, ] +deprecated = [ + {file = "Deprecated-1.2.13-py2.py3-none-any.whl", hash = "sha256:64756e3e14c8c5eea9795d93c524551432a0be75629f8f29e67ab8caf076c76d"}, + {file = "Deprecated-1.2.13.tar.gz", hash = "sha256:43ac5335da90c31c24ba028af536a91d41d53f9e6901ddb021bcc572ce44e38d"}, +] dj-static = [ {file = "dj-static-0.0.6.tar.gz", hash = "sha256:032ec1c532617922e6e3e956d504a6fb1acce4fc1c7c94612d0fda21828ce8ef"}, ] django = [ - {file = "Django-3.2.13-py3-none-any.whl", hash = "sha256:b896ca61edc079eb6bbaa15cf6071eb69d6aac08cce5211583cfb41515644fdf"}, - {file = "Django-3.2.13.tar.gz", hash = "sha256:6d93497a0a9bf6ba0e0b1a29cccdc40efbfc76297255b1309b3a884a688ec4b6"}, + {file = "Django-3.2.15-py3-none-any.whl", hash = "sha256:115baf5049d5cf4163e43492cdc7139c306ed6d451e7d3571fe9612903903713"}, + {file = "Django-3.2.15.tar.gz", hash = "sha256:f71934b1a822f14a86c9ac9634053689279cd04ae69cb6ade4a59471b886582b"}, ] django-braces = [ {file = "django-braces-1.13.0.tar.gz", hash = "sha256:ba68e98b817c6f01d71d10849f359979617b3fe4cefb7f289adefddced092ddc"}, @@ -1648,8 +2192,8 @@ django-environ = [ {file = "django_environ-0.4.5-py2.py3-none-any.whl", hash = "sha256:c57b3c11ec1f319d9474e3e5a79134f40174b17c7cc024bbb2fad84646b120c4"}, ] django-prometheus = [ - {file = "django-prometheus-1.0.15.tar.gz", hash = "sha256:e8da2eb91cb20cfe0b8130305d34f3503766b7a08a244ce1031c36136beeb7a5"}, - {file = "django_prometheus-1.0.15-py3-none-any.whl", hash = "sha256:571d89a13e2547e1c3d19901f155bde8e101323fbcb0439363d670bc61c8c541"}, + {file = "django-prometheus-2.1.0.tar.gz", hash = "sha256:dd3f8da1399140fbef5c00d1526a23d1ade286b144281c325f8e409a781643f2"}, + {file = "django_prometheus-2.1.0-py2.py3-none-any.whl", hash = "sha256:c338d6efde1ca336e90c540b5e87afe9287d7bcc82d651a778f302b0be17a933"}, ] flake8 = [ {file = "flake8-4.0.1-py2.py3-none-any.whl", hash = "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d"}, @@ -1689,6 +2233,10 @@ gevent = [ {file = "gevent-21.1.2-pp27-pypy_73-win32.whl", hash = "sha256:a54b9c7516c211045d7897a73a4ccdc116b3720c9ad3c591ef9592b735202a3b"}, {file = "gevent-21.1.2.tar.gz", hash = "sha256:520cc2a029a9eef436e4e56b007af7859315cafa21937d43c1d5269f12f2c981"}, ] +googleapis-common-protos = [ + {file = "googleapis-common-protos-1.56.3.tar.gz", hash = "sha256:6f1369b58ed6cf3a4b7054a44ebe8d03b29c309257583a2bbdc064cd1e4a1442"}, + {file = "googleapis_common_protos-1.56.3-py2.py3-none-any.whl", hash = "sha256:87955d7b3a73e6e803f2572a33179de23989ebba725e05ea42f24838b792e461"}, +] greenlet = [ {file = "greenlet-1.1.2-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:58df5c2a0e293bf665a51f8a100d3e9956febfbf1d9aaf8c0677cf70218910c6"}, {file = "greenlet-1.1.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:aec52725173bd3a7b56fe91bc56eccb26fbdff1386ef123abb63c84c5b43b63a"}, @@ -1746,6 +2294,54 @@ greenlet = [ {file = "greenlet-1.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:013d61294b6cd8fe3242932c1c5e36e5d1db2c8afb58606c5a67efce62c1f5fd"}, {file = "greenlet-1.1.2.tar.gz", hash = "sha256:e30f5ea4ae2346e62cedde8794a56858a67b878dd79f7df76a0767e356b1744a"}, ] +grpcio = [ + {file = "grpcio-1.48.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:4a049a032144641ed5d073535c0dc69eb6029187cc729a66946c86dcc8eec3a1"}, + {file = "grpcio-1.48.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:f8bc76f5cd95f5476e5285fe5d3704a9332586a569fbbccef551b0b6f7a270f9"}, + {file = "grpcio-1.48.0-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:448d397fe88e9fef8170f019b86abdc4d554ae311aaf4dbff1532fde227d3308"}, + {file = "grpcio-1.48.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8f9b6b6f7c83869d2316c5d13f953381881a16741275a34ec5ed5762f11b206e"}, + {file = "grpcio-1.48.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5bd8541c4b6b43c9024496d30b4a12346325d3a17a1f3c80ad8924caed1e35c3"}, + {file = "grpcio-1.48.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:877d33aeba05ae0b9e81761a694914ed33613f655c35f6bbcf4ebbcb984e0167"}, + {file = "grpcio-1.48.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cd01a8201fd8ab2ce496f7e65975da1f1e629eac8eea84ead0fd77e32e4350cd"}, + {file = "grpcio-1.48.0-cp310-cp310-win32.whl", hash = "sha256:0388da923dff58ba7f711233e41c2b749b5817b8e0f137a107672d9c15a1009c"}, + {file = "grpcio-1.48.0-cp310-cp310-win_amd64.whl", hash = "sha256:8dcffdb8921fd88857ae350fd579277a5f9315351e89ed9094ef28927a46d40d"}, + {file = "grpcio-1.48.0-cp36-cp36m-linux_armv7l.whl", hash = "sha256:2138c50331232f56178c2b36dcfa6ad67aad705fe410955f3b2a53d722191b89"}, + {file = "grpcio-1.48.0-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:af2d80f142da2a6af45204a5ca2374e2747af07a99de54a1164111e169a761ff"}, + {file = "grpcio-1.48.0-cp36-cp36m-manylinux_2_17_aarch64.whl", hash = "sha256:59284bd4cdf47c147c26d91aca693765318d524328f6ece2a1a0b85a12a362af"}, + {file = "grpcio-1.48.0-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cc3ebfe356c0c6750379cd194bf2b7e5d1d2f29db1832358f05a73e9290db98c"}, + {file = "grpcio-1.48.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc2619a31339e1c53731f54761f1a2cb865d3421f690e00ef3e92f90d2a0c5ae"}, + {file = "grpcio-1.48.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:7df637405de328a54c1c8c08a3206f974c7a577730f90644af4c3400b7bfde2d"}, + {file = "grpcio-1.48.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:9e73b95969a579798bfbeb85d376695cce5172357fb52e450467ceb8e7365152"}, + {file = "grpcio-1.48.0-cp36-cp36m-win32.whl", hash = "sha256:059e9d58b5aba7fb9eabe3a4d2ac49e1dcbc2b54b0f166f6475e40b7f4435343"}, + {file = "grpcio-1.48.0-cp36-cp36m-win_amd64.whl", hash = "sha256:7cebcf645170f0c82ef71769544f9ac4515993a4d367f5900aba2eb4ecd2a32f"}, + {file = "grpcio-1.48.0-cp37-cp37m-linux_armv7l.whl", hash = "sha256:8af3a8845df35b838104d6fb1ae7f4969d248cf037fa2794916d31e917346f72"}, + {file = "grpcio-1.48.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:a1ef40975ec9ced6c17ce7fbec9825823da782fa606f0b92392646ff3886f198"}, + {file = "grpcio-1.48.0-cp37-cp37m-manylinux_2_17_aarch64.whl", hash = "sha256:7cccbf6db31f2a78e1909047ff69620f94a4e6e53251858e9502fbbff5714b48"}, + {file = "grpcio-1.48.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f3f142579f58def64c0850f0bb0eb1b425ae885f5669dda5b73ade64ad2b753"}, + {file = "grpcio-1.48.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:656c6f6f7b815bca3054780b8cdfa1e4e37cd36c887a48558d00c2cf85f31697"}, + {file = "grpcio-1.48.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:cba4538e8a2ef123ea570e7b1d62162e158963c2471e35d79eb9690c971a10c0"}, + {file = "grpcio-1.48.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:9daa67820fafceec6194ed1686c1783816e62d6756ff301ba93e682948836846"}, + {file = "grpcio-1.48.0-cp37-cp37m-win32.whl", hash = "sha256:7ec264a7fb413e0c804a7a48a6f7d7212742955a60724c44d793da35a8f30873"}, + {file = "grpcio-1.48.0-cp37-cp37m-win_amd64.whl", hash = "sha256:a2b1b33b92359388b8164807313dcbb3317101b038a5d54342982560329d958f"}, + {file = "grpcio-1.48.0-cp38-cp38-linux_armv7l.whl", hash = "sha256:7b820696a5ce7b98f459f234698cb323f89b355373789188efa126d7f47a2a92"}, + {file = "grpcio-1.48.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:e4dfae66ebc165c46c5b7048eb554472ee72fbaab2c2c2da7f9b1621c81e077c"}, + {file = "grpcio-1.48.0-cp38-cp38-manylinux_2_17_aarch64.whl", hash = "sha256:f7115038edce33b494e0138b0bd31a2eb6595d45e2eed23be46bc32886feb741"}, + {file = "grpcio-1.48.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b4e996282238943ca114628255be61980e38b25f73a08ae2ffd02b63eaf70d3a"}, + {file = "grpcio-1.48.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13dad31f5155fa555d393511cc8108c41b1b5b54dc4c24c27d4694ddd7a78fad"}, + {file = "grpcio-1.48.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:c84b9d90b2641963de98b35bb7a2a51f78119fe5bd00ef27246ba9f4f0835e36"}, + {file = "grpcio-1.48.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:41b65166779d7dafac4c98380ac19f690f1c5fe18083a71d370df87b24dd30ff"}, + {file = "grpcio-1.48.0-cp38-cp38-win32.whl", hash = "sha256:b890e5f5fbc21cb994894f73ecb2faaa66697d8debcb228a5adb0622b9bec3b2"}, + {file = "grpcio-1.48.0-cp38-cp38-win_amd64.whl", hash = "sha256:5fe3af539d2f50891ed93aed3064ffbcc38bf848aa3f7ed1fbedcce139c57302"}, + {file = "grpcio-1.48.0-cp39-cp39-linux_armv7l.whl", hash = "sha256:a4ed57f4e3d91259551e6765782b22d9e8b8178fec43ebf8e1b2c392c4ced37b"}, + {file = "grpcio-1.48.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:60843d8184e171886dd7a93d6672e2ef0b08dfd4f88da7421c10b46b6e031ac4"}, + {file = "grpcio-1.48.0-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:0ecba22f25ccde2442be7e7dd7fa746905d628f03312b4a0c9961f0d99771f53"}, + {file = "grpcio-1.48.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:34f5917f0c49a04633dc12d483c8aee6f6d9f69133b700214d3703f72a72f501"}, + {file = "grpcio-1.48.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f4c4ad8ad7e2cf3a272cbc96734d56635e6543939022f17e0c4487f7d2a45bf9"}, + {file = "grpcio-1.48.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:111fb2f5f4a069f331ae23106145fd16dd4e1112ca223858a922068614dac6d2"}, + {file = "grpcio-1.48.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:beb0573daa49889efcfea0a6e995b4f39d481aa1b94e1257617406ef417b56a6"}, + {file = "grpcio-1.48.0-cp39-cp39-win32.whl", hash = "sha256:ce70254a082cb767217b2fdee374cc79199d338d46140753438cd6d67c609b2f"}, + {file = "grpcio-1.48.0-cp39-cp39-win_amd64.whl", hash = "sha256:ae3fd135666448058fe277d93c10e0f18345fbcbb015c4642de2fa3db6f0c205"}, + {file = "grpcio-1.48.0.tar.gz", hash = "sha256:eaf4bb73819863440727195411ab3b5c304f6663625e66f348e91ebe0a039306"}, +] gunicorn = [ {file = "gunicorn-19.9.0-py2.py3-none-any.whl", hash = "sha256:aa8e0b40b4157b36a5df5e599f45c9c76d6af43845ba3b3b0efe2c70473c2471"}, {file = "gunicorn-19.9.0.tar.gz", hash = "sha256:fa2662097c66f920f53f70621c6c58ca4a3c4d3434205e608e121b5b3b71f4f3"}, @@ -1754,6 +2350,57 @@ idna = [ {file = "idna-2.8-py2.py3-none-any.whl", hash = "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"}, {file = "idna-2.8.tar.gz", hash = "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407"}, ] +immutables = [ + {file = "immutables-0.18-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d841dfa15b932bdad27f5149bce86b32d0dd8a29679ed61405677317b6893447"}, + {file = "immutables-0.18-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:29a5886845cd0ca8263b721337750a895e28feee2f16694a526977a791909db5"}, + {file = "immutables-0.18-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e979a9225507e3cd830ea73ac68b69fe82f495313a891485800daa5b6567e05"}, + {file = "immutables-0.18-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9949f704b80d0e601587d0a3b1a0cc6ff5d49528f6dfc1c8a1476b2137bb925e"}, + {file = "immutables-0.18-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b6c820c9bb5aac62b76de703384bb8bb706108be90c3def4a7f047f185a92bb"}, + {file = "immutables-0.18-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:03696193b276db3a9b619629685198886ddd7c4098c544bd8d0f87532c74120b"}, + {file = "immutables-0.18-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:798b4d6c388116effa7523591e4e39865292e4fa74e169b05a0759a16f604ce1"}, + {file = "immutables-0.18-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b3621256bc8058a7973f736b9e2c940e17133476265a0a83b8df8c0f446ca32f"}, + {file = "immutables-0.18-cp310-cp310-win32.whl", hash = "sha256:98f67bd36532582751dcc9021fdb60e7efc82e5717ae5927b84d0b86ea58fe12"}, + {file = "immutables-0.18-cp310-cp310-win_amd64.whl", hash = "sha256:69352b45a115808219feaf0bb7a551e9aa76c72684db93cd03f11474165f4569"}, + {file = "immutables-0.18-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:6ee2d6f8816fce53fa89b6a1ba2d4a96b344bf584d6ed0b10a871b17fff46e49"}, + {file = "immutables-0.18-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13159cedb698fdd243d9f2a7469c1628e075a180fc02f865dd98322b92a14aaf"}, + {file = "immutables-0.18-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9d72527fde329e3b566b67c954237be52b07d6e84ff23dcc1e94499755cacff6"}, + {file = "immutables-0.18-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53fccddd28cc3214aa48ca564702311c07eac069190dd890e097802c5d69b33a"}, + {file = "immutables-0.18-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:a29e3aa0fe05fb2cc6b31039f448aa6206d7f0cdb660c98aa9be6d12070d6840"}, + {file = "immutables-0.18-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:ffced8535cc673fcfb411d28ba5744689a6978fa596c803725a76f43c1bda911"}, + {file = "immutables-0.18-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:9f17407491164beb689d426f7985f79ae9dfa69868653cfbdb95645f6bf05cb0"}, + {file = "immutables-0.18-cp36-cp36m-win32.whl", hash = "sha256:74456c579cfd53f883cdcc0700e3871648a3316767efc1adf8c723ad3d8addec"}, + {file = "immutables-0.18-cp36-cp36m-win_amd64.whl", hash = "sha256:e4c2110173649acf67bd763bbd2a9c3a863a1d20fd7f3db3493ce4e0fb04fae5"}, + {file = "immutables-0.18-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2aa5292630b08c874972931bac06ee381cb6fb7382d7be1856234d7bd4a8e676"}, + {file = "immutables-0.18-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc830a689a55e404f0e23d7d69e01c218fa8a0be54a6ca5df45b6fbfeeac648a"}, + {file = "immutables-0.18-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5caf9c670e6851e7f310716c7dcdf8705236d13056eda1fab3deaad5d7198468"}, + {file = "immutables-0.18-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:853d63f4a07b2ea2131ba0831aeec11f6a6ee5e290e8f175bf56842762d7412e"}, + {file = "immutables-0.18-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:9a86dcca4bb406f80e7a18c233aec0e76a7530c456e24aa1e19a708a34f2aac1"}, + {file = "immutables-0.18-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:6baf4dc11ba0e9f41a6cbde7ecaa7af9cb482559b92ba3254e3e37a518b1970e"}, + {file = "immutables-0.18-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:734ec4467dd15f9135ca5ecccc91e796a67d27c227e81554f9e06b1bb3b28d6d"}, + {file = "immutables-0.18-cp37-cp37m-win32.whl", hash = "sha256:f6edb73619aa0a5fe4a77d97dd9d39bfeef61a5afe71aa5bdceccf59b933999e"}, + {file = "immutables-0.18-cp37-cp37m-win_amd64.whl", hash = "sha256:fade8ccf7afbc1e7ea353159fa90cc04395f2f4f57658160d7a02f6aa60c4e77"}, + {file = "immutables-0.18-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:8b650d779a46441dccd02e7ee8326dbd0dec633c6bd75e9fe13373a6b19570dd"}, + {file = "immutables-0.18-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1acbbc333f1643fd1ed21bcc3e09aad2ef6648478a0cae76a2ca5823764a7d3b"}, + {file = "immutables-0.18-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3bad4d43009fa61ea40d887e6fa89ae7c4e62dff5e4a878d60b76cf245720bb"}, + {file = "immutables-0.18-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e04b61ddffd4ccb4d7ab823b2e55dbb4ad47c37697e311fae4b98b3c023ab194"}, + {file = "immutables-0.18-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54577e46c5332d7390212040c084335b7d667504847ed2788428d44f20e595ce"}, + {file = "immutables-0.18-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:1330f96eb6a3a11f5d02f30b2c6393ef30d01a79f7144d63d2a3e6ff05cb99db"}, + {file = "immutables-0.18-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1d6821d7718cf9f4a7b1d9e765fc22a9d1ae0fad3fabd8724b4e614d2a6e0b54"}, + {file = "immutables-0.18-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:45bd862a5dfb952eaff4a9c2448712c5a550dd956575e23cbfc512010fb06c74"}, + {file = "immutables-0.18-cp38-cp38-win32.whl", hash = "sha256:989606e440492736112b471dcd80586e3d4a63bc6f8ff4f9d1d612e0f96cb683"}, + {file = "immutables-0.18-cp38-cp38-win_amd64.whl", hash = "sha256:ac9e05f846392e983fb59f74ed2334031b366251d16d24122e4c85f70fb6e2da"}, + {file = "immutables-0.18-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:de1a091ab89b7ba50501a915a0fbdceb52b079c752f4f7c76d2060237774a714"}, + {file = "immutables-0.18-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5d43b16b6adbe1327c6688e14b125cb3b940e748790b305de96c8d55668ac25f"}, + {file = "immutables-0.18-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f32b5933393e4cc204d8f9e7d9f503ec052e30f612090be0de0dd31b1464b35"}, + {file = "immutables-0.18-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:525fe9001b5a96c325eec41677efaeb8c3610776e834ce7f31fbe3d33cc05252"}, + {file = "immutables-0.18-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d11da4946e19f3b24a873b2ba2891cc226a89bb398561c62dfb966a9b6501a4a"}, + {file = "immutables-0.18-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:90da9dea0a1c0a907d511f124cd87fe090c0e30a951c3fe68bc9782ae4f2c77f"}, + {file = "immutables-0.18-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:77bdc96dc24e32839557cde3785f8039a369c95529ff9179044b81d0ba4bd02c"}, + {file = "immutables-0.18-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:210efea163a704597cfdb2d30713d3c0963c30f0d997539c9ab5da40e3d6a886"}, + {file = "immutables-0.18-cp39-cp39-win32.whl", hash = "sha256:535616ad7ca1174a27ade637192c970bfedb0b0e0467e69ce415b40d7cf7ba0c"}, + {file = "immutables-0.18-cp39-cp39-win_amd64.whl", hash = "sha256:1338aad6fd69f11442adcbb3402a028c90f6e945682ddb8aba462a3827f2d427"}, + {file = "immutables-0.18.tar.gz", hash = "sha256:5336c7974084cce62f7e29aaff81a3c3f75e0fd0a23a2faeb986ae0ea08d8cf4"}, +] importlib-metadata = [ {file = "importlib_metadata-4.2.0-py3-none-any.whl", hash = "sha256:057e92c15bc8d9e8109738a48db0ccb31b4d9d5cfbee5a8670879a30be66304b"}, {file = "importlib_metadata-4.2.0.tar.gz", hash = "sha256:b7e52a1f8dec14a75ea73e0891f3060099ca1d8e6a462a4dff11c3e119ea1b31"}, @@ -1811,6 +2458,82 @@ mypy-extensions = [ {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, ] +opentelemetry-api = [ + {file = "opentelemetry-api-1.7.1.tar.gz", hash = "sha256:aa4c29150042fd4e9efc30810bc5413a16a442b75fa16bef879651016ca8497e"}, + {file = "opentelemetry_api-1.7.1-py3-none-any.whl", hash = "sha256:01f3129ca2797a98c7a032e1fdf24650a1cab506666b33a5974618724e262c78"}, +] +opentelemetry-exporter-jaeger = [ + {file = "opentelemetry-exporter-jaeger-1.7.1.tar.gz", hash = "sha256:daf0f55d926754bad0bc20ddde4c56d0e9a66800613cd6bb15e409c660930c14"}, + {file = "opentelemetry_exporter_jaeger-1.7.1-py3-none-any.whl", hash = "sha256:976e5ef20fe81c5f81d3c398f3c7ddce65c5ead1a9810445b488532b29ab81dc"}, +] +opentelemetry-exporter-jaeger-proto-grpc = [ + {file = "opentelemetry-exporter-jaeger-proto-grpc-1.7.1.tar.gz", hash = "sha256:284f00fc53de0ca48b4805548fb22506fdf48e317853e80eac8f2173d35dfd68"}, + {file = "opentelemetry_exporter_jaeger_proto_grpc-1.7.1-py3-none-any.whl", hash = "sha256:62fcf7a465db407d8e9beca92aa5a921cda93eee2787341dbf6181153f99a2ee"}, +] +opentelemetry-exporter-jaeger-thrift = [ + {file = "opentelemetry-exporter-jaeger-thrift-1.7.1.tar.gz", hash = "sha256:884e3f67fdd922a30337614d2093ce21031b71bac23f73de1ce178349b542aa5"}, + {file = "opentelemetry_exporter_jaeger_thrift-1.7.1-py3-none-any.whl", hash = "sha256:c0607493252fe92443599198d01ee976660954c75f856a1fe06a762848175c29"}, +] +opentelemetry-exporter-otlp = [ + {file = "opentelemetry-exporter-otlp-1.7.1.tar.gz", hash = "sha256:f369b9c9b0f64c5967c553430fc1b1acab9606a3d6c3ba8907a1a0b56f1767da"}, + {file = "opentelemetry_exporter_otlp-1.7.1-py3-none-any.whl", hash = "sha256:9ea18fdd32dcfe872c7a0c50ad58833d61d2b0b7595ee3366bc6aff77054bfc5"}, +] +opentelemetry-exporter-otlp-proto-grpc = [ + {file = "opentelemetry-exporter-otlp-proto-grpc-1.7.1.tar.gz", hash = "sha256:cf8c8cdeaea9c3438a1faff94b00cdac2dfcb672a0081c4334ffe6bdb5a40b55"}, + {file = "opentelemetry_exporter_otlp_proto_grpc-1.7.1-py3-none-any.whl", hash = "sha256:272f80b7bd5ab925dc49cfe8427e835911e62ab793b703ba0817686e50532046"}, +] +opentelemetry-exporter-otlp-proto-http = [ + {file = "opentelemetry-exporter-otlp-proto-http-1.7.1.tar.gz", hash = "sha256:f79d6dddfed6f454975311fa11d819f1f418557a7546d87ec9d3b184adcf9804"}, + {file = "opentelemetry_exporter_otlp_proto_http-1.7.1-py3-none-any.whl", hash = "sha256:c167fd418032994009c241189e18dab8abe1246b4ff2b98f7f16acaba6afdf33"}, +] +opentelemetry-instrumentation = [ + {file = "opentelemetry-instrumentation-0.26b1.tar.gz", hash = "sha256:c8f60bbe8a803f2489627fdd66726891de64347e8828c259d3c607a9c97d20c0"}, + {file = "opentelemetry_instrumentation-0.26b1-py3-none-any.whl", hash = "sha256:6f84d5496c59b449c282258fe834e5f3cd2cc1887ba39933607f5a710e3ee657"}, +] +opentelemetry-instrumentation-celery = [ + {file = "opentelemetry-instrumentation-celery-0.26b1.tar.gz", hash = "sha256:35bf7783072aa6c939630d3c89b2c0a92ef5f7d4a3666a3048c6bdcc9c4a0997"}, + {file = "opentelemetry_instrumentation_celery-0.26b1-py3-none-any.whl", hash = "sha256:a66b346a7e4ad1dfd91b056d23395995e466216e66f7aa32f8e7ecbd569f4dcb"}, +] +opentelemetry-instrumentation-dbapi = [ + {file = "opentelemetry-instrumentation-dbapi-0.26b1.tar.gz", hash = "sha256:f0083d7b12b2ab70ca2b3071d1b4a8722969ec63c0c3abb5cd5998f9c1df1377"}, + {file = "opentelemetry_instrumentation_dbapi-0.26b1-py3-none-any.whl", hash = "sha256:743f012d0cfe1c50850994ed959c44e73c2b491575ab12ddb9224f99eed153d9"}, +] +opentelemetry-instrumentation-django = [ + {file = "opentelemetry-instrumentation-django-0.26b1.tar.gz", hash = "sha256:d4ddc6ce7d67286146f940ec4f7bea3af1169dca8a227aeac0af3e0e8ed8ed71"}, + {file = "opentelemetry_instrumentation_django-0.26b1-py3-none-any.whl", hash = "sha256:9a5898abcd1cb037306f816013257757bf97cd893508b089d2c11e2c5f06f6e3"}, +] +opentelemetry-instrumentation-logging = [ + {file = "opentelemetry-instrumentation-logging-0.26b1.tar.gz", hash = "sha256:daec6f023a487020f5516373daea551454685f1e1f99fcd39c3336a1418af182"}, + {file = "opentelemetry_instrumentation_logging-0.26b1-py3-none-any.whl", hash = "sha256:d3e7a87183960dc86c8dd0b9906567a42f99a376b9895a4fd4719317e256dc87"}, +] +opentelemetry-instrumentation-redis = [ + {file = "opentelemetry-instrumentation-redis-0.26b1.tar.gz", hash = "sha256:39be460a7a259e5daf73071c651c7acc93cd4b8ac25433328427617e7dd3827d"}, + {file = "opentelemetry_instrumentation_redis-0.26b1-py3-none-any.whl", hash = "sha256:969c881f2a9d6c5c8f0136e75bf61f5533c2006ffb07dc6e35b5620b06802162"}, +] +opentelemetry-instrumentation-requests = [ + {file = "opentelemetry-instrumentation-requests-0.26b1.tar.gz", hash = "sha256:44e00c74b861b4ba4e741cebaf228d0c5626fb846b69f66344c402afa145c3a5"}, + {file = "opentelemetry_instrumentation_requests-0.26b1-py3-none-any.whl", hash = "sha256:0ccd9dfe13b5261161a5d13343b52692f6e279a34909999cb72b3c9b28d4233c"}, +] +opentelemetry-instrumentation-wsgi = [ + {file = "opentelemetry-instrumentation-wsgi-0.26b1.tar.gz", hash = "sha256:43ab3d78487f82a8a0f9c2be3d21b5920621712027e3fea756ba05039a6a3521"}, + {file = "opentelemetry_instrumentation_wsgi-0.26b1-py3-none-any.whl", hash = "sha256:19d2fd538e15b16e985946074631cd6853d67a46f8304bb5da2ac3163c5803c9"}, +] +opentelemetry-proto = [ + {file = "opentelemetry-proto-1.7.1.tar.gz", hash = "sha256:fadb617947e2567740f8e92ae8f4564c5d8bfb2816dd34b19ca3efcf5eaa0806"}, + {file = "opentelemetry_proto-1.7.1-py3-none-any.whl", hash = "sha256:ec0737f0277dfe3f1eac4cb0db277278573f7b071cf3389f1d9d01e3d3cfb465"}, +] +opentelemetry-sdk = [ + {file = "opentelemetry-sdk-1.7.1.tar.gz", hash = "sha256:80f532dd5b293e80e563312977434faac59a9931fdd44761f6b34d3578f796ff"}, + {file = "opentelemetry_sdk-1.7.1-py3-none-any.whl", hash = "sha256:3344ec6e0fef7aaef034cfe284fb0b75615d40fa988f81caba7aa9c1e7e28cb6"}, +] +opentelemetry-semantic-conventions = [ + {file = "opentelemetry-semantic-conventions-0.26b1.tar.gz", hash = "sha256:edce22d1c320f896cccb6994f8467594a7cdc47a84156bc34485f2f0e5adce8f"}, + {file = "opentelemetry_semantic_conventions-0.26b1-py3-none-any.whl", hash = "sha256:cba9799d26c8183f869c84cff1f217bb712c9306d05dc346f50032916e8d7065"}, +] +opentelemetry-util-http = [ + {file = "opentelemetry-util-http-0.26b1.tar.gz", hash = "sha256:76bfa0e9defba9563630fb836546a321ec4287f42a81ae2040b49b4d134e7f84"}, + {file = "opentelemetry_util_http-0.26b1-py3-none-any.whl", hash = "sha256:dbbc31bd5f53786f01c4c229868d0c93b807acf71e59c251c9b0c670d30ba130"}, +] packaging = [ {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, @@ -1847,6 +2570,34 @@ prompt-toolkit = [ {file = "prompt_toolkit-3.0.29-py3-none-any.whl", hash = "sha256:62291dad495e665fca0bda814e342c69952086afb0f4094d0893d357e5c78752"}, {file = "prompt_toolkit-3.0.29.tar.gz", hash = "sha256:bd640f60e8cecd74f0dc249713d433ace2ddc62b65ee07f96d358e0b152b6ea7"}, ] +protobuf = [ + {file = "protobuf-3.19.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f51d5a9f137f7a2cec2d326a74b6e3fc79d635d69ffe1b036d39fc7d75430d37"}, + {file = "protobuf-3.19.4-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:09297b7972da685ce269ec52af761743714996b4381c085205914c41fcab59fb"}, + {file = "protobuf-3.19.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:072fbc78d705d3edc7ccac58a62c4c8e0cec856987da7df8aca86e647be4e35c"}, + {file = "protobuf-3.19.4-cp310-cp310-win32.whl", hash = "sha256:7bb03bc2873a2842e5ebb4801f5c7ff1bfbdf426f85d0172f7644fcda0671ae0"}, + {file = "protobuf-3.19.4-cp310-cp310-win_amd64.whl", hash = "sha256:f358aa33e03b7a84e0d91270a4d4d8f5df6921abe99a377828839e8ed0c04e07"}, + {file = "protobuf-3.19.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:1c91ef4110fdd2c590effb5dca8fdbdcb3bf563eece99287019c4204f53d81a4"}, + {file = "protobuf-3.19.4-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c438268eebb8cf039552897d78f402d734a404f1360592fef55297285f7f953f"}, + {file = "protobuf-3.19.4-cp36-cp36m-win32.whl", hash = "sha256:835a9c949dc193953c319603b2961c5c8f4327957fe23d914ca80d982665e8ee"}, + {file = "protobuf-3.19.4-cp36-cp36m-win_amd64.whl", hash = "sha256:4276cdec4447bd5015453e41bdc0c0c1234eda08420b7c9a18b8d647add51e4b"}, + {file = "protobuf-3.19.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6cbc312be5e71869d9d5ea25147cdf652a6781cf4d906497ca7690b7b9b5df13"}, + {file = "protobuf-3.19.4-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:54a1473077f3b616779ce31f477351a45b4fef8c9fd7892d6d87e287a38df368"}, + {file = "protobuf-3.19.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:435bb78b37fc386f9275a7035fe4fb1364484e38980d0dd91bc834a02c5ec909"}, + {file = "protobuf-3.19.4-cp37-cp37m-win32.whl", hash = "sha256:16f519de1313f1b7139ad70772e7db515b1420d208cb16c6d7858ea989fc64a9"}, + {file = "protobuf-3.19.4-cp37-cp37m-win_amd64.whl", hash = "sha256:cdc076c03381f5c1d9bb1abdcc5503d9ca8b53cf0a9d31a9f6754ec9e6c8af0f"}, + {file = "protobuf-3.19.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:69da7d39e39942bd52848438462674c463e23963a1fdaa84d88df7fbd7e749b2"}, + {file = "protobuf-3.19.4-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:48ed3877fa43e22bcacc852ca76d4775741f9709dd9575881a373bd3e85e54b2"}, + {file = "protobuf-3.19.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd95d1dfb9c4f4563e6093a9aa19d9c186bf98fa54da5252531cc0d3a07977e7"}, + {file = "protobuf-3.19.4-cp38-cp38-win32.whl", hash = "sha256:b38057450a0c566cbd04890a40edf916db890f2818e8682221611d78dc32ae26"}, + {file = "protobuf-3.19.4-cp38-cp38-win_amd64.whl", hash = "sha256:7ca7da9c339ca8890d66958f5462beabd611eca6c958691a8fe6eccbd1eb0c6e"}, + {file = "protobuf-3.19.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:36cecbabbda242915529b8ff364f2263cd4de7c46bbe361418b5ed859677ba58"}, + {file = "protobuf-3.19.4-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:c1068287025f8ea025103e37d62ffd63fec8e9e636246b89c341aeda8a67c934"}, + {file = "protobuf-3.19.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96bd766831596d6014ca88d86dc8fe0fb2e428c0b02432fd9db3943202bf8c5e"}, + {file = "protobuf-3.19.4-cp39-cp39-win32.whl", hash = "sha256:84123274d982b9e248a143dadd1b9815049f4477dc783bf84efe6250eb4b836a"}, + {file = "protobuf-3.19.4-cp39-cp39-win_amd64.whl", hash = "sha256:3112b58aac3bac9c8be2b60a9daf6b558ca3f7681c130dcdd788ade7c9ffbdca"}, + {file = "protobuf-3.19.4-py2.py3-none-any.whl", hash = "sha256:8961c3a78ebfcd000920c9060a262f082f29838682b1f7201889300c1fbe0616"}, + {file = "protobuf-3.19.4.tar.gz", hash = "sha256:9df0c10adf3e83015ced42a9a7bd64e13d06c4cf45c340d2c63020ea04499d0a"}, +] ptyprocess = [ {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, @@ -1977,6 +2728,9 @@ sqlparse = [ static3 = [ {file = "static3-0.7.0.tar.gz", hash = "sha256:674641c64bc75507af2eb20bef7e7e3593dca993dec6674be108fa15b42f47c8"}, ] +thrift = [ + {file = "thrift-0.16.0.tar.gz", hash = "sha256:2b5b6488fcded21f9d312aa23c9ff6a0195d0f6ae26ddbd5ad9e3e25dfc14408"}, +] toml = [ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, @@ -2056,6 +2810,72 @@ werkzeug = [ {file = "Werkzeug-2.0.3-py3-none-any.whl", hash = "sha256:1421ebfc7648a39a5c58c601b154165d05cf47a3cd0ccb70857cbdacf6c8f2b8"}, {file = "Werkzeug-2.0.3.tar.gz", hash = "sha256:b863f8ff057c522164b6067c9e28b041161b4be5ba4d0daceeaa50a163822d3c"}, ] +wrapt = [ + {file = "wrapt-1.14.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:1b376b3f4896e7930f1f772ac4b064ac12598d1c38d04907e696cc4d794b43d3"}, + {file = "wrapt-1.14.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:903500616422a40a98a5a3c4ff4ed9d0066f3b4c951fa286018ecdf0750194ef"}, + {file = "wrapt-1.14.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5a9a0d155deafd9448baff28c08e150d9b24ff010e899311ddd63c45c2445e28"}, + {file = "wrapt-1.14.1-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:ddaea91abf8b0d13443f6dac52e89051a5063c7d014710dcb4d4abb2ff811a59"}, + {file = "wrapt-1.14.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:36f582d0c6bc99d5f39cd3ac2a9062e57f3cf606ade29a0a0d6b323462f4dd87"}, + {file = "wrapt-1.14.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:7ef58fb89674095bfc57c4069e95d7a31cfdc0939e2a579882ac7d55aadfd2a1"}, + {file = "wrapt-1.14.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:e2f83e18fe2f4c9e7db597e988f72712c0c3676d337d8b101f6758107c42425b"}, + {file = "wrapt-1.14.1-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:ee2b1b1769f6707a8a445162ea16dddf74285c3964f605877a20e38545c3c462"}, + {file = "wrapt-1.14.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:833b58d5d0b7e5b9832869f039203389ac7cbf01765639c7309fd50ef619e0b1"}, + {file = "wrapt-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:80bb5c256f1415f747011dc3604b59bc1f91c6e7150bd7db03b19170ee06b320"}, + {file = "wrapt-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:07f7a7d0f388028b2df1d916e94bbb40624c59b48ecc6cbc232546706fac74c2"}, + {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02b41b633c6261feff8ddd8d11c711df6842aba629fdd3da10249a53211a72c4"}, + {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fe803deacd09a233e4762a1adcea5db5d31e6be577a43352936179d14d90069"}, + {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:257fd78c513e0fb5cdbe058c27a0624c9884e735bbd131935fd49e9fe719d310"}, + {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4fcc4649dc762cddacd193e6b55bc02edca674067f5f98166d7713b193932b7f"}, + {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:11871514607b15cfeb87c547a49bca19fde402f32e2b1c24a632506c0a756656"}, + {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8ad85f7f4e20964db4daadcab70b47ab05c7c1cf2a7c1e51087bfaa83831854c"}, + {file = "wrapt-1.14.1-cp310-cp310-win32.whl", hash = "sha256:a9a52172be0b5aae932bef82a79ec0a0ce87288c7d132946d645eba03f0ad8a8"}, + {file = "wrapt-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:6d323e1554b3d22cfc03cd3243b5bb815a51f5249fdcbb86fda4bf62bab9e164"}, + {file = "wrapt-1.14.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:43ca3bbbe97af00f49efb06e352eae40434ca9d915906f77def219b88e85d907"}, + {file = "wrapt-1.14.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:6b1a564e6cb69922c7fe3a678b9f9a3c54e72b469875aa8018f18b4d1dd1adf3"}, + {file = "wrapt-1.14.1-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:00b6d4ea20a906c0ca56d84f93065b398ab74b927a7a3dbd470f6fc503f95dc3"}, + {file = "wrapt-1.14.1-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:a85d2b46be66a71bedde836d9e41859879cc54a2a04fad1191eb50c2066f6e9d"}, + {file = "wrapt-1.14.1-cp35-cp35m-win32.whl", hash = "sha256:dbcda74c67263139358f4d188ae5faae95c30929281bc6866d00573783c422b7"}, + {file = "wrapt-1.14.1-cp35-cp35m-win_amd64.whl", hash = "sha256:b21bb4c09ffabfa0e85e3a6b623e19b80e7acd709b9f91452b8297ace2a8ab00"}, + {file = "wrapt-1.14.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9e0fd32e0148dd5dea6af5fee42beb949098564cc23211a88d799e434255a1f4"}, + {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9736af4641846491aedb3c3f56b9bc5568d92b0692303b5a305301a95dfd38b1"}, + {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b02d65b9ccf0ef6c34cba6cf5bf2aab1bb2f49c6090bafeecc9cd81ad4ea1c1"}, + {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21ac0156c4b089b330b7666db40feee30a5d52634cc4560e1905d6529a3897ff"}, + {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:9f3e6f9e05148ff90002b884fbc2a86bd303ae847e472f44ecc06c2cd2fcdb2d"}, + {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:6e743de5e9c3d1b7185870f480587b75b1cb604832e380d64f9504a0535912d1"}, + {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:d79d7d5dc8a32b7093e81e97dad755127ff77bcc899e845f41bf71747af0c569"}, + {file = "wrapt-1.14.1-cp36-cp36m-win32.whl", hash = "sha256:81b19725065dcb43df02b37e03278c011a09e49757287dca60c5aecdd5a0b8ed"}, + {file = "wrapt-1.14.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b014c23646a467558be7da3d6b9fa409b2c567d2110599b7cf9a0c5992b3b471"}, + {file = "wrapt-1.14.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:88bd7b6bd70a5b6803c1abf6bca012f7ed963e58c68d76ee20b9d751c74a3248"}, + {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5901a312f4d14c59918c221323068fad0540e34324925c8475263841dbdfe68"}, + {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d77c85fedff92cf788face9bfa3ebaa364448ebb1d765302e9af11bf449ca36d"}, + {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d649d616e5c6a678b26d15ece345354f7c2286acd6db868e65fcc5ff7c24a77"}, + {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7d2872609603cb35ca513d7404a94d6d608fc13211563571117046c9d2bcc3d7"}, + {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:ee6acae74a2b91865910eef5e7de37dc6895ad96fa23603d1d27ea69df545015"}, + {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2b39d38039a1fdad98c87279b48bc5dce2c0ca0d73483b12cb72aa9609278e8a"}, + {file = "wrapt-1.14.1-cp37-cp37m-win32.whl", hash = "sha256:60db23fa423575eeb65ea430cee741acb7c26a1365d103f7b0f6ec412b893853"}, + {file = "wrapt-1.14.1-cp37-cp37m-win_amd64.whl", hash = "sha256:709fe01086a55cf79d20f741f39325018f4df051ef39fe921b1ebe780a66184c"}, + {file = "wrapt-1.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8c0ce1e99116d5ab21355d8ebe53d9460366704ea38ae4d9f6933188f327b456"}, + {file = "wrapt-1.14.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e3fb1677c720409d5f671e39bac6c9e0e422584e5f518bfd50aa4cbbea02433f"}, + {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:642c2e7a804fcf18c222e1060df25fc210b9c58db7c91416fb055897fc27e8cc"}, + {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b7c050ae976e286906dd3f26009e117eb000fb2cf3533398c5ad9ccc86867b1"}, + {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef3f72c9666bba2bab70d2a8b79f2c6d2c1a42a7f7e2b0ec83bb2f9e383950af"}, + {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:01c205616a89d09827986bc4e859bcabd64f5a0662a7fe95e0d359424e0e071b"}, + {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5a0f54ce2c092aaf439813735584b9537cad479575a09892b8352fea5e988dc0"}, + {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2cf71233a0ed05ccdabe209c606fe0bac7379fdcf687f39b944420d2a09fdb57"}, + {file = "wrapt-1.14.1-cp38-cp38-win32.whl", hash = "sha256:aa31fdcc33fef9eb2552cbcbfee7773d5a6792c137b359e82879c101e98584c5"}, + {file = "wrapt-1.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:d1967f46ea8f2db647c786e78d8cc7e4313dbd1b0aca360592d8027b8508e24d"}, + {file = "wrapt-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3232822c7d98d23895ccc443bbdf57c7412c5a65996c30442ebe6ed3df335383"}, + {file = "wrapt-1.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:988635d122aaf2bdcef9e795435662bcd65b02f4f4c1ae37fbee7401c440b3a7"}, + {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cca3c2cdadb362116235fdbd411735de4328c61425b0aa9f872fd76d02c4e86"}, + {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d52a25136894c63de15a35bc0bdc5adb4b0e173b9c0d07a2be9d3ca64a332735"}, + {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40e7bc81c9e2b2734ea4bc1aceb8a8f0ceaac7c5299bc5d69e37c44d9081d43b"}, + {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b9b7a708dd92306328117d8c4b62e2194d00c365f18eff11a9b53c6f923b01e3"}, + {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6a9a25751acb379b466ff6be78a315e2b439d4c94c1e99cb7266d40a537995d3"}, + {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:34aa51c45f28ba7f12accd624225e2b1e5a3a45206aa191f6f9aac931d9d56fe"}, + {file = "wrapt-1.14.1-cp39-cp39-win32.whl", hash = "sha256:dee0ce50c6a2dd9056c20db781e9c1cfd33e77d2d569f5d1d9321c641bb903d5"}, + {file = "wrapt-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:dee60e1de1898bde3b238f18340eec6148986da0455d8ba7848d50470a7a32fb"}, + {file = "wrapt-1.14.1.tar.gz", hash = "sha256:380a85cf89e0e69b7cfbe2ea9f765f004ff419f34194018a6827ac0e3edfed4d"}, +] xlrd = [ {file = "xlrd-1.0.0-py3-none-any.whl", hash = "sha256:05f55eb39a68f1c3d336de186aeb90c98ea5add722813738d8ae8b97819c5924"}, {file = "xlrd-1.0.0.tar.gz", hash = "sha256:0ff87dd5d50425084f7219cb6f86bb3eb5aa29063f53d50bf270ed007e941069"}, diff --git a/src/login/pyproject.toml b/src/login/pyproject.toml index 5c8d6bb81..497aa5169 100644 --- a/src/login/pyproject.toml +++ b/src/login/pyproject.toml @@ -6,7 +6,7 @@ authors = ["IMBlues "] [tool.poetry.dependencies] python = "3.6.14" -django = "3.2.13" +Django = "3.2.15" django-braces = "1.13.0" dj-static = "0.0.6" pycrypto = "2.6.1" @@ -20,12 +20,22 @@ pytz = "2016.6.1" python-dateutil = "2.8.1" cachetools = "3.1.1" django-environ = "0.4.5" -django-prometheus = "1.0.15" +django-prometheus = "2.1.0" blue-krill = "^1.0.7" python-json-logger = "^2.0.2" sentry-sdk = "1.5.6" django-decorator-include = "^3.0" werkzeug = "2.0.3" +opentelemetry-api = "1.7.1" +opentelemetry-sdk = "1.7.1" +opentelemetry-exporter-otlp = "1.7.1" +opentelemetry-instrumentation-django = "0.26b1" +opentelemetry-instrumentation-dbapi = "0.26b1" +opentelemetry-instrumentation-redis = "0.26b1" +opentelemetry-instrumentation-requests = "0.26b1" +opentelemetry-instrumentation-celery = "0.26b1" +opentelemetry-instrumentation-logging = "0.26b1" +opentelemetry-exporter-jaeger = "1.7.1" [tool.poetry.dev-dependencies] ipython = "^7.15.0" diff --git a/src/login/static/css_ce/login.css b/src/login/static/css_ce/login.css index f14c2b5b2..c74a2aacb 100755 --- a/src/login/static/css_ce/login.css +++ b/src/login/static/css_ce/login.css @@ -216,6 +216,49 @@ input[type="number"]::-webkit-outer-spin-button { border-radius: 8px; box-shadow: 0px 2px 6px 0px rgba(0,0,0,0.10); } +.page-content .login-from .account-content { + position: relative; +} +.page-content .login-from .account-content .error-text { + margin: 10px 0; + color: #ea3636; +} +.page-content .login-from .account-content .error-text #admin { + color: #5C7AC6; +} +.page-content .login-from .account-content .error-text #admin:hover { + cursor: pointer; +} +.page-content .login-from .account-content .tips { + display: none; +} +.show-tips { + max-width: 200px; + padding: 5px; + background-color: rgba(0, 0, 0, 0.4); + font-size: 13px; + color: #ffffffA5; + position: absolute; + bottom: 25px; + left: 200px; + border-radius: 5px; + display: block !important; +} +.page-content .login-from .account-content .show-tips span { + width: 100%; +} +.page-content .login-from .account-content .show-tips:after { + display: block; + content: ''; + width: 0; + height: 0; + border-top: 6px solid rgba(0, 0, 0, 0.4); + border-left: 6px solid transparent; + border-right: 6px solid transparent; + position: absolute; + bottom: -6px; + left: 27px; +} .page-content .login-from .logo-title { opacity: 1; padding: 39px 0 40px 0; diff --git a/src/login/static/css_ce/login.min.css b/src/login/static/css_ce/login.min.css index 18ace2be8..3fce00972 100755 --- a/src/login/static/css_ce/login.min.css +++ b/src/login/static/css_ce/login.min.css @@ -1 +1,57 @@ -*{box-sizing:border-box}b,blockquote,body,button,dd,div,dl,dt,fieldset,form,h1,h2,h3,h4,h5,h6,i,input,li,ol,p,pre,span,td,textarea,th,ul{margin:0;padding:0}body,html{font-size:14px;font-family:"Microsoft YaHei";height:100%;position:relative}a{text-decoration:none;-webkit-transition:all .5s;-moz-transition:all .5s;-ms-transition:all .5s;transition:all .5s}button{text-decoration:none;-webkit-transition:all .5s;-moz-transition:all .5s;-ms-transition:all .5s;transition:all .5s}a:hover{text-decoration:none}li,ol,ul{list-style:none}h1,h2,h3,h4,h5,h6{font-weight:400}input::-webkit-input-placeholder{font-family:"Microsoft YaHei";color:#c4c6cc}input:-moz-placeholder{font-family:"Microsoft YaHei";color:#c4c6cc}input::-moz-placeholder{font-family:"Microsoft YaHei";color:#c4c6cc}input:-ms-input-placeholder{font-family:"Microsoft YaHei";color:#c4c6cc}textarea::-webkit-input-placeholder{font-family:"Microsoft YaHei";color:#c4c6cc}textarea:-moz-placeholder{font-family:"Microsoft YaHei";color:#c4c6cc}textarea::-moz-placeholder{font-family:"Microsoft YaHei";color:#c4c6cc}textarea:-ms-input-placeholder{font-family:"Microsoft YaHei";color:#c4c6cc}.pb110{padding-bottom:110px}.clearfix:after,.clearfix:before{content:"";display:table}.clearfix:after{clear:both}.hide{display:none!important;visibility:hidden}input[type=number]{-moz-appearance:textfield}input,select{background:0 0}input[disabled]{background:0 0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}.page-content{height:100%;width:100%;position:relative;margin:0 auto;background-color:#ebf2fa}#particles-js{height:100%}.page-content .right-top{width:52%;height:0;position:absolute;right:0;top:0;padding-bottom:10%;background-color:#ebf2fa}.page-content .right-top img{width:100%}.page-content .right-bottom{width:31%;height:0;position:absolute;right:1%;bottom:0;padding-bottom:15%}.page-content .right-bottom img{width:100%}.page-content .left-bottom{width:35%;height:0;position:absolute;left:0;bottom:0;padding-bottom:12%}.page-content .left-bottom img{width:100%}.page-content .login-from{width:400px;position:absolute;top:17%;left:50%;margin-left:-200px;z-index:100;overflow:visible;background:#fff;border-radius:8px;box-shadow:0 2px 6px 0 rgba(0,0,0,.1)}.page-content .login-from .logo-title{opacity:1;padding:39px 0 40px 0;display:flex;align-items:center;justify-content:center}.page-content .login-from .logo-title .logo-img{height:34px}.page-content .login-from .from-detail{position:relative;padding-bottom:28px}.page-content .login-from .from-detail .is-danger-tip{position:absolute;color:#ea3636;top:11px;left:38px;font-size:12px}.page-content .login-from .from-detail .is-danger-tip .icon-exclamation-circle-shape{margin-right:10px}.page-content .login-from .form-login{width:100%;padding:0 38px}.page-content .login-from .form-login .change-password{margin-top:2px}.page-content .login-from .form-login .change-password span{color:#ea3636}.page-content .login-from .form-login .change-password a{color:#1768ef}.page-content .login-from .form-login.is-danger .group-control p{color:#ff5656}.page-content .login-from .form-login.is-danger .group-control input{border-color:#ff5656;color:#63656e}.page-content .login-from .form-login.is-danger .group-control input:focus{border-color:#ff5656}.page-content .login-from .form-login.is-danger .group-control input::-webkit-input-placeholder{color:#c4c6cc}.page-content .login-from .form-login.is-danger .group-control input:-moz-placeholder{color:#c4c6cc}.page-content .login-from .form-login.is-danger .group-control input::-moz-placeholder{color:#c4c6cc}.page-content .login-from .form-login.is-danger .group-control input:-ms-input-placeholder{color:#c4c6cc}.page-content .login-from .form-login.certificate-expired .group-control i{color:#cad3dc}.page-content .login-from .form-login.certificate-expired .group-control input{border-color:#dde4eb;color:#63656e}.page-content .login-from .form-login.certificate-expired .group-control input:focus{border-color:#ff5656}.page-content .login-from .form-login.certificate-expired .group-control input::-webkit-input-placeholder{color:#c4c6cc}.page-content .login-from .form-login.certificate-expired .group-control input:-moz-placeholder{color:#c4c6cc}.page-content .login-from .form-login.certificate-expired .group-control input::-moz-placeholder{color:#c4c6cc}.page-content .login-from .form-login.certificate-expired .group-control input:-ms-input-placeholder{color:#c4c6cc}.page-content .login-from .form-login.certificate-expired .btn-content .login-btn{background:#313b4c;cursor:not-allowed}.page-content .login-from .form-login.certificate-expired .btn-content .login-btn:hover{background:#344157}.page-content .login-from .form-login .group-control{width:100%;height:40px;border-radius:2px;position:relative}.page-content .login-from .form-login .user{margin-bottom:28px}.page-content .login-from .form-login .group-control i{position:absolute;font-size:16px;top:12px;right:13px;color:#979ba5}.page-content .login-from .form-login .group-control i:hover{cursor:pointer}.page-content .login-from .form-login .group-control input{width:100%;height:100%;outline:0;border:1px solid #c4c6cc;padding:0 40px 0 12px;color:#63656e;border-radius:2px}.page-content .login-from .form-login .action{margin-top:12px}.page-content .login-from .form-login .group-control input:focus{border-color:#3c96ff}.page-content .login-from .form-login .group-control input::-webkit-input-placeholder{color:#c4c6cc}.page-content .login-from .form-login .group-control input:-moz-placeholder{color:#c4c6cc}.page-content .login-from .form-login .group-control input::-moz-placeholder{color:#c4c6cc}.page-content .login-from .form-login .group-control input:-ms-input-placeholder{color:#c4c6cc}.page-content .login-from .form-login .btn-content{font-size:0;padding-top:28px}.page-content .login-from .form-login .btn-content .login-btn{width:100%;height:42px;display:inline-block;background-color:#3a84ff;border-radius:2px;outline:0;border:none;font-size:14px;line-height:18px;letter-spacing:0;color:#fff;cursor:pointer;float:left}.page-content .login-from .form-login .btn-content .login-btn:hover{background:#3a84ff}.page-content .login-from .form-login .password-btn,.page-content .login-from .form-login .protocol-btn{font-size:14px;letter-spacing:0;color:#63656e;display:inline-block!important;cursor:pointer;float:right}.page-content .login-from .form-login .password-btn:hover,.page-content .login-from .form-login .protocol-btn:hover{color:#1768ef}.language-switcher{display:flex;border-radius:2px;height:24px;line-height:24px;justify-content:end;text-align:right;margin-top:23px}.language-switcher .language-item{width:70px;text-align:center;background:#f5f7fa;transform:skew(-15deg,0deg);display:inline-block;height:24px;cursor:pointer}.language-switcher .language-item:nth-child(1){border-radius:2px 0 0 2px}.language-switcher .language-item:nth-child(2){border-radius:0 2px 2px 0}.language-switcher .language-item .text-active{display:block;width:70px;height:24px;line-height:24px;font-size:12px;transform:skew(15deg,0deg)}.language-switcher .active{background:#e1ecff}.language-switcher .active .text-active{color:#3a84ff}.footer{width:100%;line-height:20px;padding:2% 0;position:absolute;bottom:0;color:#bfcbd7;font-size:12px;text-align:center;background:url(../img/logo_ce/footer.png) no-repeat center;background-size:100% 100%}.footer a{color:#bfcbd7;margin:0 5px}.footer a:hover{color:#fff}.follow-us{position:relative}.follow-us:hover{cursor:pointer}.follow-us:hover .qr-box{display:inline-block}.follow-us .qr-box{position:absolute;bottom:25px;right:-20px;display:none;width:100px;height:100px;background-color:#fff;z-index:101}.follow-us .qr-box::before{content:"";width:0;height:0;border-top:6px solid #fff;border-left:6px solid transparent;border-right:6px solid transparent;position:absolute;top:100px;left:50px}.follow-us .qr-box .qr{padding-top:5px}.protocol-pop{position:fixed;top:0;left:0;width:100%;height:100%;display:none;z-index:101}.protocol-pop .protocol-detail{width:1200px;height:700px;background-color:#fff;border-radius:2px;top:10%;left:50%;margin-left:-600px;position:absolute;padding:59px 23px 40px 37px}.protocol-pop .protocol-detail .del-text{position:absolute;top:0;right:0;width:27px;height:27px;line-height:26px;border-radius:50%;text-align:center;margin:4px 4px 0 0;background-repeat:no-repeat;background-size:11px 11px;background-position:50% 50%;cursor:pointer;display:inline-block}.protocol-pop .protocol-detail .del-text:hover{background-color:#f3f3f3}.protocol-pop .protocol-detail .del-text>i{font-size:10px;color:#50525f;font-weight:700}.protocol-pop .protocol-detail .detail-content{height:536px;overflow-y:auto}.protocol-pop .protocol-detail .detail-content::-webkit-scrollbar{width:6px;height:5px}.protocol-pop .protocol-detail .detail-content::-webkit-scrollbar-thumb{border-radius:20px;background:#a5a5a5;box-shadow:inset 0 0 6px rgba(204,204,204,.3)}.protocol-pop .protocol-detail .detail-content>.title{text-align:center;font-size:32px;font-weight:400;font-stretch:normal;line-height:36px;letter-spacing:1px;color:#4f515e;position:relative;margin-bottom:67px}.protocol-pop .protocol-detail .detail-content>.title:after{content:"";position:absolute;width:30px;height:2px;background:#5c7ac6;top:46px;left:50%;margin-left:-15px}.protocol-pop .protocol-detail .detail-content .detail-list{padding-right:23px}.protocol-pop .protocol-detail .detail-content .detail-list>.title{font-weight:700}.protocol-pop .protocol-detail .detail-content .detail-list P{text-align:left;font-size:12px;line-height:32px;letter-spacing:0;color:#7b7d8a}.protocol-pop .protocol-detail .consent-content{text-align:center;margin-top:25px}.protocol-pop .protocol-detail .consent-content .consent-btn{width:160px;height:42px;display:inline-block;background-color:#5c7ac6;border-radius:2px;border:none;font-size:16px;font-weight:400;font-stretch:normal;line-height:18px;letter-spacing:0;color:#fff}.protocol-pop .protocol-detail .consent-content .consent-btn:hover{background:#526eb5}.error-message-content{position:fixed;top:0;width:100%;height:40px;line-height:40px;text-align:center;display:none}.error-message-content i{cursor:pointer}.error-message-content.is-chrome{background:#f8f6db}.error-message-content.is-certificate{background:#fbd9d9;color:#ff5656}.error-message-content span{color:#ff5656;display:inline-block;margin-right:20px}.error-message-content i{color:#ff5656;display:inline-block} \ No newline at end of file +*{box-sizing:border-box;}body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,i,pre,form,fieldset,input,blockquote,th,td,p,span,button,textarea,b{margin:0;padding:0;}html,body{font-size:14px;font-family:"Microsoft YaHei";height:100%;position:relative;}a{text-decoration:none;-webkit-transition:all 0.5s;-moz-transition:all 0.5s;-ms-transition:all 0.5s;transition:all 0.5s;}button{text-decoration:none;-webkit-transition:all 0.5s;-moz-transition:all 0.5s;-ms-transition:all 0.5s;transition:all 0.5s;}a:hover{text-decoration:none;}ul,ol,li{list-style:none;}h1,h2,h3,h4,h5,h6{font-weight:normal;}input::-webkit-input-placeholder{font-family:"Microsoft YaHei";color:#C4C6CC;}input:-moz-placeholder{font-family:"Microsoft YaHei";color:#C4C6CC;}input::-moz-placeholder{font-family:"Microsoft YaHei";color:#C4C6CC;}input:-ms-input-placeholder{font-family:"Microsoft YaHei";color:#C4C6CC;}textarea::-webkit-input-placeholder{font-family:"Microsoft YaHei";color:#C4C6CC;}textarea:-moz-placeholder{font-family:"Microsoft YaHei";color:#C4C6CC;}textarea::-moz-placeholder{font-family:"Microsoft YaHei";color:#C4C6CC;}textarea:-ms-input-placeholder{font-family:"Microsoft YaHei";color:#C4C6CC;}.pb110{padding-bottom:110px;}.clearfix:before,.clearfix:after{content:"";display:table;}.clearfix:after{clear:both;}.clearfix{*zoom:1;}.hide{display:none !important;visibility:hidden;}input[type="number"]{-moz-appearance:textfield;}input,select{background:none;}input[disabled]{background:none;}input[type="number"]::-webkit-inner-spin-button,input[type="number"]::-webkit-outer-spin-button{-webkit-appearance:none;margin:0;}.page-content{height:100%;width:100%;position:relative;margin:0 auto;background-color:rgb(235,242,250);}#particles-js{height:100%;}.page-content .right-top{width:52%;height:0;position:absolute;right:0;top:0;padding-bottom:10%;background-color:rgb(235,242,250);}.page-content .right-top img{width:100%;}.page-content .right-bottom{width:31%;height:0;position:absolute;right:1%;bottom:0;padding-bottom:15%;}.page-content .right-bottom img{width:100%;}.page-content .left-bottom{width:35%;height:0;position:absolute;left:0;bottom:0;padding-bottom:12%;}.page-content .left-bottom img{width:100%;}.page-content .login-from{width:400px;position:absolute;top:17%;left:50%;margin-left:-200px;z-index:100;overflow:visible;background:#fff;border-radius:8px;box-shadow:0px 2px 6px 0px rgba(0,0,0,0.10);}.page-content .login-from .account-content{position:relative;}.page-content .login-from .account-content .error-text{margin:10px 0;color:#ea3636;}.page-content .login-from .account-content .error-text #admin{color:#5C7AC6;}.page-content .login-from .account-content .error-text #admin:hover{cursor:pointer;}.page-content .login-from .account-content .tips{display:none;}.show-tips{max-width:200px;padding:5px;background-color:rgba(0,0,0,0.4);font-size:13px;color:#ffffffA5;position:absolute;bottom:25px;left:200px;border-radius:5px;display:block !important;}.page-content .login-from .account-content .show-tips span{width:100%;}.page-content .login-from .account-content .show-tips:after{display:block;content:'';width:0;height:0;border-top:6px solid rgba(0,0,0,0.4);border-left:6px solid transparent;border-right:6px solid transparent;position:absolute;bottom:-6px;left:27px;}.page-content .login-from .logo-title{opacity:1;padding:39px 0 40px 0;display:flex;align-items:center;justify-content:center;}.page-content .login-from .logo-title .logo-img{height:34px;}.page-content .login-from .from-detail{position:relative;padding-bottom:28px;}.page-content .login-from .from-detail .is-danger-tip{position:absolute;color:#EA3636;top:11px;left:38px;font-size:12px}.page-content + .login-from + .from-detail + .is-danger-tip + .icon-exclamation-circle-shape{margin-right:10px;}.page-content .login-from .form-login{width:100%;padding:0 38px;}.page-content .login-from .form-login .change-password{margin-top:2px;}.page-content .login-from .form-login .change-password span{color:#ea3636;}.page-content .login-from .form-login .change-password a{color:#1768EF;}.page-content .login-from .form-login.is-danger .group-control p{color:#ff5656;}.page-content .login-from .form-login.is-danger .group-control input{border-color:#ff5656;color:#63656e;}.page-content .login-from .form-login.is-danger .group-control input:focus{border-color:#ff5656;}.page-content + .login-from + .form-login.is-danger + .group-control + input::-webkit-input-placeholder{color:#C4C6CC;}.page-content + .login-from + .form-login.is-danger + .group-control + input:-moz-placeholder{color:#C4C6CC;}.page-content + .login-from + .form-login.is-danger + .group-control + input::-moz-placeholder{color:#C4C6CC;}.page-content + .login-from + .form-login.is-danger + .group-control + input:-ms-input-placeholder{color:#C4C6CC;}.page-content .login-from .form-login.certificate-expired .group-control i{color:#cad3dc;}.page-content .login-from .form-login.certificate-expired .group-control input{border-color:#dde4eb;color:#63656e;}.page-content + .login-from + .form-login.certificate-expired + .group-control + input:focus{border-color:#ff5656;}.page-content + .login-from + .form-login.certificate-expired + .group-control + input::-webkit-input-placeholder{color:#C4C6CC;}.page-content + .login-from + .form-login.certificate-expired + .group-control + input:-moz-placeholder{color:#C4C6CC;}.page-content + .login-from + .form-login.certificate-expired + .group-control + input::-moz-placeholder{color:#C4C6CC;}.page-content + .login-from + .form-login.certificate-expired + .group-control + input:-ms-input-placeholder{color:#C4C6CC;}.page-content + .login-from + .form-login.certificate-expired + .btn-content + .login-btn{background:#313b4c;cursor:not-allowed;}.page-content + .login-from + .form-login.certificate-expired + .btn-content + .login-btn:hover{background:#344157;}.page-content .login-from .form-login .group-control{width:100%;height:40px;border-radius:2px;position:relative;}.page-content .login-from .form-login .user{margin-bottom:28px;}.page-content .login-from .form-login .group-control i{position:absolute;font-size:16px;top:12px;right:13px;color:#979BA5;}.page-content .login-from .form-login .group-control i:hover{cursor:pointer;}.page-content .login-from .form-login .group-control input{width:100%;height:100%;outline:0;border:1px solid #C4C6CC;padding:0 40px 0 12px;color:#63656e;border-radius:2px;}.page-content .login-from .form-login .action{margin-top:12px;}.page-content .login-from .form-login .group-control input:focus{border-color:#3c96ff;}.page-content + .login-from + .form-login + .group-control + input::-webkit-input-placeholder{color:#C4C6CC;}.page-content .login-from .form-login .group-control input:-moz-placeholder{color:#C4C6CC;}.page-content .login-from .form-login .group-control input::-moz-placeholder{color:#C4C6CC;}.page-content + .login-from + .form-login + .group-control + input:-ms-input-placeholder{color:#C4C6CC;}.page-content .login-from .form-login .btn-content{font-size:0;padding-top:28px;}.page-content .login-from .form-login .btn-content .login-btn{width:100%;height:42px;display:inline-block;background-color:#3A84FF;border-radius:2px;outline:0;border:none;font-size:14px;line-height:18px;letter-spacing:0;color:#fff;cursor:pointer;float:left;}.page-content .login-from .form-login .btn-content .login-btn:hover{background:#3A84FF;}.page-content .login-from .form-login .protocol-btn,.page-content .login-from .form-login .password-btn{font-size:14px;letter-spacing:0;color:#63656e;display:inline-block !important;cursor:pointer;float:right}.page-content .login-from .form-login .protocol-btn:hover,.page-content .login-from .form-login .password-btn:hover{color:#1768EF;}.language-switcher{display:flex;border-radius:2px;height:24px;line-height:24px;justify-content:end;text-align:right;margin-top:23px;}.language-switcher .language-item{width:70px;text-align:center;background:#f5f7fa;transform:skew(-15deg,0deg);display:inline-block;height:24px;cursor:pointer;}.language-switcher .language-item:nth-child(1){border-radius:2px 0 0 2px;}.language-switcher .language-item:nth-child(2){border-radius:0 2px 2px 0;}.language-switcher .language-item .text-active{display:block;width:70px;height:24px;line-height:24px;font-size:12px;transform:skew(15deg,0deg);}.language-switcher .active{background:#e1ecff;}.language-switcher .active .text-active{color:#3a84ff;}.footer{width:100%;line-height:20px;padding:2% 0;position:absolute;bottom:0;color:#bfcbd7;font-size:12px;text-align:center;background:url(../img/logo_ce/footer.png) no-repeat center;background-size:100% 100%;}.footer a{color:#bfcbd7;margin:0 5px;}.footer a:hover{color:#fff;}.follow-us{position:relative;}.follow-us:hover{cursor:pointer;}.follow-us:hover .qr-box{display:inline-block;}.follow-us .qr-box{position:absolute;bottom:25px;right:-20px;display:none;width:100px;height:100px;background-color:#fff;z-index:101;}.follow-us .qr-box::before{content:"";width:0px;height:0px;border-top:6px solid #fff;border-left:6px solid transparent;border-right:6px solid transparent;position:absolute;top:100px;left:50px;}.follow-us .qr-box .qr{padding-top:5px;}.protocol-pop{position:fixed;top:0;left:0;width:100%;height:100%;display:none;z-index:101;}.protocol-pop .protocol-detail{width:1200px;height:700px;background-color:#ffffff;border-radius:2px;top:10%;left:50%;margin-left:-600px;position:absolute;padding:59px 23px 40px 37px;}.protocol-pop .protocol-detail .del-text{position:absolute;top:0;right:0;width:27px;height:27px;line-height:26px;border-radius:50%;text-align:center;margin:4px 4px 0 0;background-repeat:no-repeat;background-size:11px 11px;background-position:50% 50%;cursor:pointer;display:inline-block;}.protocol-pop .protocol-detail .del-text:hover{background-color:#f3f3f3;}.protocol-pop .protocol-detail .del-text > i{font-size:10px;color:#50525f;font-weight:bold;}.protocol-pop .protocol-detail .detail-content{height:536px;overflow-y:auto;}.protocol-pop .protocol-detail .detail-content::-webkit-scrollbar{width:6px;height:5px;}.protocol-pop .protocol-detail .detail-content::-webkit-scrollbar-thumb{border-radius:20px;background:#a5a5a5;box-shadow:inset 0 0 6px rgba(204,204,204,0.3);}.protocol-pop .protocol-detail .detail-content > .title{text-align:center;font-size:32px;font-weight:normal;font-stretch:normal;line-height:36px;letter-spacing:1px;color:#4f515e;position:relative;margin-bottom:67px;}.protocol-pop .protocol-detail .detail-content > .title:after{content:"";position:absolute;width:30px;height:2px;background:#5c7ac6;top:46px;left:50%;margin-left:-15px;}.protocol-pop .protocol-detail .detail-content .detail-list{padding-right:23px;}.protocol-pop .protocol-detail .detail-content .detail-list > .title{font-weight:bold;}.protocol-pop .protocol-detail .detail-content .detail-list P{text-align:left;font-size:12px;line-height:32px;letter-spacing:0;color:#7b7d8a;}.protocol-pop .protocol-detail .consent-content{text-align:center;margin-top:25px;}.protocol-pop .protocol-detail .consent-content .consent-btn{width:160px;height:42px;display:inline-block;background-color:#5c7ac6;border-radius:2px;border:none;font-size:16px;font-weight:normal;font-stretch:normal;line-height:18px;letter-spacing:0px;color:#ffffff;}.protocol-pop .protocol-detail .consent-content .consent-btn:hover{background:#526eb5;}.error-message-content{position:fixed;top:0;width:100%;height:40px;line-height:40px;text-align:center;display:none;}.error-message-content i{cursor:pointer;}.error-message-content.is-chrome{background:#f8f6db;}.error-message-content.is-certificate{background:#fbd9d9;color:#ff5656;}.error-message-content span{color:#ff5656;display:inline-block;margin-right:20px;}.error-message-content i{color:#ff5656;display:inline-block;} \ No newline at end of file diff --git a/src/login/static/js_ce/login.js b/src/login/static/js_ce/login.js index e62b855bf..7bcf294c4 100755 --- a/src/login/static/js_ce/login.js +++ b/src/login/static/js_ce/login.js @@ -85,4 +85,12 @@ $(document).ready(function(){ $('#invisible').attr('class', 'bk-icon icon-invisible-eye'); } }) + + // 管理员 + $('#admin').on('mouseover', function () { + $('.tips').addClass('show-tips'); + }) + $('#admin').on('mouseout', function () { + $('.tips').removeClass('show-tips'); + }) }); diff --git a/src/login/static/js_ce/login.min.js b/src/login/static/js_ce/login.min.js index caf200a82..2d6e7874b 100755 --- a/src/login/static/js_ce/login.min.js +++ b/src/login/static/js_ce/login.min.js @@ -1 +1 @@ -function getCookie(o){var e=null;if(document.cookie&&""!=document.cookie)for(var n=document.cookie.split(";"),t=0;t + + + + + diff --git a/src/pages/src/components/catalog/operation/SetAccount.vue b/src/pages/src/components/catalog/operation/SetAccount.vue new file mode 100644 index 000000000..93760d586 --- /dev/null +++ b/src/pages/src/components/catalog/operation/SetAccount.vue @@ -0,0 +1,366 @@ + + + + + diff --git a/src/pages/src/components/catalog/operation/SetPassword.vue b/src/pages/src/components/catalog/operation/SetPassword.vue index 8569de8ca..4511031da 100644 --- a/src/pages/src/components/catalog/operation/SetPassword.vue +++ b/src/pages/src/components/catalog/operation/SetPassword.vue @@ -21,277 +21,312 @@ -->