diff --git a/.github/workflows/unittest.yml b/.github/workflows/unittest.yml index d3a2085e8..f18592786 100644 --- a/.github/workflows/unittest.yml +++ b/.github/workflows/unittest.yml @@ -20,13 +20,17 @@ jobs: DB_DATABASE: bk_user_api_test DB_USER: root DB_PASSWORD: root + DB_PORT: 3306 steps: - uses: actions/checkout@v2 - - name: Set up MySQL - run: | - sudo /etc/init.d/mysql start - mysql -e 'CREATE DATABASE ${{ env.DB_DATABASE }};' -u${{ env.DB_USER }} -p${{ env.DB_PASSWORD }} + - uses: samin/mysql-action@v1.3 + with: + mysql version: '5.7' + mysql database: ${{ env.DB_DATABASE }} + mysql root password: ${{ env.DB_PASSWORD }} + mysql user: ${{ env.DB_USER }} + mysql password: ${{ env.DB_PASSWORD }} - name: Set up Python uses: actions/setup-python@v2 with: @@ -38,7 +42,7 @@ jobs: - name: Install dependencies run: | cd src/api/ - poetry config virtualenvs.create false && poetry install + poetry config virtualenvs.create false && bash bin/install_ci_dependencies.sh - name: Run api unittest env: DJANGO_SETTINGS_MODULE: "bkuser_core.config.overlays.unittest" @@ -52,7 +56,7 @@ jobs: DB_USER: ${{ env.DB_USER }} DB_PASSWORD: ${{ env.DB_PASSWORD }} DB_HOST: "127.0.0.1" - DB_PORT: "3306" + DB_PORT: ${{ env.DB_PORT }} run: | make link cd src/api diff --git a/.gitignore b/.gitignore index 77570079c..efc0ac5f9 100644 --- a/.gitignore +++ b/.gitignore @@ -203,6 +203,7 @@ src/pages/index-dev.html # ignore global code src/api/bkuser_global src/saas/bkuser_global +src/login/bkuser_global # sdk src/saas/bkuser_sdk @@ -220,12 +221,15 @@ src/pages/dist/ /poetry.lock # helm -deploy/helm/*/charts/ +deploy/helm/bk-user/charts/api/charts/ +deploy/helm/bk-user/charts/saas/charts/ +deploy/helm/bk-user/charts/login/charts/ deploy/helm/local_values.yaml deploy/helm/dist/ deploy/helm/api/templates/c_*.* deploy/helm/saas/templates/c_*.* +deploy/helm/login/templates/c_*.* deploy/helm/bk-user-stack/templates/c_*.* # local hooks diff --git a/Makefile b/Makefile index 9f136fea7..6ecc67173 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,5 @@ version ?= "development" +login_version ?= "development" values ?= image_repo ?= "ccr.ccs.tencentyun.com/bk.io" chart_repo ?= @@ -6,12 +7,20 @@ namespace ?= "bk-user" test_release_name ?= "bk-user-test" generate-release-md: - cd src/saas/ && poetry run python manage.py generate_release_md > release.md + rm docs/changelogs/*.md || true + cd src/saas/ && mkdir -p changelogs/ && poetry run python manage.py generate_release_md + mv src/saas/changelogs docs/ mv src/saas/release.md docs/ link: + rm src/api/bkuser_global || true + rm src/saas/bkuser_global || true + rm src/login/bkuser_global || true + rm src/saas/bkuser_sdk || true + ln -s ${PWD}/src/bkuser_global src/api || true ln -s ${PWD}/src/bkuser_global src/saas || true + ln -s ${PWD}/src/bkuser_global src/login || true ln -s ${PWD}/src/sdk/bkuser_sdk src/saas || true generate-sdk: @@ -23,17 +32,23 @@ build-api: build-saas: docker build -f src/saas/Dockerfile . -t ${image_repo}/bk-user-saas:${version} -build-all: build-api build-saas +build-login: + docker build -f src/login/Dockerfile . -t ${image_repo}/bk-login:${login_version} + +build-all: build-api build-saas build-login push: docker push ${image_repo}/bk-user-api:${version} docker push ${image_repo}/bk-user-saas:${version} + docker push ${image_repo}/bk-login:${login_version} helm-sync: mkdir -p deploy/helm/api/templates/ mkdir -p deploy/helm/saas/templates/ + mkdir -p deploy/helm/login/templates/ ln -s ${PWD}/deploy/helm/chartty/* deploy/helm/api/templates/ || true ln -s ${PWD}/deploy/helm/chartty/* deploy/helm/saas/templates/ || true + ln -s ${PWD}/deploy/helm/chartty/* deploy/helm/login/templates/ || true ln -s ${PWD}/deploy/helm/chartty/c_*.tpl deploy/helm/bk-user-stack/templates/ || true diff --git a/deploy/helm/api/Chart.yaml b/deploy/helm/api/Chart.yaml deleted file mode 100644 index ab9e4a49b..000000000 --- a/deploy/helm/api/Chart.yaml +++ /dev/null @@ -1,6 +0,0 @@ -apiVersion: v2 -appVersion: v2.3.1 -description: A Helm chart for bk user api -name: bkuserapi -type: application -version: 1.0.0 diff --git a/deploy/helm/api/values.yaml b/deploy/helm/api/values.yaml deleted file mode 100644 index dcd474584..000000000 --- a/deploy/helm/api/values.yaml +++ /dev/null @@ -1,240 +0,0 @@ -# 全局变量,通常用于多个 Chart 之间共享 -global: - imagePullSecrets: [] - # imagePullSecrets, 预先创建的 imagePullSecrets, 将直接被添加到 chartty.imagePullSecretNames 中. - # - name: "secret-a" - # - name: "secret-b" - - # credential, 用于创建独享的 Secret 资源 - imageCredentials: - # 当且仅当 enabled 为 true 时,会生成 dockerconfigjson 类型的 Secret 资源, 并在 chartty.imagePullSecretNames 添加该名称. - enabled: false - password: "" - registry: "" - username: "" - name: "" - - # 全局镜像配置 - image: - registry: "ccr.ccs.tencentyun.com/bk.io" - pullPolicy: Always - - # 全局环境变量,当 `env` 指定时,`global.env` 内相同 key 值变量将被覆盖 - env: {} - - # 默认的全局根域 - sharedDomain: "" - -# 缺省实例数 -replicaCount: 1 - -image: - name: bk-user-api - -# 用来覆盖 Chart 名 -nameOverride: "" -# 用来覆盖 fullName (通常是 release-chart 拼接) -fullnameOverride: "" - -# 是否自动创建 serviceAccount -serviceAccount: - create: true - annotations: {} - name: "" - -podAnnotations: {} - -podSecurityContext: {} - -# 支持定义 labels -podLabels: {} - -securityContext: {} - -service: - type: ClusterIP - port: 80 - -#--------------- -# 调度 -#--------------- -nodeSelector: {} - -tolerations: [] - -affinity: {} - -#--------------- -# 环境变量 -# 除 global.env 和 env 外 -# 其余变量定义均不去重,请手动确保无变量名冲突 -#--------------- - -# key-value 结构渲染 -env: - # ------------- - # 默认配置,不了解详情时请不要修改 - # ------------- - BK_APP_CODE: "bk-user" - DJANGO_SETTINGS_MODULE: "bkuser_core.config.overlays.prod" - # ------------- - # 权限中心相关配置 - # ------------- - BK_IAM_SYSTEM_ID: "bk_usermgr" - # 权限中心后台访问地址 - BK_IAM_V3_INNER_HOST: "http://bkiam-web" - # 默认我们会按照 BK_PAAS_URL/o/bk_iam 拼接权限中心 SaaS 访问地址,可以通过以下值覆盖 - # BK_IAM_SAAS_HOST: "http://apps.bktencent-example.com/bkapp-bk-iam-saas-prod/" - -envFrom: [] - -# 提供原生的 env 写法 -extrasEnv: [] - -# 额外提供一种基于 sharedDomain 自动生成的 URL 类型环境变量 -sharedUrlEnvMap: - SAAS_URL: "http://bkuser.{{ .Values.global.sharedDomain }}" - # 使容器可以自我感知访问地址 - BK_USER_API_URL: "http://bkuser-api.{{ .Values.global.sharedDomain }}" - -# 标识必填的环境变量列表 -requiredEnvList: [] - -#--------------- -# 进程定义 -#-------------- -httpPort: 8000 -database: - preferName: bk-user-api - -# 定义应用内的多个进程 -processes: - web: - ingress: - enabled: true - host: "bkuser-api.{{ .Values.global.sharedDomain }}" - paths: ["/"] - replicas: 1 - resources: - limits: - cpu: 1024m - memory: 1024Mi - requests: - cpu: 200m - memory: 128Mi - readinessProbe: - tcpSocket: - port: 8000 - initialDelaySeconds: 5 - periodSeconds: 30 - livenessProbe: - httpGet: - path: /ping - port: http - initialDelaySeconds: 5 - periodSeconds: 30 - celery: - replicas: 1 - resources: - limits: - cpu: 1024m - memory: 1024Mi - requests: - cpu: 200m - memory: 128Mi - command: - - bash - args: - - /app/start_celery.sh - beat: - replicas: 1 - resources: - limits: - cpu: 1024m - memory: 512Mi - requests: - cpu: 100m - memory: 128Mi - command: - - bash - args: - - /app/start_beat.sh - - -# 部署前钩子 -preRunHooks: - db-migrate: - weight: 1 - enabled: true - position: "pre-install,pre-upgrade" - command: - - bash - args: - - -c - - python manage.py migrate - bkiam-migrate: - weight: 2 - enabled: false - position: "pre-install,pre-upgrade" - command: - - bash - args: - - /app/migrate_iam.sh - -# 支持定义多个 cronJobs -cronJobs: - jobs: [] - -# 挂载配置 -volumes: [] -volumeMounts: [] - -# 支持定义 configmaps -configMaps: [] - -# 当 Chart 独立部署时,默认关闭内建存储 -mariadb: - enabled: false - -## ServiceMonitor configuration -## -serviceMonitor: - ## @param serviceMonitor.enabled Creates a ServiceMonitor to monitor kube-state-metrics - ## - enabled: false - ## @param serviceMonitor.jobLabel The name of the label on the target service to use as the job name in prometheus. - ## - jobLabel: "" - ## @param serviceMonitor.interval Scrape interval (use by default, falling back to Prometheus' default) - ## ref: https://github.com/coreos/prometheus-operator/blob/master/Documentation/api.md#endpoint - ## e.g: - ## interval: 10s - ## - interval: "" - ## @param serviceMonitor.scrapeTimeout Timeout after which the scrape is ended - ## ref: https://github.com/coreos/prometheus-operator/blob/master/Documentation/api.md#endpoint - ## e.g: - ## scrapeTimeout: 10s - ## - scrapeTimeout: "" - ## @param serviceMonitor.selector ServiceMonitor selector labels - ## ref: https://github.com/bitnami/charts/tree/master/bitnami/prometheus-operator#prometheus-configuration - ## e.g: - ## selector: - ## prometheus: my-prometheus - ## - selector: {} - ## @param serviceMonitor.honorLabels Honor metrics labels - ## ref: https://github.com/coreos/prometheus-operator/blob/master/Documentation/api.md#endpoint - ## e.g: - ## honorLabels: false - ## - honorLabels: false - ## @param serviceMonitor.relabelings ServiceMonitor relabelings - ## ref: https://github.com/coreos/prometheus-operator/blob/master/Documentation/api.md#relabelconfig - ## - relabelings: [] - ## @param serviceMonitor.metricRelabelings ServiceMonitor metricRelabelings - ## ref: https://github.com/coreos/prometheus-operator/blob/master/Documentation/api.md#relabelconfig - ## - metricRelabelings: [] diff --git a/deploy/helm/bk-user-stack/Chart.lock b/deploy/helm/bk-user-stack/Chart.lock deleted file mode 100644 index 75d9d8ba8..000000000 --- a/deploy/helm/bk-user-stack/Chart.lock +++ /dev/null @@ -1,15 +0,0 @@ -dependencies: -- name: bkuserapi - repository: file://../api - version: 1.0.0 -- name: bkusersaas - repository: file://../saas - version: 1.0.0 -- name: mariadb - repository: https://charts.bitnami.com/bitnami - version: 9.4.0 -- name: redis - repository: https://charts.bitnami.com/bitnami - version: 14.8.7 -digest: sha256:5f5c69a6e9e9f002d5897f19c2de8ad441ae65aaa09b641880bb044b1b9cdda1 -generated: "2021-09-01T15:22:58.235089+08:00" diff --git a/deploy/helm/bk-user-stack/README.md b/deploy/helm/bk-user-stack/README.md deleted file mode 100644 index b8f086f4d..000000000 --- a/deploy/helm/bk-user-stack/README.md +++ /dev/null @@ -1,206 +0,0 @@ -# Bk-User-Helm-Stack - -Bk-User-Helm-Stack 是一个旨在快速部署用户管理部署工具,它在 Helm Chart 的基础上开发,旨在为用户管理产品提供方便快捷的部署能力。 - -## 准备依赖服务 - -要部署蓝鲸用户管理,首先需要准备 1 个 Kubernetes 集群(版本 1.12 或更高),并安装 Helm 命令行工具(版本 3.0 或更高)。 - -我们使用 `Ingress` 对外提供服务访问,所以请在集群中至少安装一个可用的 [Ingress Controller](https://kubernetes.io/docs/concepts/services-networking/ingress-controllers/) - -### 配置 Helm 仓库地址 -```bash -# 请将 `` 替换为 Chart 所在的 Helm 仓库地址 -helm repo add bk-paas3 `` -helm repo update -``` - -### 其他服务 -由于蓝鲸用户管理 SaaS 是需要校验用户身份的服务,所以在能够正常访问前,请确认以下服务已就绪: - -- [蓝鲸登录](https://github.com/Tencent/bk-PaaS/tree/master/paas-ce/paas/login) -- [蓝鲸权限中心](https://github.com/TencentBlueKing/bk-iam) - - -## 快速安装 - -### 准备 `values.yaml` - -#### 1. 获取蓝鲸平台访问地址 -首先,你需要获取到蓝鲸平台的访问地址,例如 `https://paas.example.com`,确保 `https://paas.example.com/login` 可以访问蓝鲸登录,然后将该值的内容填入全局环境变量中。 - -配置示例: -```yaml -global: - env: - # 蓝鲸平台域名 - BK_PAAS_URL: "https://paas.example.com" -``` - -#### 2. 确定用户管理访问地址 - -你需要为用户管理提供一个访问根域,类似 `example.com`,配置示例: -```yaml -global: - sharedDomain: "example.com" -``` - -默认地,我们会为 `Api` & `SaaS` 分别创建两个访问入口(Ingress): -- `bkuser.example.com` SaaS 页面访问入口 -- `bkuser-api.example.com` Api 访问入口 - -#### 3. 准备用户管理镜像 - -用户管理官方提供了两个镜像: -```text -ccr.ccs.tencentyun.com/bk.io/bk-user-api:${version} -ccr.ccs.tencentyun.com/bk.io/bk-user-saas:${version} -``` -我们会在每次发布用户管理新版时,会同步更新 Chart 中的镜像版本,所以如果你只是想使用最新版本的官方镜像,可以跳过此节,不用关注镜像的填写。 - -如果你想使用官方其他版本或者自己构建的镜像,也可以在 `values.yaml` 中修改,配置示例: -```yaml -global: - image: - # 修改镜像地址,我们会按照 {registry}/{repository} 方式拼接 - registry: any-mirrors-you-want.com/any-group - # 修改用户管理版本,从 https://github.com/TencentBlueKing/bk-user/releases 获取 - tag: "v2.3.0" -``` - -#### 4. 数据库依赖 - -我们为**功能快速验证**提供了内嵌的 `mariadb` 组件,但我们并不保证该数据库的高可用性,所以***不建议在生产环境中直接使用***。 - -如果你没有数据库方面的特殊要求,那么不需要关注以下 `mariadb` 的默认配置。 - -```yaml -mariadb: - enabled: true - architecture: standalone - auth: - rootPassword: "root" - username: "bk-user" - password: "root" - primary: - # 默认我们未开启持久化,如有需求可以参考: https://kubernetes.io/docs/user-guide/persistent-volumes/ - persistence: - enabled: false - initdbScriptsConfigMap: "bk-user-mariadb-init -``` - -如果你想要在生产环境中使用其他外部数据库,那么可以通过环境变量来指定,并禁用 `mariadb`,配置示例: - -```yaml -bkuserapi: - enabeld: true - env: - # 手动指定外部 DB ,仅支持 MySQL/MariaDB - DB_NAME: "your-db-name" - DB_USER: "your-db-user" - DB_PASSWORD: "your-db-password" - DB_HOST: "your-db-host" - DB_PORT: "your-db-port" - # 外部 Celery Broker,任意符合要求的 Broker 存储均可 - CELERY_BROKER_URL: "your-broker-url" - CELERY_RESULT_BACKEND: "your-broker-url" - # 手动取消内建存储挂载 - envFrom: [] - -bkusersaas: - enabled: true - env: - DB_NAME: "your-db-name" - DB_USER: "your-db-user" - DB_PASSWORD: "your-db-password" - DB_HOST: "your-db-host" - DB_PORT: "your-db-port" - # 手动取消内建存储挂载 - envFrom: [] - -mariadb: - enabled: false - -redis: - enabled: false -``` - -#### 5. 权限中心 -默认地,我们未开启权限中心,如果在权限中心已经就绪之后,想体验用户管理功能,那么你可以手动向权限中心注册模型: -```yaml -global: - env: - ENABLE_IAM: true - -bkuserapi: - env: - # 填充权限中心相关变量 - BK_IAM_V3_INNER_HOST: "https://iam.example.com" - # 打开权限中心模型注册,每次重新部署即会运行 - preRunHooks: - bkiam-migrate: - enabled: true -``` - -#### 6. 账号密码 -我们需要为 `admin` 账户添加用户名密码,虽然我们给定了默认值,但是为了安全,请手动修改: -```yaml -bkuserapi: - env: - # !!!请修改初始账号密码!!! - INITIAL_ADMIN_USERNAME: "your-user-name" - INITIAL_ADMIN_PASSWORD: "your-super-strong-password" -``` - -#### 7. 如何扩容 -我们支持对任意进程进行扩容,就像这样: -```yaml -bkuserapi: - processes: - web: - replicas: 3 - celery: - replicas: 2 - beat: - replicas: 1 (切记,beat 进程只能存在一个副本,否则后台任务会重复执行) - -bkusersaas: - processes: - web: - replicas: 2 -``` - -### 安装 - -如果你已经准备好了 `values.yaml`,就可以直接进行安装操作了 - -```bash -# 假定你想在 bk-user 命名空间安装 -kubectl create namespace bk-user -helm install bk-user bk-user-stack -n bk-user -f values.yaml -``` -安装过程中,命令行会预期**阻塞等待**数据库进行 `migrate` 操作: -- 首次安装时,会多次提示 `Pod api-on-migrate pending` 类似字样,原因是 `mariadb` 等待就绪耗时较长, `migrate` 容器会不断失败重试,请耐心等待。 -- 升级安装时,会出现 `Pod api-on-migrate running` 类似字样,表示正在执行 `migrate` 操作,耗时一般在 10s 以内,具体视 migrate 内容而定,请耐心等待。 - -如果确认此次安装或更新无须变更数据库,可以临时手动关闭: -```bash -helm install bk-user bk-user-stack -n bk-user -f values.yaml \ - --set api.preRunHooks.db-migrate.enabled=false \ - --set saas.preRunHooks.db-migrate.enabled=false -``` - -如果在安装完成之后,访问 SaaS 地址出现 `503`,可以检查一下 `saas-web` 容器是否完全就绪,静候就绪后刷新页面即可。 - -## 卸载 -```bash -# 卸载资源 -helm uninstall bk-user -n bk-user - -# 已安装的 mariadb & redis 并不会被删除,防止没有开启持久化期间产生的数据被销毁 -# 如果确认已不再需要相关内容,可以手动删除命名空间内的资源 -# 独立命名空间时 -kubectl delete ns bk-user -# 非独立命名空间时 -kubectl delete deploy,sts,cronjob,pod,svc,ingress,secret,cm,sa,role,rolebinding,pvc -l app.kubernetes.io/instance=bk-user -n bk-user -``` diff --git a/deploy/helm/bk-user-stack/templates/mariadb-env-configmap.yaml b/deploy/helm/bk-user-stack/templates/mariadb-env-configmap.yaml deleted file mode 100644 index 170f106f4..000000000 --- a/deploy/helm/bk-user-stack/templates/mariadb-env-configmap.yaml +++ /dev/null @@ -1,34 +0,0 @@ -{{- if .Values.mariadb.enabled }} -apiVersion: v1 -kind: ConfigMap -metadata: - name: bk-user-api-mariadb-env - labels: - {{- include "chartty.labels" . | nindent 4 }} - annotations: - "helm.sh/hook": pre-install - "helm.sh/hook-weight": "-1" -data: - DB_NAME: "{{ .Values.bkuserapi.database.preferName }}" - DB_USER: "{{ .Values.mariadb.auth.username }}" - DB_PASSWORD: "{{ .Values.mariadb.auth.password }}" - DB_HOST: "{{ .Release.Name }}-mariadb" - DB_PORT: "3306" -# Another configmap ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: bk-user-saas-mariadb-env - labels: - {{- include "chartty.labels" . | nindent 4 }} - annotations: - "helm.sh/hook": pre-install - "helm.sh/hook-weight": "-1" -data: - DB_NAME: "{{ .Values.bkusersaas.database.preferName }}" - DB_USER: "{{ .Values.mariadb.auth.username }}" - DB_PASSWORD: "{{ .Values.mariadb.auth.password }}" - DB_HOST: "{{ .Release.Name }}-mariadb" - DB_PORT: "3306" -{{- end -}} diff --git a/deploy/helm/bk-user-stack/templates/mariadb-init-configmap.yaml b/deploy/helm/bk-user-stack/templates/mariadb-init-configmap.yaml deleted file mode 100644 index 53b472908..000000000 --- a/deploy/helm/bk-user-stack/templates/mariadb-init-configmap.yaml +++ /dev/null @@ -1,18 +0,0 @@ -{{- if .Values.mariadb.enabled }} ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: bk-user-mariadb-init - labels: - {{- include "chartty.labels" . | nindent 4 }} - annotations: - "helm.sh/hook": pre-install - "helm.sh/hook-weight": "-1" -data: - init.sql: | - CREATE DATABASE IF NOT EXISTS `{{ .Values.bkuserapi.database.preferName }}` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; - CREATE DATABASE IF NOT EXISTS `{{ .Values.bkusersaas.database.preferName }}` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; - GRANT ALL PRIVILEGES ON `{{ .Values.bkuserapi.database.preferName }}`.* TO `{{ .Values.mariadb.auth.username }}`@'%'; - GRANT ALL PRIVILEGES ON `{{ .Values.bkusersaas.database.preferName }}`.* TO `{{ .Values.mariadb.auth.username }}`@'%'; -{{- end -}} \ No newline at end of file diff --git a/deploy/helm/bk-user-stack/templates/redis-env-configmap.yaml b/deploy/helm/bk-user-stack/templates/redis-env-configmap.yaml deleted file mode 100644 index 2487e111d..000000000 --- a/deploy/helm/bk-user-stack/templates/redis-env-configmap.yaml +++ /dev/null @@ -1,15 +0,0 @@ -{{- if .Values.redis.enabled }} -apiVersion: v1 -kind: ConfigMap -metadata: - name: bk-user-api-redis-env - labels: - {{- include "chartty.labels" . | nindent 4 }} - annotations: - "helm.sh/hook": pre-install - "helm.sh/hook-weight": "-1" -data: - {{- $redis_url := printf "redis://:%s@%s-redis-master:%s/0" $.Values.redis.auth.password $.Release.Name ($.Values.redis.master.service.port | toString )}} - CELERY_BROKER_URL: {{ $redis_url }} - CELERY_RESULT_BACKEND: {{ $redis_url }} -{{- end }} diff --git a/deploy/helm/bk-user-stack/values.yaml b/deploy/helm/bk-user-stack/values.yaml deleted file mode 100644 index 0c39ce525..000000000 --- a/deploy/helm/bk-user-stack/values.yaml +++ /dev/null @@ -1,93 +0,0 @@ -global: - # 用户管理产品对外暴露访问根域 - sharedDomain: "example.com" - # 全局镜像配置 - image: - registry: "ccr.ccs.tencentyun.com/bk.io" - pullPolicy: Always - - # 日志采集,默认关闭,当日志采集就绪时,手动开启 - bkLogConfig: - enabled: false - dataId: "" - - env: - # 请在 PaaS 产品就绪后,查询 secret 并填入,否则影响用户管理调用 ESB 的相关功能(邮件通知等) - BK_APP_SECRET: "your-own-secret" - # 默认采用集群内 Service 访问 PaaS 平台和蓝鲸产品,如果有其他部署方式,请手动覆盖相关地址 - # PaaS 平台访问地址 - BK_PAAS_URL: "http://paas.example.com" - # ESB Api 访问地址 - BK_COMPONENT_API_URL: "http://bkapi.example.com" - # 由于用户管理先于权限中心拉起,所以默认禁用,后期所有产品就绪后,可手动开启 - ENABLE_IAM: false - -bkuserapi: - enabeld: true - env: - # !!!安全:请修改初始账号密码!!! - INITIAL_ADMIN_USERNAME: "admin" - INITIAL_ADMIN_PASSWORD: "Blueking@2019" - envFrom: - # 挂载内建 DB 变量 - - configMapRef: - name: bk-user-api-mariadb-env - - configMapRef: - name: bk-user-api-redis-env - - # 默认我们关闭了监控采集,当监控就绪时,请手动开启 - # serviceMonitor: - # enabled: true - -bkusersaas: - enabled: true - envFrom: - # 挂载内建 DB 变量 - - configMapRef: - name: bk-user-saas-mariadb-env - - # 默认我们关闭了监控采集,当监控就绪时,请手动开启 - # serviceMonitor: - # enabled: true - -# ------------- -# 内建存储配置 -# 默认通过 .Release.Name 拼接访问,请不要配置 nameOverride 或 fullnameOverride -# 否则会出现无法访问存储的异常 -# ------------- -mariadb: - enabled: true - commonAnnotations: - "helm.sh/hook": "pre-install" - "helm.sh/hook-weight": "-1" - "helm.sh/hook-delete-policy": hook-failed,before-hook-creation - architecture: standalone - auth: - username: "bk-user" - password: "maybe_a_strong_passwd" - primary: - # 默认我们未开启持久化,如有需求可以参考: - # - https://kubernetes.io/docs/user-guide/persistent-volumes/ - # - https://github.com/bitnami/charts/blob/master/bitnami/mariadb/values.yaml#L360 - # 当同时请注意,当开启 PVC 可能会导致首次安装部署时间延长 - persistence: - enabled: false - initdbScriptsConfigMap: "bk-user-mariadb-init" - -redis: - enabled: true - commonAnnotations: - "helm.sh/hook": "pre-install" - "helm.sh/hook-weight": "-1" - "helm.sh/hook-delete-policy": hook-failed,before-hook-creation - sentinel: - enabled: false - auth: - password: "maybe_another_strong_passwd" - master: - persistence: - enabled: false - replica: - replicaCount: 1 - persistence: - enabled: false \ No newline at end of file diff --git a/deploy/helm/api/.helmignore b/deploy/helm/bk-user/.helmignore similarity index 100% rename from deploy/helm/api/.helmignore rename to deploy/helm/bk-user/.helmignore diff --git a/deploy/helm/bk-user/Chart.lock b/deploy/helm/bk-user/Chart.lock new file mode 100644 index 000000000..61f3b7826 --- /dev/null +++ b/deploy/helm/bk-user/Chart.lock @@ -0,0 +1,18 @@ +dependencies: +- name: mariadb + repository: https://charts.bitnami.com/bitnami + version: 9.8.1 +- name: redis + repository: https://charts.bitnami.com/bitnami + version: 14.8.11 +- name: api + repository: "" + version: 1.0.0 +- name: login + repository: "" + version: 1.0.0 +- name: saas + repository: "" + version: 1.0.0 +digest: sha256:7751d2e4cfea1e615575714c797492542822bc7517502c016ddf69a72b080f77 +generated: "2022-01-14T14:40:50.915521+08:00" diff --git a/deploy/helm/bk-user-stack/Chart.yaml b/deploy/helm/bk-user/Chart.yaml similarity index 62% rename from deploy/helm/bk-user-stack/Chart.yaml rename to deploy/helm/bk-user/Chart.yaml index eaeda5995..c0379b50c 100644 --- a/deploy/helm/bk-user-stack/Chart.yaml +++ b/deploy/helm/bk-user/Chart.yaml @@ -1,20 +1,11 @@ apiVersion: v2 -name: bk-user-stack +name: bk-user description: A Helm chart for bk-user type: application -version: 0.5.6 -appVersion: v2.3.1 +version: 1.1.7 +appVersion: v2.3.2 dependencies: -- name: bkuserapi - version: "1.0.0" - repository: "file://../api" - condition: bkuserapi.enabled - -- name: bkusersaas - version: "1.0.0" - repository: "file://../saas" - condition: bkusersaas.enabled - name: mariadb version: "9.x.x" @@ -25,3 +16,15 @@ dependencies: version: "14.x.x" repository: "https://charts.bitnami.com/bitnami" condition: redis.enabled + +- name: api + version: "1.0.0" + condition: api.enabled + +- name: login + version: "1.0.0" + condition: login.enabled + +- name: saas + version: "1.0.0" + condition: saas.enabled diff --git a/deploy/helm/bk-user/README.md b/deploy/helm/bk-user/README.md new file mode 100644 index 000000000..0c6b12ee2 --- /dev/null +++ b/deploy/helm/bk-user/README.md @@ -0,0 +1,253 @@ +# bk-user Chart 安装说明 + +bk-user 是一个旨在快速部署用户管理部署工具,它在 Helm Chart 的基础上开发,旨在为用户管理产品提供方便快捷的部署能力。 + +## 准备依赖服务 + +要部署蓝鲸用户管理,首先需要准备 1 个 Kubernetes 集群(版本 1.12 或更高),并安装 Helm 命令行工具(版本 3.0 或更高)。 + +我们使用 `Ingress` 对外提供服务访问,所以请在集群中至少安装一个可用的 [Ingress Controller](https://kubernetes.io/docs/concepts/services-networking/ingress-controllers/) + +### 配置 Helm 仓库地址 +```bash +# 请将 `` 替换为 Chart 所在的 Helm 仓库地址 +helm repo add bk-paas3 `` +helm repo update +``` + +### 其他服务 +由于蓝鲸用户管理 SaaS 是需要校验用户身份的服务,所以在能够正常访问前,请确认以下服务已就绪: + +- [蓝鲸登录](https://github.com/Tencent/bk-PaaS/tree/master/paas-ce/paas/login) +- [蓝鲸权限中心](https://github.com/TencentBlueKing/bk-iam) + + +## 快速安装 + +### 准备 `values.yaml` + +#### 1. 获取蓝鲸平台访问地址 +首先,你需要获取到蓝鲸平台的访问地址,例如 `https://paas.example.com`,确保 `https://paas.example.com/login` 可以访问蓝鲸登录,然后将该值的内容填入全局环境变量中。 + +配置示例: +```yaml +global: + bkDomain: "example.com" + bkDomainScheme: "http" + +api: + enabeld: true + bkIamUrl: "http://bkiam.example.com" + bkPaasUrl: "http://paas.example.com" + bkComponentApiUrl: "http://bkapi.example.com" + +saas: + enabled: true + bkUserAddr: bkuser.example.com + bkIamUrl: "http://bkiam.example.com" + bkPaasUrl: "http://paas.example.com" + bkComponentApiUrl: "http://bkapi.example.comm" + +login: + enabled: true + bkPaas3Addr: "paas.example.com" +``` + +#### 2. 确定应用鉴权信息 + +需要以下 3 类鉴权信息: +- 用户管理应用 code (bk_user) 对应的 bk_app_secret +- 统一登录服务: bk_paas 对应的 bk_app_secret +- 统一登录服务: 32位随机字符串,用于加密登录态票据(bk_token) + +你需要为用户管理提供一个访问根域,类似 `example.com`,配置示例: +```yaml +api: + appCode: "bk_usermgr" + appSecret: "some-app-secret" + +saas: + appCode: "bk_usermgr" + appSecret: "some-app-secret" + +login: + # bk_paas 对应的 bk_app_secret 信息 + bkPaasSerectKey: "enter-paas-secret-key" + # 32位随机字符串,用于加密登录态票据(bk_token) + # tr -dc A-Za-z0-9 注意这里同时也会修改 bitnami 内建存储的 Registry + +#### 4. 数据库依赖 + +我们为**功能快速验证**提供了内嵌的 `mariadb` 组件,但我们并不保证该数据库的高可用性,所以***不建议在生产环境中直接使用***。 + +如果你没有数据库方面的特殊要求,那么不需要关注以下 `mariadb` 的默认配置。 + +```yaml +mariadb: + enabled: true + architecture: standalone + auth: + rootPassword: "root" + username: "bk-user" + password: "root" + primary: + # 默认我们未开启持久化,如有需求可以参考: https://kubernetes.io/docs/user-guide/persistent-volumes/ + persistence: + enabled: false + initdbScriptsConfigMap: "bk-user-mariadb-init +``` + +如果你想要在生产环境中使用其他外部数据库,那么可以通过环境变量来指定,并禁用 `mariadb`,配置示例: + +```yaml +api: + externalDatabase: + default: + host: "" + password: "" + port: 3306 + user: "" + name: "bk_user_api" + +saas: + externalDatabase: + default: + host: "" + password: "" + port: 3306 + user: "" + name: "bk_user_saas" + +login: + externalDatabase: + default: + host: "" + password: "" + port: 3306 + user: "" + name: "bk_login" + +mariadb: + enabled: false + +redis: + enabled: false +``` + +#### 5. 权限中心 +默认地,我们已开启权限中心,如果功能验证时想跳过权限中心,可以手动关闭 +```yaml +global: + enableIAM: false +``` + +#### 6. 账号密码 +我们需要为 `admin` 账户添加用户名密码,虽然我们给定了默认值,但是为了安全,请手动修改: +```yaml +api: + initialAdminUsername: "admin" + initialAdminPassword: "Blueking@2019" +``` + +### 7. 蓝鲸日志采集配置 + +用于将容器日志和标准输出日志采集到蓝鲸日志平台。默认未开启,如需开启请将 `global.bkLogConfig.enabled` 设置为 true。 + +##### `values.yaml` 配置示例: +```yaml +global: + bkLogConfig: + enabled: true + dataId: 1 +``` + +### 8. 容器监控 Service Monitor + +默认未开启,如需开启请将 `global.serviceMonitor.enabled` 设置为 true。 + +##### `values.yaml` 配置示例: +```yaml +global: + serviceMonitor: + enabled: true +``` + +### 9. 安装 + +如果你已经准备好了 `values.yaml`,就可以直接进行安装操作了 + +```bash +# 假定你想在 bk-user 命名空间安装 +kubectl create namespace bk-user +helm install bk-user bk-user -n bk-user -f values.yaml +``` + + +如果在安装完成之后,访问 SaaS 地址出现 `503`,可以检查一下 `saas-web` 容器是否完全就绪,静候就绪后刷新页面即可。 + +## 资源释义 +你可以通过 kubectl 获取安装详情: +```bash +# 获取所有 controller +kubectl get deploy,job,sts -l app.kubernetes.io/instance=bk-user +# 获取所有 Pod +kubectl get pod -l app.kubernetes.io/instance=bk-user +# 获取访问入口 +kubectl get svc,ingress -l app.kubernetes.io/instance=bk-user +``` + +通常在安装后,我们会看到这些 Pod + +| Pod 前缀 | 所属模块 | 作用 | +|-------------------------|-----------|-------------| +| bk-login-web | 蓝鲸登录 | 主进程 | +| bk-login-migrate-db | 蓝鲸登录 | 初始化数据库作业 | +| bk-user-saas | 用户管理 SaaS | 主进程 | +| bk-user-saas-migrate-db | 用户管理 SaaS | 初始化数据库作业 | +| bk-user-api-web | 用户管理 API | 主进程 | +| bk-user-api-worker | 用户管理 API | 后台任务进程 | +| bk-user-api-beat | 用户管理 API | 周期任务 | +| bk-user-api-migrate-db | 用户管理 API | 初始化数据库作业 | +| bk-user-api-migrate-db | 用户管理 API | 初始化数据库作业 | +| bk-user-api-migrate-iam | 用户管理 API | 初始化权限中心模型作业 | + +## 卸载 +```bash +helm uninstall bk-user -n bk-user +``` diff --git a/deploy/helm/bk-user-stack/.helmignore b/deploy/helm/bk-user/charts/api/.helmignore similarity index 100% rename from deploy/helm/bk-user-stack/.helmignore rename to deploy/helm/bk-user/charts/api/.helmignore diff --git a/deploy/helm/bk-user/charts/api/Chart.yaml b/deploy/helm/bk-user/charts/api/Chart.yaml new file mode 100644 index 000000000..f903872b4 --- /dev/null +++ b/deploy/helm/bk-user/charts/api/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +name: api +description: Api module for bk-user +type: application +version: 1.0.0 +appVersion: "2.3.2" diff --git a/deploy/helm/bk-user/charts/api/templates/NOTES.txt b/deploy/helm/bk-user/charts/api/templates/NOTES.txt new file mode 100644 index 000000000..3dadaeedf --- /dev/null +++ b/deploy/helm/bk-user/charts/api/templates/NOTES.txt @@ -0,0 +1,22 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "bk-user.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "bk-user.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "bk-user.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "bk-user.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} diff --git a/deploy/helm/bk-user/charts/api/templates/_helpers.tpl b/deploy/helm/bk-user/charts/api/templates/_helpers.tpl new file mode 100644 index 000000000..645b58786 --- /dev/null +++ b/deploy/helm/bk-user/charts/api/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "bk-user.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "bk-user.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "bk-user.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "bk-user.labels" -}} +helm.sh/chart: {{ include "bk-user.chart" . }} +{{ include "bk-user.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "bk-user.selectorLabels" -}} +app.kubernetes.io/name: {{ include "bk-user.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "bk-user.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "bk-user.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/deploy/helm/bk-user/charts/api/templates/_storage.tpl b/deploy/helm/bk-user/charts/api/templates/_storage.tpl new file mode 100644 index 000000000..8db2eb4dc --- /dev/null +++ b/deploy/helm/bk-user/charts/api/templates/_storage.tpl @@ -0,0 +1,6 @@ +{{/* +Shortcuts for redis +*/}} +{{- define "bk-user.externalRedisBrokerUrl" -}} +{{- printf "redis://:%s@%s:%s/0" .Values.externalRedis.default.password .Values.externalRedis.default.host (.Values.externalRedis.default.port | toString )}} +{{- end }} \ No newline at end of file diff --git a/deploy/helm/bk-user/charts/api/templates/beat-deployment.yaml b/deploy/helm/bk-user/charts/api/templates/beat-deployment.yaml new file mode 100644 index 000000000..2396023a3 --- /dev/null +++ b/deploy/helm/bk-user/charts/api/templates/beat-deployment.yaml @@ -0,0 +1,80 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: bk-user-api-beat + labels: + {{- include "bk-user.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "bk-user.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "bk-user.selectorLabels" . | nindent 8 }} + spec: + {{- with .Values.global.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: bk-user-api + {{- with .Values.global.hostAliases }} + hostAliases: + {{- toYaml . | nindent 8 }} + {{- end }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + initContainers: + - name: check-migrate-db + image: "{{ .Values.global.imageRegistry | default .Values.migration.images.k8sWaitFor.registry }}/{{ .Values.migration.images.k8sWaitFor.repository }}:{{ .Values.migration.images.k8sWaitFor.tag }}" + imagePullPolicy: IfNotPresent + args: + - job + - "bk-user-api-migrate-db-{{ .Release.Revision }}" + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.global.imageRegistry | default .Values.image.registry }}/{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + command: + - bash + args: + - /app/start_beat.sh + {{- with .Values.volumeMounts }} + volumeMounts: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.env }} + env: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.envFrom }} + envFrom: + {{- toYaml . | nindent 12 }} + {{- end }} + resources: + {{- toYaml .Values.resources.beat | nindent 12 }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.volumes }} + volumes: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/deploy/helm/bk-user/charts/api/templates/bklogconfig.yaml b/deploy/helm/bk-user/charts/api/templates/bklogconfig.yaml new file mode 100644 index 000000000..41f170432 --- /dev/null +++ b/deploy/helm/bk-user/charts/api/templates/bklogconfig.yaml @@ -0,0 +1,15 @@ +{{- $namePrefix := include "bk-user.name" . -}} +{{- if .Values.global.bkLogConfig.enabled }} +apiVersion: bk.tencent.com/v1alpha1 +kind: BkLogConfig +metadata: + name: bk-user-api-stdout-log + labels: + {{- include "bk-user.labels" . | nindent 4 }} +spec: + dataId: {{ .Values.global.bkLogConfig.dataId }} + logConfigType: "std_log_config" + namespace: {{ .Release.Namespace | quote }} + labelSelector: + matchLabels: {{- include "bk-user.labels" . | nindent 6 }} +{{- end }} \ No newline at end of file diff --git a/deploy/helm/bk-user/charts/api/templates/external-storage-configmap.yaml b/deploy/helm/bk-user/charts/api/templates/external-storage-configmap.yaml new file mode 100644 index 000000000..8dcc6166c --- /dev/null +++ b/deploy/helm/bk-user/charts/api/templates/external-storage-configmap.yaml @@ -0,0 +1,19 @@ +{{- $namePrefix := include "bk-user.name" . -}} +apiVersion: v1 +kind: ConfigMap +metadata: + name: bk-user-api-external-storage +data: + # --------------- + # 数据库 + # --------------- + DB_NAME: "{{ .Values.externalDatabase.default.name | default .Values.preferDBName }}" + DB_USER: "{{ .Values.externalDatabase.default.user }}" + DB_PASSWORD: "{{ .Values.externalDatabase.default.password }}" + DB_HOST: "{{ .Values.externalDatabase.default.host }}" + DB_PORT: "{{ .Values.externalDatabase.default.port }}" + # --------------- + # Redis Related + # --------------- + CELERY_BROKER_URL: {{ .Values.celeryBrokerUrl | default (include "bk-user.externalRedisBrokerUrl" .) }} + CELERY_RESULT_BACKEND: {{ .Values.celeryResultBackend | default (include "bk-user.externalRedisBrokerUrl" .) }} 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 new file mode 100644 index 000000000..ec929c447 --- /dev/null +++ b/deploy/helm/bk-user/charts/api/templates/general-envs-configmap.yaml @@ -0,0 +1,32 @@ +{{- $namePrefix := include "bk-user.name" . -}} +apiVersion: v1 +kind: ConfigMap +metadata: + name: bk-user-api-general-envs +data: + # ------------- + # 默认配置,不了解详情时请不要修改 + # ------------- + BK_APP_CODE: "{{ .Values.appCode }}" + BK_APP_SECRET: "{{ .Values.appSecret }}" + DJANGO_SETTINGS_MODULE: "bkuser_core.config.overlays.prod" + # ------------- + # 权限中心相关配置 + # ------------- + BK_IAM_SYSTEM_ID: "bk_usermgr" + # 权限中心后台访问地址 + BK_IAM_V3_INNER_HOST: "{{ .Values.bkIamApiUrl }}" + # 默认我们会按照 BK_PAAS_URL/o/bk_iam 拼接权限中心 SaaS 访问地址,可以通过以下值覆盖 + BK_IAM_SAAS_HOST: "{{ .Values.bkIamUrl }}" + # !!!安全:请修改初始账号密码!!! + INITIAL_ADMIN_USERNAME: "{{ .Values.initialAdminUsername }}" + INITIAL_ADMIN_PASSWORD: "{{ .Values.initialAdminPassword }}" + BK_USER_SAAS_URL: "{{ .Values.global.bkDomainScheme }}://{{ .Values.bkUserAddr }}" + # 使容器可以自我感知访问地址 + BK_USER_API_URL: "{{ .Values.bkUserApiUrl }}" + # PaaS 平台访问地址 + BK_PAAS_URL: "{{ .Values.bkPaasUrl }}" + # ESB Api 访问地址 + BK_COMPONENT_API_URL: "{{ .Values.bkComponentApiUrl }}" + # 由于用户管理先于权限中心拉起,所以默认禁用,后期所有产品就绪后,可手动开启 + ENABLE_IAM: "{{ .Values.global.enableIAM }}" \ No newline at end of file diff --git a/deploy/helm/bk-user/charts/api/templates/hpa.yaml b/deploy/helm/bk-user/charts/api/templates/hpa.yaml new file mode 100644 index 000000000..26edac84e --- /dev/null +++ b/deploy/helm/bk-user/charts/api/templates/hpa.yaml @@ -0,0 +1,28 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2beta1 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "bk-user.fullname" . }} + labels: + {{- include "bk-user.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "bk-user.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/deploy/helm/bk-user/charts/api/templates/ingress.yaml b/deploy/helm/bk-user/charts/api/templates/ingress.yaml new file mode 100644 index 000000000..2bbf273af --- /dev/null +++ b/deploy/helm/bk-user/charts/api/templates/ingress.yaml @@ -0,0 +1,61 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "bk-user.fullname" . -}} +{{- $svcPort := .Values.service.port -}} +{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} + {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} + {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} + {{- end }} +{{- end }} +{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1 +{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1beta1 +{{- else -}} +apiVersion: extensions/v1beta1 +{{- end }} +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + {{- include "bk-user.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} + pathType: {{ .pathType }} + {{- end }} + backend: + {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} + service: + name: bkuserapi-web + port: + number: {{ $svcPort }} + {{- else }} + serviceName: bkuserapi-web + servicePort: {{ $svcPort }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} diff --git a/deploy/helm/bk-user/charts/api/templates/migrate-iam.yaml b/deploy/helm/bk-user/charts/api/templates/migrate-iam.yaml new file mode 100644 index 000000000..5e60c8a13 --- /dev/null +++ b/deploy/helm/bk-user/charts/api/templates/migrate-iam.yaml @@ -0,0 +1,50 @@ +{{- if .Values.global.enableIAM }} +apiVersion: batch/v1 +kind: Job +metadata: + name: bk-user-api-migrate-iam-{{ .Release.Revision }} + labels: + {{- include "bk-user.labels" . | nindent 4 }} +spec: + backoffLimit: 10 + template: + metadata: + labels: + {{- include "bk-user.labels" . | nindent 8 }} + spec: + {{- with .Values.global.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: bk-user-api + restartPolicy: OnFailure + {{- with .Values.global.hostAliases }} + hostAliases: + {{- toYaml . | nindent 8 }} + {{- end }} + initContainers: + - name: check-migrate-db + image: "{{ .Values.global.imageRegistry | default .Values.migration.images.k8sWaitFor.registry }}/{{ .Values.migration.images.k8sWaitFor.repository }}:{{ .Values.migration.images.k8sWaitFor.tag }}" + imagePullPolicy: IfNotPresent + args: + - job + - "bk-user-api-migrate-db-{{ .Release.Revision }}" + containers: + - name: api-db-migrate + image: "{{ .Values.global.imageRegistry | default .Values.image.registry }}/{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + command: + - bash + args: + - /app/migrate_iam.sh + {{- with .Values.env }} + env: + {{- toYaml . | nindent 12}} + {{- end }} + {{- with .Values.envFrom }} + envFrom: + {{- toYaml . | nindent 12}} + {{- end }} + resources: + {{- toYaml .Values.resources.web | nindent 12 }} +{{- end }} \ No newline at end of file diff --git a/deploy/helm/bk-user/charts/api/templates/migrate-job.yaml b/deploy/helm/bk-user/charts/api/templates/migrate-job.yaml new file mode 100644 index 000000000..e437b8bf3 --- /dev/null +++ b/deploy/helm/bk-user/charts/api/templates/migrate-job.yaml @@ -0,0 +1,60 @@ +apiVersion: batch/v1 +kind: Job +metadata: + name: bk-user-api-migrate-db-{{ .Release.Revision }} + labels: + {{- include "bk-user.labels" . | nindent 4 }} +spec: + backoffLimit: 10 + template: + metadata: + labels: + {{- include "bk-user.labels" . | nindent 8 }} + spec: + {{- with .Values.global.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + restartPolicy: OnFailure + {{- with .Values.global.hostAliases }} + hostAliases: + {{- toYaml . | nindent 8 }} + {{- end }} + initContainers: + - name: check-database-ready + image: "{{ .Values.global.imageRegistry | default .Values.migration.images.busybox.registry }}/{{ .Values.migration.images.busybox.repository }}:{{ .Values.migration.images.busybox.tag }}" + imagePullPolicy: IfNotPresent + command: + - sh + - -c + args: + - "echo Start check database: $(DB_HOST):$(DB_PORT); until telnet $(DB_HOST) $(DB_PORT); do echo waiting for db $(DB_NAME); sleep 2; done;" + envFrom: + {{- toYaml .Values.envFrom | nindent 12 }} + {{- with .Values.volumes }} + volumes: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: api-db-migrate + image: "{{ .Values.global.imageRegistry | default .Values.image.registry }}/{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + command: + - /bin/bash + - -c + args: + - python manage.py migrate --no-input + {{- with .Values.volumeMounts }} + volumeMounts: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.env }} + env: + {{- toYaml . | nindent 12}} + {{- end }} + {{- with .Values.envFrom }} + envFrom: + {{- toYaml . | nindent 12}} + {{- end }} + resources: + {{- toYaml .Values.resources.web | nindent 12 }} diff --git a/deploy/helm/bk-user/charts/api/templates/service.yaml b/deploy/helm/bk-user/charts/api/templates/service.yaml new file mode 100644 index 000000000..ad7a7ca94 --- /dev/null +++ b/deploy/helm/bk-user/charts/api/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: bkuserapi-web + labels: + {{- include "bk-user.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "bk-user.selectorLabels" . | nindent 4 }} diff --git a/deploy/helm/bk-user/charts/api/templates/serviceaccount.yaml b/deploy/helm/bk-user/charts/api/templates/serviceaccount.yaml new file mode 100644 index 000000000..df874bffe --- /dev/null +++ b/deploy/helm/bk-user/charts/api/templates/serviceaccount.yaml @@ -0,0 +1,39 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: bk-user-api + labels: + {{- include "bk-user.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: bk-user-api-role +rules: +- apiGroups: + - batch + resources: + - jobs + verbs: + - get + - list + - watch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: bk-user-api-role-binding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: bk-user-api-role +subjects: +- kind: ServiceAccount + name: bk-user-api + namespace: {{ default "default" .Release.Namespace }} +{{- end }} \ No newline at end of file diff --git a/deploy/helm/bk-user/charts/api/templates/servicemonitor.yaml b/deploy/helm/bk-user/charts/api/templates/servicemonitor.yaml new file mode 100644 index 000000000..ab1a4722b --- /dev/null +++ b/deploy/helm/bk-user/charts/api/templates/servicemonitor.yaml @@ -0,0 +1,31 @@ +{{- if .Values.global.serviceMonitor.enabled }} +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: {{ template "bk-user.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "bk-user.labels" . | nindent 4 }} +spec: + {{- if .Values.global.serviceMonitor.jobLabel }} + jobLabel: {{ .Values.global.serviceMonitor.jobLabel }} + {{- end }} + selector: + matchLabels: + {{- include "bk-user.selectorLabels" . | nindent 6 }} + endpoints: + - port: http + path: "/metrics" + {{- if .Values.global.serviceMonitor.interval }} + interval: {{ .Values.global.serviceMonitor.interval }} + {{- end }} + {{- if .Values.global.serviceMonitor.scrapeTimeout }} + scrapeTimeout: {{ .Values.global.serviceMonitor.scrapeTimeout }} + {{- end }} + {{- if hasKey .Values.global.serviceMonitor "honorLabels" }} + honorLabels: {{ .Values.global.serviceMonitor.honorLabels }} + {{- end }} + namespaceSelector: + matchNames: + - {{ .Release.Namespace }} +{{- end }} \ No newline at end of file diff --git a/deploy/helm/bk-user/charts/api/templates/web-deployment.yaml b/deploy/helm/bk-user/charts/api/templates/web-deployment.yaml new file mode 100644 index 000000000..9b34ec70e --- /dev/null +++ b/deploy/helm/bk-user/charts/api/templates/web-deployment.yaml @@ -0,0 +1,92 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: bk-user-api-web + labels: + {{- include "bk-user.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "bk-user.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "bk-user.selectorLabels" . | nindent 8 }} + spec: + {{- with .Values.global.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: bk-user-api + {{- with .Values.global.hostAliases }} + hostAliases: + {{- toYaml . | nindent 8 }} + {{- end }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + initContainers: + - name: check-migrate-db + image: "{{ .Values.global.imageRegistry | default .Values.migration.images.k8sWaitFor.registry }}/{{ .Values.migration.images.k8sWaitFor.repository }}:{{ .Values.migration.images.k8sWaitFor.tag }}" + imagePullPolicy: IfNotPresent + args: + - job + - "bk-user-api-migrate-db-{{ .Release.Revision }}" + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.global.imageRegistry | default .Values.image.registry }}/{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + command: + - bash + args: + - /app/start.sh + {{- with .Values.volumeMounts }} + volumeMounts: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.env }} + env: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.envFrom }} + envFrom: + {{- toYaml . | nindent 12 }} + {{- end }} + ports: + - name: http + containerPort: 8000 + protocol: TCP + livenessProbe: + httpGet: + path: /ping + port: http + readinessProbe: + httpGet: + path: /ping + port: http + resources: + {{- toYaml .Values.resources.web | nindent 12 }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.volumes }} + volumes: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/deploy/helm/bk-user/charts/api/templates/worker-deployment.yaml b/deploy/helm/bk-user/charts/api/templates/worker-deployment.yaml new file mode 100644 index 000000000..16a0a174b --- /dev/null +++ b/deploy/helm/bk-user/charts/api/templates/worker-deployment.yaml @@ -0,0 +1,80 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: bk-user-api-worker + labels: + {{- include "bk-user.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "bk-user.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "bk-user.selectorLabels" . | nindent 8 }} + spec: + {{- with .Values.global.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: bk-user-api + {{- with .Values.global.hostAliases }} + hostAliases: + {{- toYaml . | nindent 8 }} + {{- end }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + initContainers: + - name: check-migrate-db + image: "{{ .Values.global.imageRegistry | default .Values.migration.images.k8sWaitFor.registry }}/{{ .Values.migration.images.k8sWaitFor.repository }}:{{ .Values.migration.images.k8sWaitFor.tag }}" + imagePullPolicy: IfNotPresent + args: + - job + - "bk-user-api-migrate-db-{{ .Release.Revision }}" + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.global.imageRegistry | default .Values.image.registry }}/{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + command: + - bash + args: + - /app/start_celery.sh + {{- with .Values.volumeMounts }} + volumeMounts: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.env }} + env: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.envFrom }} + envFrom: + {{- toYaml . | nindent 12 }} + {{- end }} + resources: + {{- toYaml .Values.resources.worker | nindent 12 }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.volumes }} + volumes: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/deploy/helm/bk-user/charts/api/values.yaml b/deploy/helm/bk-user/charts/api/values.yaml new file mode 100644 index 000000000..03722d8ae --- /dev/null +++ b/deploy/helm/bk-user/charts/api/values.yaml @@ -0,0 +1,256 @@ +global: + imageRegistry: "" + ## E.g. + ## imagePullSecrets: + ## - myRegistryKeySecretName + imagePullSecrets: [] + storageClass: "" + + ## 蓝鲸产品统一根域 + bkDomain: "example.com" + bkDomainScheme: "http" + ## 是否开启权限中心 + enableIAM: true + + hostAliases: [] + # - ip: "" + # hostnames: + # - "" + + ## -------------- + ## 蓝鲸监控 + ## -------------- + serviceMonitor: + ## @param serviceMonitor.enabled Creates a ServiceMonitor to monitor kube-state-metrics + ## + enabled: false + ## @param serviceMonitor.jobLabel The name of the label on the target service to use as the job name in prometheus. + ## + jobLabel: "" + ## @param serviceMonitor.interval Scrape interval (use by default, falling back to Prometheus' default) + ## ref: https://github.com/coreos/prometheus-operator/blob/master/Documentation/api.md#endpoint + ## e.g: + ## interval: 10s + ## + interval: "" + ## @param serviceMonitor.scrapeTimeout Timeout after which the scrape is ended + ## ref: https://github.com/coreos/prometheus-operator/blob/master/Documentation/api.md#endpoint + ## e.g: + ## scrapeTimeout: 10s + ## + scrapeTimeout: "" + ## @param serviceMonitor.selector ServiceMonitor selector labels + ## ref: https://github.com/bitnami/charts/tree/master/bitnami/prometheus-operator#prometheus-configuration + ## e.g: + ## selector: + ## prometheus: my-prometheus + ## + selector: {} + ## @param serviceMonitor.honorLabels Honor metrics labels + ## ref: https://github.com/coreos/prometheus-operator/blob/master/Documentation/api.md#endpoint + ## e.g: + ## honorLabels: false + ## + honorLabels: false + ## @param serviceMonitor.relabelings ServiceMonitor relabelings + ## ref: https://github.com/coreos/prometheus-operator/blob/master/Documentation/api.md#relabelconfig + ## + relabelings: [] + ## @param serviceMonitor.metricRelabelings ServiceMonitor metricRelabelings + ## ref: https://github.com/coreos/prometheus-operator/blob/master/Documentation/api.md#relabelconfig + ## + metricRelabelings: [] + + ## -------------- + ## 蓝鲸日志采集 + ## -------------- + bkLogConfig: + enabled: false + dataId: 1 + +## web deployment 副本数 +replicaCount: 1 +## celery deployment 副本数 +celeryReplicaCount: 1 +## 自定义 Celery Broker 地址, 未定义时则默认拼接 redis 逻辑 +celeryBrokerUrl: "" +celeryResultBackend: "" + +appCode: "bk_usermgr" +appSecret: "" + +## !!!安全:请修改初始账号密码!!! +initialAdminUsername: "admin" +initialAdminPassword: "Blueking@2019" + +image: + registry: mirrors.tencent.com + repository: blueking/bk-user-api + pullPolicy: IfNotPresent + tag: "v2.3.2" + +nameOverride: "" +fullnameOverride: "" + +serviceAccount: + ## Specifies whether a service account should be created + create: true + ## Annotations to add to the service account + annotations: {} + ## The name of the service account to use. + ## If not set and create is true, a name is generated using the fullname template + name: "" + +## 蓝鲸 PaaS url(浏览器访问蓝鲸入口) +bkPaasUrl: http://paas.example.com +## 蓝鲸 ESB/APIGATEWAY url,注意集群内外都是统一域名。集群内可以配置域名解析到内网ip +bkComponentApiUrl: http://bkapi.example.com +## 蓝鲸 Login url(浏览器跳转登录用的URL前缀) +bkLoginUrl: http://paas.example.com/login/ +## 蓝鲸登录后台的内部服务地址(一般用于校验登录token) +bkLoginApiUrl: http://bk-login-web +## 蓝鲸用户管理 SaaS地址 +bkUserAddr: bkuser.example.com +## 蓝鲸用户管理后台 API 地址 +bkUserApiUrl: http://bkuserapi-web +## 蓝鲸权限中心 SaaS 地址 +bkIamUrl: http://bkiam.example.com +## 蓝鲸权限中心后台 API 地址 +bkIamApiUrl: http://bkiam-web + + +## --------------- +## 环境变量 +## --------------- +## 请按照原生 env 格式填写 +## env 优先级高于 envFrom,你可以用它来覆盖内置环境变量 +# env: +# - name: "FOO" +# value: "BAR" + +envFrom: + - configMapRef: + name: bk-user-api-general-envs + - configMapRef: + name: bk-user-api-external-storage + +podAnnotations: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +service: + type: ClusterIP + port: 80 + +ingress: + ## 由于默认 API 没有开启任何鉴权,为了保证数据安全关闭 ingress + enabled: false + className: "" + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: bk-user-api.{{ .Values.global.bkDomain }} + paths: + - path: / + pathType: ImplementationSpecific + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +resources: + web: + requests: + cpu: "100m" + memory: "128Mi" + limits: + cpu: "1024m" + memory: "2048Mi" + worker: + requests: + cpu: "100m" + memory: "128Mi" + limits: + cpu: "1024m" + memory: "2048Mi" + beat: + requests: + cpu: "100m" + memory: "128Mi" + limits: + cpu: "1024m" + memory: "1024Mi" + +## --------------- +## 调度 +## --------------- +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +nodeSelector: {} + +tolerations: [] + +affinity: {} + +## -------------- +## 外部数据库配置 +## -------------- +preferDBName: "bk_user_api" +externalDatabase: + default: + host: "" + password: "" + port: 3306 + user: "" + name: "bk_user_api" + +## --------------- +## 外部 Redis +## --------------- +externalRedis: + default: + host: "" + port: 6379 + password: "blueking" + +## -------------- +## 检查 DB 变更 +## -------------- +migration: + images: + busybox: + registry: "mirrors.tencent.com" + repository: blueking/busybox + tag: "1.34.0" + k8sWaitFor: + registry: "mirrors.tencent.com" + repository: blueking/k8s-wait-for + tag: "v1.5.1" + +volumes: [] + ## 自定义插件示例 + # - name: bk-user-plugin-example + # configMap: + # name: bk-user-plugin-example + +volumeMounts: [] + ## 自定义插件示例 + # - name: bk-user-plugin-example + # mountPath: /app/bkuser_core/categories/plugins/example + + diff --git a/deploy/helm/saas/.helmignore b/deploy/helm/bk-user/charts/login/.helmignore similarity index 100% rename from deploy/helm/saas/.helmignore rename to deploy/helm/bk-user/charts/login/.helmignore diff --git a/deploy/helm/bk-user/charts/login/Chart.yaml b/deploy/helm/bk-user/charts/login/Chart.yaml new file mode 100644 index 000000000..8cbfa051d --- /dev/null +++ b/deploy/helm/bk-user/charts/login/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +name: login +description: login module for blueking +type: application +version: 1.0.0 +appVersion: "2.3.2" diff --git a/deploy/helm/bk-user/charts/login/templates/NOTES.txt b/deploy/helm/bk-user/charts/login/templates/NOTES.txt new file mode 100644 index 000000000..3dadaeedf --- /dev/null +++ b/deploy/helm/bk-user/charts/login/templates/NOTES.txt @@ -0,0 +1,22 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "bk-user.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "bk-user.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "bk-user.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "bk-user.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} diff --git a/deploy/helm/bk-user/charts/login/templates/_helpers.tpl b/deploy/helm/bk-user/charts/login/templates/_helpers.tpl new file mode 100644 index 000000000..645b58786 --- /dev/null +++ b/deploy/helm/bk-user/charts/login/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "bk-user.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "bk-user.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "bk-user.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "bk-user.labels" -}} +helm.sh/chart: {{ include "bk-user.chart" . }} +{{ include "bk-user.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "bk-user.selectorLabels" -}} +app.kubernetes.io/name: {{ include "bk-user.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "bk-user.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "bk-user.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/deploy/helm/bk-user/charts/login/templates/bklogconfig.yaml b/deploy/helm/bk-user/charts/login/templates/bklogconfig.yaml new file mode 100644 index 000000000..da4b98924 --- /dev/null +++ b/deploy/helm/bk-user/charts/login/templates/bklogconfig.yaml @@ -0,0 +1,15 @@ +{{- $namePrefix := include "bk-user.name" . -}} +{{- if .Values.global.bkLogConfig.enabled }} +apiVersion: bk.tencent.com/v1alpha1 +kind: BkLogConfig +metadata: + name: bk-login-stdout-log + labels: + {{- include "bk-user.labels" . | nindent 4 }} +spec: + dataId: {{ .Values.global.bkLogConfig.dataId }} + logConfigType: "std_log_config" + namespace: {{ .Release.Namespace | quote }} + labelSelector: + matchLabels: {{- include "bk-user.labels" . | nindent 6 }} +{{- end }} \ No newline at end of file diff --git a/deploy/helm/bk-user/charts/login/templates/deployment.yaml b/deploy/helm/bk-user/charts/login/templates/deployment.yaml new file mode 100644 index 000000000..6185f61a2 --- /dev/null +++ b/deploy/helm/bk-user/charts/login/templates/deployment.yaml @@ -0,0 +1,87 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: bk-login-web + labels: + {{- include "bk-user.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "bk-user.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "bk-user.selectorLabels" . | nindent 8 }} + spec: + {{- with .Values.global.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: bk-login + {{- with .Values.global.hostAliases }} + hostAliases: + {{- toYaml . | nindent 8 }} + {{- end }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + initContainers: + - name: check-migrate-db + image: "{{ .Values.global.imageRegistry | default .Values.migration.images.k8sWaitFor.registry }}/{{ .Values.migration.images.k8sWaitFor.repository }}:{{ .Values.migration.images.k8sWaitFor.tag }}" + imagePullPolicy: IfNotPresent + args: + - job + - "bk-login-migrate-db-{{ .Release.Revision }}" + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.global.imageRegistry | default .Values.image.registry }}/{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + command: ["./start.sh"] + {{- with .Values.volumeMounts }} + volumeMounts: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.env }} + env: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.envFrom }} + envFrom: + {{- toYaml . | nindent 12 }} + {{- end }} + ports: + - name: http + containerPort: 5000 + protocol: TCP + livenessProbe: + tcpSocket: + port: http + readinessProbe: + tcpSocket: + port: http + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.volumes }} + volumes: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/deploy/helm/bk-user/charts/login/templates/external-storage-configmap.yaml b/deploy/helm/bk-user/charts/login/templates/external-storage-configmap.yaml new file mode 100644 index 000000000..7f1b42a6b --- /dev/null +++ b/deploy/helm/bk-user/charts/login/templates/external-storage-configmap.yaml @@ -0,0 +1,14 @@ +{{- $namePrefix := include "bk-user.name" . -}} +apiVersion: v1 +kind: ConfigMap +metadata: + name: bk-login-external-storage +data: + # --------------- + # 数据库 + # --------------- + DB_NAME: "{{ .Values.externalDatabase.default.name | default .Values.preferDBName }}" + DB_USER: "{{ .Values.externalDatabase.default.user }}" + DB_PASSWORD: "{{ .Values.externalDatabase.default.password }}" + DB_HOST: "{{ .Values.externalDatabase.default.host }}" + DB_PORT: "{{ .Values.externalDatabase.default.port }}" 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 new file mode 100644 index 000000000..9e5c6351b --- /dev/null +++ b/deploy/helm/bk-user/charts/login/templates/general-envs-configmap.yaml @@ -0,0 +1,20 @@ +{{- $namePrefix := include "bk-user.name" . -}} +apiVersion: v1 +kind: ConfigMap +metadata: + name: bk-login-general-envs +data: + DJANGO_SETTINGS_MODULE: "bklogin.config.overlays.prod" + # 登录态 Cookie 写入的域名 + BK_DOMAIN: {{ .Values.global.bkDomain }} + BK_LOGIN_HTTP_SCHEMA: {{ .Values.global.bkDomainScheme }} + # 统一登录的外部访问域名 + BK_LOGIN_PUBLIC_ADDR: {{ .Values.bkLoginAddr }} + # 用户管理后台API访问地址 + BK_USERMGR_API_URL: {{ .Values.bkUserApiUrl }} + # 与 ESB 的通信凭证,应用(bk_paas) 对应的 bk_app_secret + BK_PAAS_SECRET_KEY: {{ .Values.bkPaasSerectKey }} + # 32位随机字符串,用于加密登录态票据(bk_token) + ENCRYPT_SECRET_KEY: {{ .Values.encryptSecretKey }} + # 用于拼接修改密码 URL + BK_USERMGR_SAAS_URL: {{ .Values.bkUserUrl }} diff --git a/deploy/helm/bk-user/charts/login/templates/hpa.yaml b/deploy/helm/bk-user/charts/login/templates/hpa.yaml new file mode 100644 index 000000000..26edac84e --- /dev/null +++ b/deploy/helm/bk-user/charts/login/templates/hpa.yaml @@ -0,0 +1,28 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2beta1 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "bk-user.fullname" . }} + labels: + {{- include "bk-user.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "bk-user.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/deploy/helm/bk-user/charts/login/templates/ingress.yaml b/deploy/helm/bk-user/charts/login/templates/ingress.yaml new file mode 100644 index 000000000..5324650d4 --- /dev/null +++ b/deploy/helm/bk-user/charts/login/templates/ingress.yaml @@ -0,0 +1,61 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "bk-user.fullname" . -}} +{{- $svcPort := .Values.service.port -}} +{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} + {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} + {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} + {{- end }} +{{- end }} +{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1 +{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1beta1 +{{- else -}} +apiVersion: extensions/v1beta1 +{{- end }} +kind: Ingress +metadata: + name: bk-login + labels: + {{- include "bk-user.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ tpl .host $ | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} + pathType: {{ .pathType }} + {{- end }} + backend: + {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} + service: + name: bk-login-web + port: + number: {{ $svcPort }} + {{- else }} + serviceName: bk-login-web + servicePort: {{ $svcPort }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} diff --git a/deploy/helm/bk-user/charts/login/templates/migrate-job.yaml b/deploy/helm/bk-user/charts/login/templates/migrate-job.yaml new file mode 100644 index 000000000..2623bfb8a --- /dev/null +++ b/deploy/helm/bk-user/charts/login/templates/migrate-job.yaml @@ -0,0 +1,60 @@ +apiVersion: batch/v1 +kind: Job +metadata: + name: bk-login-migrate-db-{{ .Release.Revision }} + labels: + {{- include "bk-user.labels" . | nindent 4 }} +spec: + backoffLimit: 10 + template: + metadata: + labels: + {{- include "bk-user.labels" . | nindent 8 }} + spec: + {{- with .Values.global.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + restartPolicy: OnFailure + {{- with .Values.global.hostAliases }} + hostAliases: + {{- toYaml . | nindent 8 }} + {{- end }} + initContainers: + - name: check-database-ready + image: "{{ .Values.global.imageRegistry | default .Values.migration.images.busybox.registry }}/{{ .Values.migration.images.busybox.repository }}:{{ .Values.migration.images.busybox.tag }}" + imagePullPolicy: IfNotPresent + command: + - sh + - -c + args: + - "echo Start check database: $(DATABASE_HOST):$(DATABASE_PORT); until telnet $(DATABASE_HOST) $(DATABASE_PORT); do echo waiting for db $(DATABASE_NAME); sleep 2; done;" + envFrom: + {{- toYaml .Values.envFrom | nindent 12 }} + {{- with .Values.volumes }} + volumes: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: login-db-migrate + image: "{{ .Values.global.imageRegistry | default .Values.image.registry }}/{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + command: + - /bin/bash + - -c + args: + - python manage.py migrate --no-input + {{- with .Values.volumeMounts }} + volumeMounts: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.env }} + env: + {{- toYaml . | nindent 12}} + {{- end }} + {{- with .Values.envFrom }} + envFrom: + {{- toYaml . | nindent 12}} + {{- end }} + resources: + {{- toYaml .Values.resources | nindent 12 }} diff --git a/deploy/helm/bk-user/charts/login/templates/service.yaml b/deploy/helm/bk-user/charts/login/templates/service.yaml new file mode 100644 index 000000000..931f97462 --- /dev/null +++ b/deploy/helm/bk-user/charts/login/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: bk-login-web + labels: + {{- include "bk-user.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "bk-user.selectorLabels" . | nindent 4 }} diff --git a/deploy/helm/bk-user/charts/login/templates/serviceaccount.yaml b/deploy/helm/bk-user/charts/login/templates/serviceaccount.yaml new file mode 100644 index 000000000..60d3b5f73 --- /dev/null +++ b/deploy/helm/bk-user/charts/login/templates/serviceaccount.yaml @@ -0,0 +1,39 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: bk-login + labels: + {{- include "bk-user.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: bk-login-role +rules: +- apiGroups: + - batch + resources: + - jobs + verbs: + - get + - list + - watch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: bk-login-role-binding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: bk-login-role +subjects: +- kind: ServiceAccount + name: bk-login + namespace: {{ default "default" .Release.Namespace }} +{{- end }} \ No newline at end of file diff --git a/deploy/helm/bk-user/charts/login/templates/servicemonitor.yaml b/deploy/helm/bk-user/charts/login/templates/servicemonitor.yaml new file mode 100644 index 000000000..ab1a4722b --- /dev/null +++ b/deploy/helm/bk-user/charts/login/templates/servicemonitor.yaml @@ -0,0 +1,31 @@ +{{- if .Values.global.serviceMonitor.enabled }} +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: {{ template "bk-user.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "bk-user.labels" . | nindent 4 }} +spec: + {{- if .Values.global.serviceMonitor.jobLabel }} + jobLabel: {{ .Values.global.serviceMonitor.jobLabel }} + {{- end }} + selector: + matchLabels: + {{- include "bk-user.selectorLabels" . | nindent 6 }} + endpoints: + - port: http + path: "/metrics" + {{- if .Values.global.serviceMonitor.interval }} + interval: {{ .Values.global.serviceMonitor.interval }} + {{- end }} + {{- if .Values.global.serviceMonitor.scrapeTimeout }} + scrapeTimeout: {{ .Values.global.serviceMonitor.scrapeTimeout }} + {{- end }} + {{- if hasKey .Values.global.serviceMonitor "honorLabels" }} + honorLabels: {{ .Values.global.serviceMonitor.honorLabels }} + {{- end }} + namespaceSelector: + matchNames: + - {{ .Release.Namespace }} +{{- end }} \ No newline at end of file diff --git a/deploy/helm/bk-user/charts/login/values.yaml b/deploy/helm/bk-user/charts/login/values.yaml new file mode 100644 index 000000000..73478be2c --- /dev/null +++ b/deploy/helm/bk-user/charts/login/values.yaml @@ -0,0 +1,239 @@ +global: + imageRegistry: "" + ## E.g. + ## imagePullSecrets: + ## - myRegistryKeySecretName + imagePullSecrets: [] + storageClass: "" + + ## 蓝鲸产品统一根域 + bkDomain: "example.com" + + hostAliases: [] + # - ip: "" + # hostnames: + # - "" + + ## -------------- + ## 蓝鲸监控 + ## -------------- + serviceMonitor: + ## @param serviceMonitor.enabled Creates a ServiceMonitor to monitor kube-state-metrics + ## + enabled: false + ## @param serviceMonitor.jobLabel The name of the label on the target service to use as the job name in prometheus. + ## + jobLabel: "" + ## @param serviceMonitor.interval Scrape interval (use by default, falling back to Prometheus' default) + ## ref: https://github.com/coreos/prometheus-operator/blob/master/Documentation/api.md#endpoint + ## e.g: + ## interval: 10s + ## + interval: "" + ## @param serviceMonitor.scrapeTimeout Timeout after which the scrape is ended + ## ref: https://github.com/coreos/prometheus-operator/blob/master/Documentation/api.md#endpoint + ## e.g: + ## scrapeTimeout: 10s + ## + scrapeTimeout: "" + ## @param serviceMonitor.selector ServiceMonitor selector labels + ## ref: https://github.com/bitnami/charts/tree/master/bitnami/prometheus-operator#prometheus-configuration + ## e.g: + ## selector: + ## prometheus: my-prometheus + ## + selector: {} + ## @param serviceMonitor.honorLabels Honor metrics labels + ## ref: https://github.com/coreos/prometheus-operator/blob/master/Documentation/api.md#endpoint + ## e.g: + ## honorLabels: false + ## + honorLabels: false + ## @param serviceMonitor.relabelings ServiceMonitor relabelings + ## ref: https://github.com/coreos/prometheus-operator/blob/master/Documentation/api.md#relabelconfig + ## + relabelings: [] + ## @param serviceMonitor.metricRelabelings ServiceMonitor metricRelabelings + ## ref: https://github.com/coreos/prometheus-operator/blob/master/Documentation/api.md#relabelconfig + ## + metricRelabelings: [] + + ## -------------- + ## 蓝鲸日志采集 + ## -------------- + bkLogConfig: + enabled: false + dataId: 1 + +## web deployment 副本数 +replicaCount: 1 +## celery deployment 副本数 +celeryReplicaCount: 1 + +# 与 ESB 的通信凭证,应用(bk_paas) 对应的 bk_app_secret +bkPaasSerectKey: "" +# 32位随机字符串,用于加密登录态票据(bk_token) +encryptSecretKey: "" +bkDomainScheme: "http" + +image: + registry: mirrors.tencent.com + repository: blueking/bk-login + pullPolicy: IfNotPresent + tag: "v2.3.2" + +nameOverride: "" +fullnameOverride: "" + +serviceAccount: + ## Specifies whether a service account should be created + create: true + ## Annotations to add to the service account + annotations: {} + ## The name of the service account to use. + ## If not set and create is true, a name is generated using the fullname template + name: "" + +## 蓝鲸 PaaS url(浏览器访问蓝鲸入口) +bkPaasUrl: http://paas.example.com +## 蓝鲸 ESB/APIGATEWAY url,注意集群内外都是统一域名。集群内可以配置域名解析到内网ip +bkComponentApiUrl: http://bkapi.paas.example.com +## 蓝鲸 Login url(浏览器跳转登录用的URL前缀) +bkLoginUrl: http://paas.example.com/login/ +bkLoginAddr: paas.example.com +## 蓝鲸用户管理 SaaS地址 +bkUserUrl: http://bkuser.paas.example.com +bkUserAddr: bkuser.paas.example.com +## 蓝鲸用户管理后台 API 地址 +bkUserApiUrl: http://bkuserapi-web + +## --------------- +## 环境变量 +## --------------- +## 请按照原生 env 格式填写 +## env 优先级高于 envFrom,你可以用它来覆盖内置环境变量 +# env: +# - name: "FOO" +# value: "BAR" + +envFrom: + - configMapRef: + name: bk-login-general-envs + - configMapRef: + name: bk-login-external-storage + +podAnnotations: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +service: + type: ClusterIP + port: 80 + +ingress: + enabled: true + className: "" + annotations: + kubernetes.io/ingress.class: "nginx" + nginx.ingress.kubernetes.io/rewrite-target: /$2 + hosts: + - host: "{{ .Values.bkLoginAddr }}" + paths: + - path: "/login(/|$)(.*)" + pathType: ImplementationSpecific + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +resources: + limits: + cpu: 200m + memory: 1024Mi + requests: + cpu: 200m + memory: 512Mi + +## --------------- +## 调度 +## --------------- +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +nodeSelector: {} + +tolerations: [] + +affinity: {} + +## -------------- +## 外部数据库配置 +## -------------- +preferDBName: "bk_login" +externalDatabase: + default: + host: "" + password: "" + port: 3306 + user: "" + name: "bk_login" + +## --------------- +## 外部 Redis +## --------------- +externalRedis: + default: + host: "" + port: 6379 + password: "" + +## -------------- +## 检查 DB 变更 +## -------------- +migration: + images: + busybox: + registry: "mirrors.tencent.com" + repository: blueking/busybox + tag: "1.34.0" + k8sWaitFor: + registry: "mirrors.tencent.com" + repository: blueking/k8s-wait-for + tag: "v1.5.1" + +volumes: [] + ## 自定义登录插件示例 + # - name: bk-login-plugin-example + # configMap: + # name: bk-login-plugin-example + # items: + # - key: settings_login.py + # path: settings_login.py + # - key: __init__.py + # path: example/__init__.py + # - key: settings.py + # path: example/settings.py + # - key: utils.py + # path: example/utils.py + # - key: backends.py + # path: example/backends.py + # - key: views.py + # path: example/views.py + +volumeMounts: [] + ## 自定义登录插件示例 + # - name: bk-login-plugin-example + # mountPath: /app/ee_login/ diff --git a/deploy/helm/bk-user/charts/saas/.helmignore b/deploy/helm/bk-user/charts/saas/.helmignore new file mode 100644 index 000000000..0e8a0eb36 --- /dev/null +++ b/deploy/helm/bk-user/charts/saas/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/deploy/helm/bk-user/charts/saas/Chart.yaml b/deploy/helm/bk-user/charts/saas/Chart.yaml new file mode 100644 index 000000000..ad5e99050 --- /dev/null +++ b/deploy/helm/bk-user/charts/saas/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +name: saas +description: SaaS module for bk-user +type: application +version: 1.0.0 +appVersion: "2.3.2" diff --git a/deploy/helm/bk-user/charts/saas/templates/NOTES.txt b/deploy/helm/bk-user/charts/saas/templates/NOTES.txt new file mode 100644 index 000000000..3dadaeedf --- /dev/null +++ b/deploy/helm/bk-user/charts/saas/templates/NOTES.txt @@ -0,0 +1,22 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "bk-user.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "bk-user.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "bk-user.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "bk-user.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} diff --git a/deploy/helm/bk-user/charts/saas/templates/_helpers.tpl b/deploy/helm/bk-user/charts/saas/templates/_helpers.tpl new file mode 100644 index 000000000..645b58786 --- /dev/null +++ b/deploy/helm/bk-user/charts/saas/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "bk-user.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "bk-user.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "bk-user.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "bk-user.labels" -}} +helm.sh/chart: {{ include "bk-user.chart" . }} +{{ include "bk-user.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "bk-user.selectorLabels" -}} +app.kubernetes.io/name: {{ include "bk-user.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "bk-user.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "bk-user.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/deploy/helm/bk-user/charts/saas/templates/bklogconfig.yaml b/deploy/helm/bk-user/charts/saas/templates/bklogconfig.yaml new file mode 100644 index 000000000..98ef234ad --- /dev/null +++ b/deploy/helm/bk-user/charts/saas/templates/bklogconfig.yaml @@ -0,0 +1,15 @@ +{{- $namePrefix := include "bk-user.name" . -}} +{{- if .Values.global.bkLogConfig.enabled }} +apiVersion: bk.tencent.com/v1alpha1 +kind: BkLogConfig +metadata: + name: bk-user-saas-stdout-log + labels: + {{- include "bk-user.labels" . | nindent 4 }} +spec: + dataId: {{ .Values.global.bkLogConfig.dataId }} + logConfigType: "std_log_config" + namespace: {{ .Release.Namespace | quote }} + labelSelector: + matchLabels: {{- include "bk-user.labels" . | nindent 6 }} +{{- end }} \ No newline at end of file diff --git a/deploy/helm/bk-user/charts/saas/templates/deployment.yaml b/deploy/helm/bk-user/charts/saas/templates/deployment.yaml new file mode 100644 index 000000000..53b391cb8 --- /dev/null +++ b/deploy/helm/bk-user/charts/saas/templates/deployment.yaml @@ -0,0 +1,88 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "bk-user.fullname" . }} + labels: + {{- include "bk-user.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "bk-user.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "bk-user.selectorLabels" . | nindent 8 }} + spec: + {{- with .Values.global.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: bk-user-saas + {{- with .Values.global.hostAliases }} + hostAliases: + {{- toYaml . | nindent 8 }} + {{- end }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + initContainers: + - name: check-migrate-db + image: "{{ .Values.global.imageRegistry | default .Values.migration.images.k8sWaitFor.registry }}/{{ .Values.migration.images.k8sWaitFor.repository }}:{{ .Values.migration.images.k8sWaitFor.tag }}" + imagePullPolicy: IfNotPresent + args: + - job + - "bk-user-saas-migrate-db-{{ .Release.Revision }}" + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.global.imageRegistry | default .Values.image.registry }}/{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + {{- with .Values.command }} + command: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.args }} + args: + {{- toYaml . | nindent 12 }} + {{- end }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + {{- with .Values.env }} + env: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.envFrom }} + envFrom: + {{- toYaml . | nindent 12 }} + {{- end }} + ports: + - name: http + containerPort: 8000 + protocol: TCP + livenessProbe: + httpGet: + path: / + port: http + readinessProbe: + httpGet: + path: / + port: http + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/deploy/helm/bk-user/charts/saas/templates/external-storage-configmap.yaml b/deploy/helm/bk-user/charts/saas/templates/external-storage-configmap.yaml new file mode 100644 index 000000000..d06a7438b --- /dev/null +++ b/deploy/helm/bk-user/charts/saas/templates/external-storage-configmap.yaml @@ -0,0 +1,14 @@ +{{- $namePrefix := include "bk-user.name" . -}} +apiVersion: v1 +kind: ConfigMap +metadata: + name: bk-user-saas-external-storage +data: + # --------------- + # 数据库 + # --------------- + DB_NAME: "{{ .Values.externalDatabase.default.name | default .Values.preferDBName }}" + DB_USER: "{{ .Values.externalDatabase.default.user }}" + DB_PASSWORD: "{{ .Values.externalDatabase.default.password }}" + DB_HOST: "{{ .Values.externalDatabase.default.host }}" + DB_PORT: "{{ .Values.externalDatabase.default.port }}" 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 new file mode 100644 index 000000000..76043980e --- /dev/null +++ b/deploy/helm/bk-user/charts/saas/templates/general-envs-configmap.yaml @@ -0,0 +1,30 @@ +{{- $namePrefix := include "bk-user.name" . -}} +apiVersion: v1 +kind: ConfigMap +metadata: + name: bk-user-saas-general-envs +data: + # ------------- + # 默认配置,不了解详情时请不要修改 + # ------------- + BK_APP_CODE: "{{ .Values.appCode }}" + BK_APP_SECRET: "{{ .Values.appSecret }}" + DJANGO_SETTINGS_MODULE: "bkuser_shell.config.overlays.prod" + # ------------- + # 权限中心相关配置 + # ------------- + BK_IAM_SYSTEM_ID: "bk_usermgr" + # 权限中心后台访问地址 + BK_IAM_V3_INNER_HOST: "http://bkiam-web" + # 默认我们会按照 BK_PAAS_URL/o/bk_iam 拼接权限中心 SaaS 访问地址,可以通过以下值覆盖 + # BK_IAM_SAAS_HOST: "http://bkiam.example.com" + BKAPP_BK_USER_CORE_API_HOST: "{{ .Values.bkUserApiUrl }}" + # 容器化版本默认采用子域名形式暴露服务 + BK_LOGIN_API_URL: "{{ .Values.bkLoginApiUrl }}" + SITE_URL: "/" + # PaaS 平台访问地址 + BK_PAAS_URL: "{{ .Values.bkPaasUrl }}" + # ESB Api 访问地址 + BK_COMPONENT_API_URL: "{{ .Values.bkComponentApiUrl }}" + # 由于用户管理先于权限中心拉起,所以默认禁用,后期所有产品就绪后,可手动开启 + ENABLE_IAM: "{{ .Values.global.enableIAM }}" \ No newline at end of file diff --git a/deploy/helm/bk-user/charts/saas/templates/hpa.yaml b/deploy/helm/bk-user/charts/saas/templates/hpa.yaml new file mode 100644 index 000000000..26edac84e --- /dev/null +++ b/deploy/helm/bk-user/charts/saas/templates/hpa.yaml @@ -0,0 +1,28 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2beta1 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "bk-user.fullname" . }} + labels: + {{- include "bk-user.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "bk-user.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/deploy/helm/bk-user/charts/saas/templates/ingress.yaml b/deploy/helm/bk-user/charts/saas/templates/ingress.yaml new file mode 100644 index 000000000..e1de38c9f --- /dev/null +++ b/deploy/helm/bk-user/charts/saas/templates/ingress.yaml @@ -0,0 +1,61 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "bk-user.fullname" . -}} +{{- $svcPort := .Values.service.port -}} +{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} + {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} + {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} + {{- end }} +{{- end }} +{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1 +{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1beta1 +{{- else -}} +apiVersion: extensions/v1beta1 +{{- end }} +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + {{- include "bk-user.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ tpl .host $ | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} + pathType: {{ .pathType }} + {{- end }} + backend: + {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} + service: + name: bkusersaas-web + port: + number: {{ $svcPort }} + {{- else }} + serviceName: bkusersaas-web + servicePort: {{ $svcPort }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} diff --git a/deploy/helm/bk-user/charts/saas/templates/migrate-job.yaml b/deploy/helm/bk-user/charts/saas/templates/migrate-job.yaml new file mode 100644 index 000000000..3b26d99a9 --- /dev/null +++ b/deploy/helm/bk-user/charts/saas/templates/migrate-job.yaml @@ -0,0 +1,52 @@ +apiVersion: batch/v1 +kind: Job +metadata: + name: bk-user-saas-migrate-db-{{ .Release.Revision }} + labels: + {{- include "bk-user.labels" . | nindent 4 }} +spec: + backoffLimit: 10 + template: + metadata: + labels: + {{- include "bk-user.labels" . | nindent 8 }} + spec: + {{- with .Values.global.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.global.hostAliases }} + hostAliases: + {{- toYaml . | nindent 8 }} + {{- end }} + restartPolicy: OnFailure + initContainers: + - name: check-database-ready + image: "{{ .Values.global.imageRegistry | default .Values.migration.images.busybox.registry }}/{{ .Values.migration.images.busybox.repository }}:{{ .Values.migration.images.busybox.tag }}" + imagePullPolicy: IfNotPresent + command: + - sh + - -c + args: + - "echo Start check database: $(DB_HOST):$(DB_PORT); until telnet $(DB_HOST) $(DB_PORT); do echo waiting for db $(DB_NAME); sleep 2; done;" + envFrom: + {{- toYaml .Values.envFrom | nindent 12 }} + containers: + - name: saas-db-migrate + image: "{{ .Values.global.imageRegistry | default .Values.image.registry }}/{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + command: + - /bin/bash + - -c + args: + - python manage.py migrate --no-input + {{- with .Values.env }} + env: + {{- toYaml . | nindent 12}} + {{- end }} + {{- with .Values.envFrom }} + envFrom: + {{- toYaml . | nindent 12}} + {{- end }} + resources: + {{- toYaml .Values.resources | nindent 12 }} diff --git a/deploy/helm/bk-user/charts/saas/templates/service.yaml b/deploy/helm/bk-user/charts/saas/templates/service.yaml new file mode 100644 index 000000000..53fd8ad85 --- /dev/null +++ b/deploy/helm/bk-user/charts/saas/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: bkusersaas-web + labels: + {{- include "bk-user.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "bk-user.selectorLabels" . | nindent 4 }} diff --git a/deploy/helm/bk-user/charts/saas/templates/serviceaccount.yaml b/deploy/helm/bk-user/charts/saas/templates/serviceaccount.yaml new file mode 100644 index 000000000..09c462054 --- /dev/null +++ b/deploy/helm/bk-user/charts/saas/templates/serviceaccount.yaml @@ -0,0 +1,39 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: bk-user-saas + labels: + {{- include "bk-user.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: bk-user-saas-role +rules: +- apiGroups: + - batch + resources: + - jobs + verbs: + - get + - list + - watch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: bk-user-saas-role-binding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: bk-user-saas-role +subjects: +- kind: ServiceAccount + name: bk-user-saas + namespace: {{ default "default" .Release.Namespace }} +{{- end }} \ No newline at end of file diff --git a/deploy/helm/bk-user/charts/saas/templates/servicemonitor.yaml b/deploy/helm/bk-user/charts/saas/templates/servicemonitor.yaml new file mode 100644 index 000000000..ab1a4722b --- /dev/null +++ b/deploy/helm/bk-user/charts/saas/templates/servicemonitor.yaml @@ -0,0 +1,31 @@ +{{- if .Values.global.serviceMonitor.enabled }} +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: {{ template "bk-user.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "bk-user.labels" . | nindent 4 }} +spec: + {{- if .Values.global.serviceMonitor.jobLabel }} + jobLabel: {{ .Values.global.serviceMonitor.jobLabel }} + {{- end }} + selector: + matchLabels: + {{- include "bk-user.selectorLabels" . | nindent 6 }} + endpoints: + - port: http + path: "/metrics" + {{- if .Values.global.serviceMonitor.interval }} + interval: {{ .Values.global.serviceMonitor.interval }} + {{- end }} + {{- if .Values.global.serviceMonitor.scrapeTimeout }} + scrapeTimeout: {{ .Values.global.serviceMonitor.scrapeTimeout }} + {{- end }} + {{- if hasKey .Values.global.serviceMonitor "honorLabels" }} + honorLabels: {{ .Values.global.serviceMonitor.honorLabels }} + {{- end }} + namespaceSelector: + matchNames: + - {{ .Release.Namespace }} +{{- end }} \ No newline at end of file diff --git a/deploy/helm/bk-user/charts/saas/values.yaml b/deploy/helm/bk-user/charts/saas/values.yaml new file mode 100644 index 000000000..3de6195e5 --- /dev/null +++ b/deploy/helm/bk-user/charts/saas/values.yaml @@ -0,0 +1,221 @@ +global: + imageRegistry: "" + ## E.g. + ## imagePullSecrets: + ## - myRegistryKeySecretName + imagePullSecrets: [] + storageClass: "" + + ## 蓝鲸产品统一根域 + bkDomain: "example.com" + bkDomainScheme: "http" + + ## 是否开启权限中心 + enableIAM: true + + hostAliases: [] + # - ip: "" + # hostnames: + # - "" + + ## -------------- + ## 蓝鲸监控 + ## -------------- + serviceMonitor: + ## @param serviceMonitor.enabled Creates a ServiceMonitor to monitor kube-state-metrics + ## + enabled: false + ## @param serviceMonitor.jobLabel The name of the label on the target service to use as the job name in prometheus. + ## + jobLabel: "" + ## @param serviceMonitor.interval Scrape interval (use by default, falling back to Prometheus' default) + ## ref: https://github.com/coreos/prometheus-operator/blob/master/Documentation/api.md#endpoint + ## e.g: + ## interval: 10s + ## + interval: "" + ## @param serviceMonitor.scrapeTimeout Timeout after which the scrape is ended + ## ref: https://github.com/coreos/prometheus-operator/blob/master/Documentation/api.md#endpoint + ## e.g: + ## scrapeTimeout: 10s + ## + scrapeTimeout: "" + ## @param serviceMonitor.selector ServiceMonitor selector labels + ## ref: https://github.com/bitnami/charts/tree/master/bitnami/prometheus-operator#prometheus-configuration + ## e.g: + ## selector: + ## prometheus: my-prometheus + ## + selector: {} + ## @param serviceMonitor.honorLabels Honor metrics labels + ## ref: https://github.com/coreos/prometheus-operator/blob/master/Documentation/api.md#endpoint + ## e.g: + ## honorLabels: false + ## + honorLabels: false + ## @param serviceMonitor.relabelings ServiceMonitor relabelings + ## ref: https://github.com/coreos/prometheus-operator/blob/master/Documentation/api.md#relabelconfig + ## + relabelings: [] + ## @param serviceMonitor.metricRelabelings ServiceMonitor metricRelabelings + ## ref: https://github.com/coreos/prometheus-operator/blob/master/Documentation/api.md#relabelconfig + ## + metricRelabelings: [] + + ## -------------- + ## 蓝鲸日志采集 + ## -------------- + bkLogConfig: + enabled: false + dataId: 1 + +## web deployment 副本数 +replicaCount: 1 +## celery deployment 副本数 +celeryReplicaCount: 1 + +appCode: "bk_usermgr" +appSecret: "" + +image: + registry: mirrors.tencent.com + repository: blueking/bk-user-saas + pullPolicy: IfNotPresent + tag: "v2.3.2" + +command: [] +args: [] + +nameOverride: "" +fullnameOverride: "" + +serviceAccount: + ## Specifies whether a service account should be created + create: true + ## Annotations to add to the service account + annotations: {} + ## The name of the service account to use. + ## If not set and create is true, a name is generated using the fullname template + name: "" + +## 蓝鲸 PaaS url(浏览器访问蓝鲸入口) +bkPaasUrl: http://paas.example.com +## 蓝鲸 ESB/APIGATEWAY url,注意集群内外都是统一域名。集群内可以配置域名解析到内网ip +bkComponentApiUrl: http://bkapi.example.com +## 蓝鲸 Login url(浏览器跳转登录用的URL前缀) +bkLoginUrl: http://paas.example.com/login/ +## 蓝鲸登录后台的内部服务地址(一般用于校验登录token) +bkLoginApiUrl: http://bk-login-web +## 蓝鲸用户管理 SaaS 地址 +bkUserAddr: bkuser.example.com +## 蓝鲸用户管理后台 API 地址 +bkUserApiUrl: http://bkuserapi-web + +## --------------- +## 环境变量 +## --------------- +## 请按照原生 env 格式填写 +## env 优先级高于 envFrom,你可以用它来覆盖内置环境变量 +# env: +# - name: "FOO" +# value: "BAR" + +envFrom: + - configMapRef: + name: bk-user-saas-general-envs + - configMapRef: + name: bk-user-saas-external-storage + +podAnnotations: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +service: + type: ClusterIP + port: 80 + +ingress: + enabled: true + className: "" + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: "{{ .Values.bkUserAddr }}" + paths: + - path: / + pathType: ImplementationSpecific + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +resources: + limits: + cpu: 1024m + memory: 2048Mi + requests: + cpu: 100m + memory: 128Mi + +## --------------- +## 调度 +## --------------- +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +nodeSelector: {} + +tolerations: [] + +affinity: {} + +## -------------- +## 外部数据库配置 +## -------------- +preferDBName: "bk_user_saas" +externalDatabase: + default: + host: "" + password: "" + port: 3306 + user: "" + name: "bk_user_saas" + +## --------------- +## 外部 Redis +## --------------- +externalRedis: + default: + host: "" + port: 6379 + password: "" + +## -------------- +## 检查 DB 变更 +## -------------- +migration: + images: + busybox: + registry: "mirrors.tencent.com" + repository: blueking/busybox + tag: "1.34.0" + k8sWaitFor: + registry: "mirrors.tencent.com" + repository: blueking/k8s-wait-for + tag: "v1.5.1" + + diff --git a/deploy/helm/bk-user-stack/templates/NOTES.txt b/deploy/helm/bk-user/templates/NOTES.txt similarity index 55% rename from deploy/helm/bk-user-stack/templates/NOTES.txt rename to deploy/helm/bk-user/templates/NOTES.txt index d20b25341..25caf9354 100644 --- a/deploy/helm/bk-user-stack/templates/NOTES.txt +++ b/deploy/helm/bk-user/templates/NOTES.txt @@ -1,11 +1,11 @@ 恭喜,你已经成功安装了蓝鲸用户管理 ! 如果集群中已经安装了 IngressController,那么可以通过以下地址访问用户管理: -- SaaS: http://bkuser.{{ .Values.global.sharedDomain }} -- Api: http://bkuser-api.{{ .Values.global.sharedDomain }}/ping +- SaaS: {{ .Values.global.bkDomainScheme }}://{{ .Values.saas.bkUserAddr }} +- Api: {{ .Values.api.bkUserApiUrl }} 登录账户名密码: -{{ .Values.bkuserapi.env.INITIAL_ADMIN_USERNAME }}/{{ .Values.bkuserapi.env.INITIAL_ADMIN_PASSWORD }} +{{ .Values.api.initialAdminUsername }}/{{ .Values.api.initialAdminPassword }} 查看更多信息: $ helm status {{ .Release.Name }} diff --git a/deploy/helm/bk-user/templates/_helpers.tpl b/deploy/helm/bk-user/templates/_helpers.tpl new file mode 100644 index 000000000..645b58786 --- /dev/null +++ b/deploy/helm/bk-user/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "bk-user.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "bk-user.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "bk-user.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "bk-user.labels" -}} +helm.sh/chart: {{ include "bk-user.chart" . }} +{{ include "bk-user.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "bk-user.selectorLabels" -}} +app.kubernetes.io/name: {{ include "bk-user.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "bk-user.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "bk-user.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/deploy/helm/bk-user/templates/_storage.tpl b/deploy/helm/bk-user/templates/_storage.tpl new file mode 100644 index 000000000..60259ba40 --- /dev/null +++ b/deploy/helm/bk-user/templates/_storage.tpl @@ -0,0 +1,10 @@ +{{/* +Shortcuts for redis +*/}} +{{- define "bk-user.apiExternalRedisBrokerUrl" -}} +{{- printf "redis://:%s@%s:%s/0" .Values.api.externalRedis.default.password .Values.api.externalRedis.default.host (.Values.api.externalRedis.default.port | toString )}} +{{- end }} + +{{- define "bk-user.builtinRedisBrokerUrl" -}} +{{- printf "redis://:%s@%s-redis-master:%s/0" .Values.redis.auth.password .Release.Name (.Values.redis.master.service.port | toString )}} +{{- end }} \ No newline at end of file diff --git a/deploy/helm/bk-user/templates/mariadb-env-configmap.yaml b/deploy/helm/bk-user/templates/mariadb-env-configmap.yaml new file mode 100644 index 000000000..a93da17e4 --- /dev/null +++ b/deploy/helm/bk-user/templates/mariadb-env-configmap.yaml @@ -0,0 +1,62 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: bk-user-api-mariadb-env + labels: + {{- include "bk-user.labels" . | nindent 4 }} +data: + {{- if .Values.mariadb.enabled }} + DB_NAME: "{{ .Values.api.preferDBName }}" + DB_USER: "{{ .Values.mariadb.auth.username }}" + DB_PASSWORD: "{{ .Values.mariadb.auth.password }}" + DB_HOST: "{{ .Release.Name }}-mariadb" + DB_PORT: "3306" + {{- else }} + DB_NAME: "{{ .Values.api.externalDatabase.default.name | default .Values.api.preferDBName }}" + DB_USER: "{{ .Values.api.externalDatabase.default.user }}" + DB_PASSWORD: "{{ .Values.api.externalDatabase.default.password }}" + DB_HOST: "{{ .Values.api.externalDatabase.default.host }}" + DB_PORT: "{{ .Values.api.externalDatabase.default.port }}" + {{- end }} +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: bk-user-saas-mariadb-env + labels: + {{- include "bk-user.labels" . | nindent 4 }} +data: + {{- if .Values.mariadb.enabled }} + DB_NAME: "{{ .Values.saas.preferDBName }}" + DB_USER: "{{ .Values.mariadb.auth.username }}" + DB_PASSWORD: "{{ .Values.mariadb.auth.password }}" + DB_HOST: "{{ .Release.Name }}-mariadb" + DB_PORT: "3306" + {{- else }} + DB_NAME: "{{ .Values.saas.externalDatabase.default.name | default .Values.saas.preferDBName }}" + DB_USER: "{{ .Values.saas.externalDatabase.default.user }}" + DB_PASSWORD: "{{ .Values.saas.externalDatabase.default.password }}" + DB_HOST: "{{ .Values.saas.externalDatabase.default.host }}" + DB_PORT: "{{ .Values.saas.externalDatabase.default.port }}" + {{- end }} +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: bk-login-mariadb-env + labels: + {{- include "bk-user.labels" . | nindent 4 }} +data: + {{- if .Values.mariadb.enabled }} + DATABASE_NAME: "{{ .Values.login.preferDBName }}" + DATABASE_USER: "{{ .Values.mariadb.auth.username }}" + DATABASE_PASSWORD: "{{ .Values.mariadb.auth.password }}" + DATABASE_HOST: "{{ .Release.Name }}-mariadb" + DATABASE_PORT: "3306" + {{- else }} + DATABASE_NAME: "{{ .Values.login.externalDatabase.default.name | default .Values.login.preferDBName }}" + DATABASE_USER: "{{ .Values.login.externalDatabase.default.user }}" + DATABASE_PASSWORD: "{{ .Values.login.externalDatabase.default.password }}" + DATABASE_HOST: "{{ .Values.login.externalDatabase.default.host }}" + DATABASE_PORT: "{{ .Values.login.externalDatabase.default.port }}" + {{- end -}} \ No newline at end of file diff --git a/deploy/helm/bk-user/templates/mariadb-init-configmap.yaml b/deploy/helm/bk-user/templates/mariadb-init-configmap.yaml new file mode 100644 index 000000000..f1c2c2d14 --- /dev/null +++ b/deploy/helm/bk-user/templates/mariadb-init-configmap.yaml @@ -0,0 +1,21 @@ +{{- if .Values.mariadb.enabled }} +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: bk-user-mariadb-init + labels: + {{- include "bk-user.labels" . | nindent 4 }} +data: + init.sql: | + GRANT ALL PRIVILEGES ON *.* TO `{{ .Values.mariadb.auth.username }}`@'%' WITH GRANT OPTION; + + CREATE DATABASE IF NOT EXISTS `{{ .Values.api.preferDBName }}` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; + GRANT ALL PRIVILEGES ON `{{ .Values.api.preferDBName }}`.* TO `{{ .Values.mariadb.auth.username }}`@'%' WITH GRANT OPTION; + + CREATE DATABASE IF NOT EXISTS `{{ .Values.saas.preferDBName }}` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; + GRANT ALL PRIVILEGES ON `{{ .Values.saas.preferDBName }}`.* TO `{{ .Values.mariadb.auth.username }}`@'%' WITH GRANT OPTION; + + CREATE DATABASE IF NOT EXISTS `{{ .Values.login.preferDBName }}` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; + GRANT ALL PRIVILEGES ON `{{ .Values.login.preferDBName }}`.* TO `{{ .Values.mariadb.auth.username }}`@'%' WITH GRANT OPTION; + {{- end -}} \ No newline at end of file diff --git a/deploy/helm/bk-user/templates/redis-env-configmap.yaml b/deploy/helm/bk-user/templates/redis-env-configmap.yaml new file mode 100644 index 000000000..5d4b94915 --- /dev/null +++ b/deploy/helm/bk-user/templates/redis-env-configmap.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: bk-user-api-redis-env + labels: + {{- include "bk-user.labels" . | nindent 4 }} +data: + {{- if .Values.redis.enabled }} + CELERY_BROKER_URL: {{ include "bk-user.builtinRedisBrokerUrl" . }} + CELERY_RESULT_BACKEND: {{ include "bk-user.builtinRedisBrokerUrl" . }} + {{- else }} + CELERY_BROKER_URL: {{ .Values.api.celeryBrokerUrl | default (include "bk-user.apiExternalRedisBrokerUrl" .) }} + CELERY_RESULT_BACKEND: {{ .Values.api.celeryResultBackend | default (include "bk-user.apiExternalRedisBrokerUrl" .) }} + {{- end -}} diff --git a/deploy/helm/bk-user/values.yaml b/deploy/helm/bk-user/values.yaml new file mode 100644 index 000000000..6d6b90599 --- /dev/null +++ b/deploy/helm/bk-user/values.yaml @@ -0,0 +1,94 @@ +global: + imageRegistry: "" + ## E.g. + ## imagePullSecrets: + ## - myRegistryKeySecretName + imagePullSecrets: [] + storageClass: "" + + ## 蓝鲸产品统一根域 + bkDomain: "example.com" + bkDomainScheme: "http" + ## 是否开启权限中心 + enableIAM: false + + hostAliases: [] + # - ip: "" + # hostnames: + # - "" + + ## -------------- + ## 蓝鲸监控 + ## -------------- + serviceMonitor: + enabled: false + + ## -------------- + ## 蓝鲸日志采集 + ## -------------- + bkLogConfig: + enabled: false + dataId: 1 + +api: + enabled: true + + envFrom: + - configMapRef: + name: bk-user-api-general-envs + - configMapRef: + name: bk-user-api-mariadb-env + - configMapRef: + name: bk-user-api-redis-env + +saas: + enabled: true + + envFrom: + - configMapRef: + name: bk-user-saas-general-envs + - configMapRef: + name: bk-user-saas-mariadb-env + +login: + enabled: true + + envFrom: + - configMapRef: + name: bk-login-general-envs + - configMapRef: + name: bk-login-mariadb-env + +# ------------- +# 内建存储配置 +# 默认通过 .Release.Name 拼接访问,请不要配置 nameOverride 或 fullnameOverride +# 否则会出现无法访问存储的异常 +# ------------- +mariadb: + enabled: true + architecture: standalone + auth: + username: "admin" + password: "blueking" + primary: + # 默认我们未开启持久化,如有需求可以参考: + # - https://kubernetes.io/docs/user-guide/persistent-volumes/ + # - https://github.com/bitnami/charts/blob/master/bitnami/mariadb/values.yaml#L360 + # 当同时请注意,当开启 PVC 可能会导致首次安装部署时间延长 + persistence: + enabled: false + initdbScriptsConfigMap: "bk-user-mariadb-init" + +redis: + enabled: true + sentinel: + enabled: false + auth: + password: "blueking" + master: + persistence: + enabled: false + replica: + replicaCount: 1 + persistence: + enabled: false \ No newline at end of file diff --git a/deploy/helm/chartty/c_base.tpl b/deploy/helm/chartty/c_base.tpl deleted file mode 100644 index 07da37b7f..000000000 --- a/deploy/helm/chartty/c_base.tpl +++ /dev/null @@ -1,83 +0,0 @@ -{{/* vim: set filetype=mustache: */}} -{{/* -Expand the name of the chart. -*/}} -{{- define "chartty.name" -}} -{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} -{{- end }} - -{{/* -Create a default fully qualified app name. -We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). -If release name contains chart name it will be used as a full name. -*/}} -{{- define "chartty.fullname" -}} -{{- if .Values.fullnameOverride }} -{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} -{{- else }} -{{- $name := default .Chart.Name .Values.nameOverride }} -{{- if contains $name .Release.Name }} -{{- .Release.Name | trunc 63 | trimSuffix "-" }} -{{- else }} -{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} -{{- end }} -{{- end }} -{{- end }} - -{{/* -Create chart name and version as used by the chart label. -*/}} -{{- define "chartty.chart" -}} -{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} -{{- end }} - - - -{{/* -Create the name of the service account to use -*/}} -{{- define "chartty.serviceAccountName" -}} -{{- if .Values.serviceAccount.create }} -{{- default (include "chartty.fullname" .) .Values.serviceAccount.name }} -{{- else }} -{{- default "default" .Values.serviceAccount.name }} -{{- end }} -{{- end }} - -{{/* Create the docker config json for image pull secret */}} -{{- define "chartty.dockerconfigjson" -}} -{{- with .Values.global.imageCredentials }} -{{- printf "{\"auths\":{\"%s\":{\"username\":\"%s\",\"password\":\"%s\",\"auth\":\"%s\"}}}" .registry .username .password (printf "%s:%s" .username .password | b64enc) | b64enc }} -{{- end }} -{{- end }} - -{{/* Create imageSerect fields */}} -{{- define "chartty.imagePullSecretNames" -}} -{{- if .Values.global.imageCredentials.enabled -}} -- name: {{ include "chartty.name" . }}-{{ default "dockerconfigjson" .Values.global.imageCredentials.name }} -{{- range $value := .Values.global.imagePullSecrets }} -- name: {{ $value }} -{{- end }} -{{- else }} -{{- if .Values.global.imagePullSecrets }} -{{- range $value := .Values.global.imagePullSecrets }} -- name: {{ $value }} -{{- end }} -{{- else }}[] -{{- end }} -{{- end }} -{{- end }} - -{{/* vim: set filetype=mustache: */}} -{{/* -Renders a value that contains template. -Usage: -{{ include "chartty.tplvalues.render" ( dict "value" .Values.path.to.the.Value "context" $) }} -*/}} -{{- define "chartty.tplvalues.render" -}} - {{- if typeIs "string" .value }} - {{- tpl .value .context }} - {{- else }} - {{- tpl (.value | toYaml) .context }} - {{- end }} -{{- end -}} \ No newline at end of file diff --git a/deploy/helm/chartty/c_bklogconfig.yaml b/deploy/helm/chartty/c_bklogconfig.yaml deleted file mode 100644 index 32b6e8f1b..000000000 --- a/deploy/helm/chartty/c_bklogconfig.yaml +++ /dev/null @@ -1,16 +0,0 @@ -{{- $global := . }} -{{- $namePrefix := include "chartty.name" . -}} -{{- if .Values.global.bkLogConfig.enabled }} -apiVersion: bk.tencent.com/v1alpha1 -kind: BkLogConfig -metadata: - name: {{ $namePrefix }}-stdout-log -spec: - dataId: {{ .Values.global.bkLogConfig.dataId }} - logConfigType: std_log_config - namespace: {{ .Release.Namespace | quote }} - container_name_match: - - {{ $namePrefix }} - labelSelector: - matchLabels: {{- include "chartty.labels" $global | nindent 6 }} -{{- end }} \ No newline at end of file diff --git a/deploy/helm/chartty/c_capabilities.tpl b/deploy/helm/chartty/c_capabilities.tpl deleted file mode 100644 index 0dae416e4..000000000 --- a/deploy/helm/chartty/c_capabilities.tpl +++ /dev/null @@ -1,114 +0,0 @@ -{{/* vim: set filetype=mustache: */}} - -{{/* -Return the target Kubernetes version -*/}} -{{- define "chartty.capabilities.kubeVersion" -}} -{{- .Values.global.kubeVersion | default .Capabilities.KubeVersion.Version -}} -{{- end -}} - -{{/* -Return the appropriate apiVersion for policy. -*/}} -{{- define "chartty.capabilities.policy.apiVersion" -}} -{{- if semverCompare "<1.21-0" (include "chartty.capabilities.kubeVersion" .) -}} -{{- print "policy/v1beta1" -}} -{{- else -}} -{{- print "policy/v1" -}} -{{- end -}} -{{- end -}} - -{{/* -Return the appropriate apiVersion for cronjob. -*/}} -{{- define "chartty.capabilities.cronjob.apiVersion" -}} -{{- if semverCompare "<1.21-0" (include "chartty.capabilities.kubeVersion" .) -}} -{{- print "batch/v1beta1" -}} -{{- else -}} -{{- print "batch/v1" -}} -{{- end -}} -{{- end -}} - -{{/* -Return the appropriate apiVersion for deployment. -*/}} -{{- define "chartty.capabilities.deployment.apiVersion" -}} -{{- if semverCompare "<1.14-0" (include "chartty.capabilities.kubeVersion" .) -}} -{{- print "extensions/v1beta1" -}} -{{- else -}} -{{- print "apps/v1" -}} -{{- end -}} -{{- end -}} - -{{/* -Return the appropriate apiVersion for statefulset. -*/}} -{{- define "chartty.capabilities.statefulset.apiVersion" -}} -{{- if semverCompare "<1.14-0" (include "chartty.capabilities.kubeVersion" .) -}} -{{- print "apps/v1beta1" -}} -{{- else -}} -{{- print "apps/v1" -}} -{{- end -}} -{{- end -}} - -{{/* -Return the appropriate apiVersion for ingress. -*/}} -{{- define "chartty.capabilities.ingress.apiVersion" -}} -{{- if semverCompare "<1.14-0" (include "chartty.capabilities.kubeVersion" .) -}} -{{- print "extensions/v1beta1" -}} -{{- else if semverCompare "<1.19-0" (include "chartty.capabilities.kubeVersion" .) -}} -{{- print "networking.k8s.io/v1beta1" -}} -{{- else -}} -{{- print "networking.k8s.io/v1" -}} -{{- end }} -{{- end -}} - -{{- define "chartty.capabilities.ingress.backendService" -}} -{{- if semverCompare "<1.19-0" (include "chartty.capabilities.kubeVersion" .global) -}} -backend: - serviceName: {{ .chartName }}-{{ .processType }} - servicePort: {{ .svcPort }} -{{- else -}} -pathType: "Prefix" -backend: - service: - name: {{ .chartName }}-{{ .processType }} - port: - number: {{ .svcPort }} -{{- end }} -{{- end -}} - -{{/* -Return the appropriate apiVersion for RBAC resources. -*/}} -{{- define "chartty.capabilities.rbac.apiVersion" -}} -{{- if semverCompare "<1.17-0" (include "chartty.capabilities.kubeVersion" .) -}} -{{- print "rbac.authorization.k8s.io/v1beta1" -}} -{{- else -}} -{{- print "rbac.authorization.k8s.io/v1" -}} -{{- end -}} -{{- end -}} - -{{/* -Return the appropriate apiVersion for CRDs. -*/}} -{{- define "chartty.capabilities.crd.apiVersion" -}} -{{- if semverCompare "<1.19-0" (include "chartty.capabilities.kubeVersion" .) -}} -{{- print "apiextensions.k8s.io/v1beta1" -}} -{{- else -}} -{{- print "apiextensions.k8s.io/v1" -}} -{{- end -}} -{{- end -}} - -{{/* -Returns true if the used Helm version is 3.3+. -A way to check the used Helm version was not introduced until version 3.3.0 with .Capabilities.HelmVersion, which contains an additional "{}}" structure. -This check is introduced as a regexMatch instead of {{ if .Capabilities.HelmVersion }} because checking for the key HelmVersion in <3.3 results in a "interface not found" error. -**To be removed when the catalog's minimun Helm version is 3.3** -*/}} -{{- define "chartty.capabilities.supportsHelmVersion" -}} -{{- if regexMatch "{(v[0-9])*[^}]*}}$" (.Capabilities | toString ) }} - {{- true -}} -{{- end -}} -{{- end -}} \ No newline at end of file diff --git a/deploy/helm/chartty/c_configmap.yaml b/deploy/helm/chartty/c_configmap.yaml deleted file mode 100644 index 882418b6c..000000000 --- a/deploy/helm/chartty/c_configmap.yaml +++ /dev/null @@ -1,23 +0,0 @@ -{{- $global := . }} -{{- $namePrefix := include "chartty.name" . -}} -{{- range $map := .Values.configMaps }} ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: {{ $namePrefix }}-{{ $map.name }} - labels: - {{- include "chartty.labels" $global | nindent 4 }} - annotations: - "helm.sh/hook": pre-install - "helm.sh/hook-weight": "-1" -data: - {{- range $file := $map.files }} - {{ $file.name }}: |- - {{- if eq (default "plain" $file.format) "yaml" }} - {{- $file.data | toYaml | toString | nindent 6 }} - {{ else }} - {{- $file.data | toString | nindent 6 }} - {{- end }} - {{- end }} -{{- end }} \ No newline at end of file diff --git a/deploy/helm/chartty/c_cronjob.yaml b/deploy/helm/chartty/c_cronjob.yaml deleted file mode 100644 index e69778466..000000000 --- a/deploy/helm/chartty/c_cronjob.yaml +++ /dev/null @@ -1,65 +0,0 @@ -{{- $global := . }} -{{- $chart_version := .Chart.Version | replace "+" "_" }} -{{- range $job := .Values.cronJobs.jobs }} -{{ if $job.enabled }} ---- -apiVersion: {{ template "chartty.capabilities.cronjob.apiVersion" $global }} -kind: CronJob -metadata: - name: {{ $.Chart.Name }}-{{ $job.name }} - labels: - {{- include "chartty.labels" $global | nindent 4 }} -spec: - schedule: {{ $job.schedule | quote }} - successfulJobsHistoryLimit: {{ $job.successfulJobsHistoryLimit }} - concurrencyPolicy: {{ $job.concurrencyPolicy }} - failedJobsHistoryLimit: {{ $job.failedJobsHistoryLimit }} - jobTemplate: - spec: - template: - metadata: - labels: - app: {{ $.Release.Name }} - cron: {{ $job.name }} - {{- include "chartty.labels" $global | nindent 12 }} - spec: - restartPolicy: OnFailure - {{- with $.Values.global.hostAliases }} - hostAliases: - {{- toYaml $.Values.global.hostAliases | nindent 12 }} - {{- end }} - imagePullSecrets: - {{- include "chartty.imagePullSecretNames" $global | nindent 12 }} - containers: - - image: {{ include "chartty.image" $global }} - imagePullPolicy: {{ $.Values.global.image.pullPolicy }} - env: - {{- include "chartty.envs" $global | nindent 14 }} - {{- with $.Values.envFrom }} - envFrom: - {{- toYaml . | nindent 14}} - {{- end }} - name: {{ $job.name }} - command: - {{- toYaml $job.command | nindent 14 }} - args: - {{- toYaml $job.args | nindent 14 }} - {{- with $job.readinessProbe }} - readinessProbe: - {{- toYaml . | nindent 14 }} - {{- end }} - {{- with $job.livenessProbe }} - livenessProbe: - {{- toYaml . | nindent 14 }} - {{- end }} - {{- with $.Values.nodeSelector }} - nodeSelector: - {{- toYaml . | nindent 12 }} - {{- end }} - {{- with $.Values.affinity }} - affinity: - {{- toYaml . | nindent 12 }} - {{- end }} -{{- end }} - -{{- end }} diff --git a/deploy/helm/chartty/c_deployment.yaml b/deploy/helm/chartty/c_deployment.yaml deleted file mode 100644 index 7c96e5da6..000000000 --- a/deploy/helm/chartty/c_deployment.yaml +++ /dev/null @@ -1,97 +0,0 @@ -{{- $global := . }} -{{- $chart_version := .Chart.Version | replace "+" "_" }} -{{- range $processType, $processInfo := .Values.processes }} ---- -apiVersion: {{ template "chartty.capabilities.deployment.apiVersion" $global }} -kind: Deployment -metadata: - name: {{ $.Chart.Name }}-{{ $processType }} - labels: - process_type: {{ $processType }} - {{- include "chartty.labels" $global | nindent 4 }} -spec: - replicas: {{ $processInfo.replicas | default $.Values.replicaCount }} - selector: - matchLabels: - process_type: {{ $processType }} - {{- include "chartty.selectorLabels" $global | nindent 6 }} - template: - metadata: - {{- with $.Values.podAnnotations }} - annotations: - {{- toYaml . | nindent 8 }} - {{- end }} - labels: - process_type: {{ $processType }} - {{- include "chartty.labels" $global | nindent 8 }} - spec: - imagePullSecrets: - {{- include "chartty.imagePullSecretNames" $global | nindent 8 }} - serviceAccountName: {{ include "chartty.serviceAccountName" $global }} - securityContext: - {{- toYaml $.Values.podSecurityContext | nindent 8 }} - {{- with $.Values.global.hostAliases }} - hostAliases: - {{- toYaml $.Values.global.hostAliases | nindent 8 }} - {{- end }} - containers: - - name: {{ $.Chart.Name }}-{{ $processType }} - securityContext: - {{- toYaml $.Values.securityContext | nindent 12 }} - image: {{ include "chartty.image" $global }} - {{- if hasKey $processInfo "command" }} - command: - {{- range $idx, $value := $processInfo.command }} - - "{{ $value }}" - {{- end }} - {{- end }} - {{- if hasKey $processInfo "args" }} - args: - {{- range $idx, $value := $processInfo.args }} - - "{{ $value }}" - {{- end }} - {{- end }} - {{- with $.Values.volumeMounts }} - volumeMounts: - {{- toYaml . | nindent 12 }} - {{- end }} - imagePullPolicy: {{ $.Values.global.image.pullPolicy }} - env: - {{- include "chartty.envs" $global | nindent 12 }} - {{- with $.Values.envFrom }} - envFrom: - {{- toYaml . | nindent 12}} - {{- end }} - ports: - - name: http - containerPort: {{ $.Values.httpPort }} - protocol: TCP - {{- with $processInfo.resources }} - resources: - {{- toYaml $processInfo.resources | nindent 12 }} - {{- end }} - {{- with $processInfo.readinessProbe }} - readinessProbe: - {{- toYaml . | nindent 14 }} - {{- end }} - {{- with $processInfo.livenessProbe }} - livenessProbe: - {{- toYaml . | nindent 14 }} - {{- end }} - {{- with $.Values.nodeSelector }} - nodeSelector: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with $.Values.affinity }} - affinity: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with $.Values.volumes }} - volumes: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with $.Values.tolerations }} - tolerations: - {{- toYaml . | nindent 8 }} - {{- end }} -{{- end }} diff --git a/deploy/helm/chartty/c_env.tpl b/deploy/helm/chartty/c_env.tpl deleted file mode 100644 index 8e578707c..000000000 --- a/deploy/helm/chartty/c_env.tpl +++ /dev/null @@ -1,30 +0,0 @@ -{{/* vim: set filetype=mustache: */}} - -{{/* Create envs */}} -{{- define "chartty.envs" -}} -{{- with .Values.extrasEnv }} -{{- /* 直接渲染 extrasEnv */ -}} -{{- toYaml . }} -{{- end }} -{{- /* 渲染 sharedUrlEnvMap */ -}} -{{- range $k, $v := .Values.sharedUrlEnvMap }} -{{- if hasKey $.Values.env $k }} -{{- else }} -- name: {{ $k }} - value: "{{ tpl $v $ }}" -{{- end }} -{{- end }} -{{- /* 渲染 global 环境变量时,如果模块已指定直接跳过 */ -}} -{{- range $k, $v := .Values.global.env }} -{{- if hasKey $.Values.env $k }} -{{- else }} -- name: {{ $k }} - value: "{{ $v }}" -{{- end }} -{{- end }} -{{- /* 高优先级渲染 .Values.env */ -}} -{{- range $k, $v := .Values.env }} -- name: {{ $k }} - value: "{{ $v }}" -{{- end }} -{{- end }} \ No newline at end of file diff --git a/deploy/helm/chartty/c_image.tpl b/deploy/helm/chartty/c_image.tpl deleted file mode 100644 index 10b034536..000000000 --- a/deploy/helm/chartty/c_image.tpl +++ /dev/null @@ -1,6 +0,0 @@ -{{/* vim: set filetype=mustache: */}} - -{{/* Create image */}} -{{- define "chartty.image" -}} -"{{ .Values.global.image.registry }}/{{ .Values.image.name }}:{{ .Values.global.image.tag | default .Chart.AppVersion }}" -{{- end }} \ No newline at end of file diff --git a/deploy/helm/chartty/c_ingress.yaml b/deploy/helm/chartty/c_ingress.yaml deleted file mode 100644 index a001a4c64..000000000 --- a/deploy/helm/chartty/c_ingress.yaml +++ /dev/null @@ -1,40 +0,0 @@ -{{- $global := . }} -{{- $chartName := include "chartty.name" . -}} -{{- $svcPort := .Values.service.port -}} -{{- range $processType, $processInfo := .Values.processes -}} -{{- with $processInfo.ingress }} -{{- if (default false $processInfo.ingress.enabled) }} ---- -apiVersion: {{ template "chartty.capabilities.ingress.apiVersion" $global }} -kind: Ingress -metadata: - name: {{ $chartName }}-{{ $processType }} - labels: - process_type: {{ $processType }} - {{- include "chartty.labels" $global | nindent 4 }} - {{- with $processInfo.ingress.annotations }} - annotations: - {{- toYaml . | nindent 4 }} - {{- end }} -spec: - {{- if $processInfo.ingress.tls }} - tls: - {{- range $processInfo.ingress.tls }} - - hosts: - {{- range .hosts }} - - {{ . | quote }} - {{- end }} - secretName: {{ .secretName }} - {{- end }} - {{- end }} - rules: - - host: {{ tpl $processInfo.ingress.host $ | quote }} - http: - paths: - {{- range $processInfo.ingress.paths }} - - path: {{ . }} - {{- include "chartty.capabilities.ingress.backendService" (dict "global" $global "chartName" $chartName "processType" $processType "svcPort" $svcPort ) | nindent 12 }} - {{- end }} -{{- end }} -{{- end }} -{{- end }} diff --git a/deploy/helm/chartty/c_labels.tpl b/deploy/helm/chartty/c_labels.tpl deleted file mode 100644 index d1015ab9b..000000000 --- a/deploy/helm/chartty/c_labels.tpl +++ /dev/null @@ -1,24 +0,0 @@ -{{/* vim: set filetype=mustache: */}} - -{{/* -Common labels -*/}} -{{- define "chartty.labels" -}} -helm.sh/chart: {{ include "chartty.chart" . }} -{{- with .Values.podLabels }} -{{ toYaml . }} -{{- end }} -{{ include "chartty.selectorLabels" . }} -{{- if .Chart.AppVersion }} -app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} -{{- end }} -app.kubernetes.io/managed-by: {{ .Release.Service }} -{{- end }} - -{{/* -Selector labels -*/}} -{{- define "chartty.selectorLabels" -}} -app.kubernetes.io/name: {{ include "chartty.name" . }} -app.kubernetes.io/instance: {{ .Release.Name }} -{{- end }} \ No newline at end of file diff --git a/deploy/helm/chartty/c_pre-run-hooks.yaml b/deploy/helm/chartty/c_pre-run-hooks.yaml deleted file mode 100644 index 3f834759b..000000000 --- a/deploy/helm/chartty/c_pre-run-hooks.yaml +++ /dev/null @@ -1,48 +0,0 @@ -{{- $global := . }} -{{- $chart_version := .Chart.Version | replace "+" "_" }} -{{- range $name, $hook := .Values.preRunHooks }} -{{- if $hook.enabled }} ---- -apiVersion: v1 -kind: Pod -metadata: - name: {{ $.Chart.Name }}-{{ $name }} - labels: - {{- include "chartty.labels" $global | nindent 4 }} - annotations: - "helm.sh/hook": {{ default "pre-upgrade,pre-install" $hook.position }} - "helm.sh/hook-weight": "{{ $hook.weight }}" - "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded -spec: - imagePullSecrets: - {{- include "chartty.imagePullSecretNames" $global | nindent 4 }} - restartPolicy: OnFailure - {{- with $.Values.volumes }} - volumes: - {{- toYaml . | nindent 4 }} - {{- end }} - {{- with $.Values.global.hostAliases }} - hostAliases: - {{- toYaml $.Values.global.hostAliases | nindent 4 }} - {{- end }} - containers: - - name: {{ $.Release.Name }}-{{ $name }} - image: {{ include "chartty.image" $global }} - imagePullPolicy: {{ $.Values.global.image.pullPolicy }} - env: - {{- include "chartty.envs" $global | nindent 8 }} - {{- with $.Values.envFrom }} - envFrom: - {{- toYaml . | nindent 8 }} - {{- end }} - command: - {{- toYaml $hook.command | nindent 8 }} - args: - {{- toYaml $hook.args | nindent 8 }} - {{- with $.Values.volumeMounts }} - volumeMounts: - {{- toYaml . | nindent 8 }} - {{- end }} -{{- end }} -{{- end }} - diff --git a/deploy/helm/chartty/c_secret.yaml b/deploy/helm/chartty/c_secret.yaml deleted file mode 100644 index 5a6c6f619..000000000 --- a/deploy/helm/chartty/c_secret.yaml +++ /dev/null @@ -1,14 +0,0 @@ -{{- $namePrefix := include "chartty.name" . -}} -{{- $enabled := default false .Values.global.imageCredentials.enabled -}} -{{- if $enabled }} -apiVersion: v1 -kind: Secret -metadata: - name: {{ $namePrefix }}-{{ .Values.global.imageCredentials.name }} - annotations: - "helm.sh/hook": pre-install - "helm.sh/hook-weight": "-2" -type: kubernetes.io/dockerconfigjson -data: - .dockerconfigjson: {{ template "chartty.dockerconfigjson" . }} -{{- end }} diff --git a/deploy/helm/chartty/c_service.yaml b/deploy/helm/chartty/c_service.yaml deleted file mode 100644 index 7f1c72f5c..000000000 --- a/deploy/helm/chartty/c_service.yaml +++ /dev/null @@ -1,22 +0,0 @@ -{{- $global := . }} -{{- $chartName := include "chartty.name" . -}} -{{- range $processType, $proccesInfo := .Values.processes }} ---- -apiVersion: v1 -kind: Service -metadata: - name: {{ $chartName }}-{{ $processType }} - labels: - process_type: {{ $processType }} - {{- include "chartty.labels" $global | nindent 4 }} -spec: - type: {{ $.Values.service.type }} - ports: - - port: {{ $.Values.service.port }} - targetPort: http - protocol: TCP - name: http - selector: - process_type: {{ $processType }} - {{- include "chartty.selectorLabels" $global | nindent 4 }} -{{- end }} \ No newline at end of file diff --git a/deploy/helm/chartty/c_serviceaccount.yaml b/deploy/helm/chartty/c_serviceaccount.yaml deleted file mode 100644 index e57794398..000000000 --- a/deploy/helm/chartty/c_serviceaccount.yaml +++ /dev/null @@ -1,14 +0,0 @@ -{{- if .Values.serviceAccount.create -}} -apiVersion: v1 -kind: ServiceAccount -metadata: - name: {{ include "chartty.serviceAccountName" . }} - labels: - {{- include "chartty.labels" . | nindent 4 }} - {{- with .Values.serviceAccount.annotations }} - annotations: - {{- toYaml . | nindent 4 }} - "helm.sh/hook": pre-install - "helm.sh/hook-weight": "-3" - {{- end }} -{{- end }} diff --git a/deploy/helm/chartty/c_servicemonitor.yaml b/deploy/helm/chartty/c_servicemonitor.yaml deleted file mode 100644 index 3d8098b84..000000000 --- a/deploy/helm/chartty/c_servicemonitor.yaml +++ /dev/null @@ -1,38 +0,0 @@ -{{- $global := . }} -{{- if .Values.serviceMonitor.enabled }} -apiVersion: monitoring.coreos.com/v1 -kind: ServiceMonitor -metadata: - name: {{ template "chartty.fullname" . }} - namespace: {{ .Release.Namespace }} - labels: - {{- include "chartty.labels" $global | nindent 4 }} -spec: - {{- if .Values.serviceMonitor.jobLabel }} - jobLabel: {{ .Values.serviceMonitor.jobLabel }} - {{- end }} - selector: - matchLabels: - {{- include "chartty.selectorLabels" $global | nindent 6 }} - endpoints: - - port: http - path: "/metrics" - {{- if .Values.serviceMonitor.interval }} - interval: {{ .Values.serviceMonitor.interval }} - {{- end }} - {{- if .Values.serviceMonitor.scrapeTimeout }} - scrapeTimeout: {{ .Values.serviceMonitor.scrapeTimeout }} - {{- end }} - {{- if hasKey .Values.serviceMonitor "honorLabels" }} - honorLabels: {{ .Values.serviceMonitor.honorLabels }} - {{- end }} - {{- if .Values.serviceMonitor.relabelings }} - relabelings: {{- include "chartty.tplvalues.render" ( dict "value" .Values.serviceMonitor.relabelings "context" $) | nindent 8 }} - {{- end }} - {{- if .Values.serviceMonitor.metricRelabelings }} - metricRelabelings: {{- include "chartty.tplvalues.render" ( dict "value" .Values.serviceMonitor.metricRelabelings "context" $) | nindent 8 }} - {{- end }} - namespaceSelector: - matchNames: - - {{ .Release.Namespace }} -{{- end }} \ No newline at end of file diff --git a/deploy/helm/chartty/c_validate_env.yaml b/deploy/helm/chartty/c_validate_env.yaml deleted file mode 100644 index b5d34d69b..000000000 --- a/deploy/helm/chartty/c_validate_env.yaml +++ /dev/null @@ -1,21 +0,0 @@ -{{/* 检查必填环境变量 */}} -{{- define "chartty.envs.check" -}} -{{- if .Values.requiredEnvList -}} - -{{- $envs := dict -}} - -{{- range $env := fromYamlArray (include "chartty.envs" .) -}} - {{- if $env.value -}} - {{- $_ := set $envs $env.name $env.value -}} - {{- end -}} -{{- end -}} - -{{- range $key := .Values.requiredEnvList -}} -{{- $_ := required (printf "env.%s is required" $key) (index $envs $key) -}} -{{- end -}} - -{{- end -}} - -{{- end }} - -{{ $_ := include "chartty.envs.check" . }} \ No newline at end of file diff --git a/deploy/helm/saas/Chart.yaml b/deploy/helm/saas/Chart.yaml deleted file mode 100644 index 6c6f51fc0..000000000 --- a/deploy/helm/saas/Chart.yaml +++ /dev/null @@ -1,6 +0,0 @@ -apiVersion: v2 -appVersion: v2.3.1 -description: A Helm chart for bk user saas -name: bkusersaas -type: application -version: 1.0.0 diff --git a/deploy/helm/saas/values.yaml b/deploy/helm/saas/values.yaml deleted file mode 100644 index b5febf931..000000000 --- a/deploy/helm/saas/values.yaml +++ /dev/null @@ -1,213 +0,0 @@ -# 全局变量,通常用于多个 Chart 之间共享 -global: - imagePullSecrets: [] - # imagePullSecrets, 预先创建的 imagePullSecrets, 将直接被添加到 chartty.imagePullSecretNames 中. - # - name: "secret-a" - # - name: "secret-b" - - # credential, 用于创建独享的 Secret 资源 - imageCredentials: - # 当且仅当 enabled 为 true 时,会生成 dockerconfigjson 类型的 Secret 资源, 并在 chartty.imagePullSecretNames 添加该名称. - enabled: false - password: "" - registry: "" - username: "" - name: "" - - # 全局镜像配置 - image: - registry: "ccr.ccs.tencentyun.com/bk.io" - pullPolicy: Always - - # 全局环境变量,当 `env` 指定时,`global.env` 内相同 key 值变量将被覆盖 - env: {} - - # 默认的全局根域 - sharedDomain: "" - -# 缺省实例数 -replicaCount: 1 - -image: - name: bk-user-saas - -# 用来覆盖 Chart 名 -nameOverride: "" -# 用来覆盖 fullName (通常是 release-chart 拼接) -fullnameOverride: "" - -# 是否自动创建 serviceAccount -serviceAccount: - create: true - annotations: {} - name: "" - -podAnnotations: {} - -podSecurityContext: {} - -# 支持定义 labels -podLabels: {} - -securityContext: {} - -service: - type: ClusterIP - port: 80 - -#--------------- -# 调度 -#--------------- -nodeSelector: {} - -tolerations: [] - -affinity: {} - -#--------------- -# 环境变量 -# 除 global.env 和 env 外 -# 其余变量定义均不去重,请手动确保无变量名冲突 -#--------------- - -# key-value 结构渲染 -env: - # ------------- - # 默认配置,不了解详情时请不要修改 - # ------------- - BK_APP_CODE: "bk-user" - DJANGO_SETTINGS_MODULE: "bkuser_shell.config.overlays.prod" - BKAPP_BK_USER_CORE_API_HOST: "http://bkuserapi-web" - BK_LOGIN_API_URL: "http://bk-login-web" - # 容器化版本默认采用子域名形式暴露服务 - SITE_URL: "/" - -envFrom: [] - -# 提供原生的 env 写法 -extrasEnv: [] - -# 额外提供一种基于 sharedDomain 自动生成的 URL 类型环境变量 -sharedUrlEnvMap: {} - # BK_JOB_HOST: "jobee-test" - -# 标识必填的环境变量列表 -requiredEnvList: [] - -#--------------- -# 进程定义 -#-------------- -httpPort: 8000 -database: - preferName: bk-user-saas - -# 定义应用内的多个进程 -processes: - web: - ingress: - enabled: true - host: "bkuser.{{ .Values.global.sharedDomain }}" - paths: ["/"] - replicas: 1 - resources: - limits: - cpu: 1024m - memory: 1024Mi - requests: - cpu: 200m - memory: 128Mi - readinessProbe: - tcpSocket: - port: 8000 - initialDelaySeconds: 5 - periodSeconds: 30 - livenessProbe: - tcpSocket: - port: 8000 - initialDelaySeconds: 5 - periodSeconds: 30 - -# 部署前钩子 -preRunHooks: - db-migrate: - weight: 1 - enabled: true - position: "pre-install,pre-upgrade" - command: - - bash - args: - - -c - - python manage.py migrate - -# 支持定义多个 cronJobs -cronJobs: - jobs: [] - # - name: example-script - # enabled: false - # schedule: "*/30 * * * *" - # command: ["echo"] - # args: - # - "hello" - # failedJobsHistoryLimit: 1 - # successfulJobsHistoryLimit: 3 - # concurrencyPolicy: Forbid - -# 挂载配置 -volumes: [] -volumeMounts: [] - -# 支持定义 configmaps -configMaps: [] - # - name: - # files: - # - name: "test.yaml" - # format: "yaml" - # data: - # debug: true - -# 当 Chart 独立部署时,默认关闭内建存储 -mariadb: - enabled: false - -## ServiceMonitor configuration -## -serviceMonitor: - ## @param serviceMonitor.enabled Creates a ServiceMonitor to monitor kube-state-metrics - ## - enabled: false - ## @param serviceMonitor.jobLabel The name of the label on the target service to use as the job name in prometheus. - ## - jobLabel: "" - ## @param serviceMonitor.interval Scrape interval (use by default, falling back to Prometheus' default) - ## ref: https://github.com/coreos/prometheus-operator/blob/master/Documentation/api.md#endpoint - ## e.g: - ## interval: 10s - ## - interval: "" - ## @param serviceMonitor.scrapeTimeout Timeout after which the scrape is ended - ## ref: https://github.com/coreos/prometheus-operator/blob/master/Documentation/api.md#endpoint - ## e.g: - ## scrapeTimeout: 10s - ## - scrapeTimeout: "" - ## @param serviceMonitor.selector ServiceMonitor selector labels - ## ref: https://github.com/bitnami/charts/tree/master/bitnami/prometheus-operator#prometheus-configuration - ## e.g: - ## selector: - ## prometheus: my-prometheus - ## - selector: {} - ## @param serviceMonitor.honorLabels Honor metrics labels - ## ref: https://github.com/coreos/prometheus-operator/blob/master/Documentation/api.md#endpoint - ## e.g: - ## honorLabels: false - ## - honorLabels: false - ## @param serviceMonitor.relabelings ServiceMonitor relabelings - ## ref: https://github.com/coreos/prometheus-operator/blob/master/Documentation/api.md#relabelconfig - ## - relabelings: [] - ## @param serviceMonitor.metricRelabelings ServiceMonitor metricRelabelings - ## ref: https://github.com/coreos/prometheus-operator/blob/master/Documentation/api.md#relabelconfig - ## - metricRelabelings: [] \ No newline at end of file diff --git a/docs/changelogs/CHANGELOG-2.0.0.md b/docs/changelogs/CHANGELOG-2.0.0.md new file mode 100644 index 000000000..5541354f9 --- /dev/null +++ b/docs/changelogs/CHANGELOG-2.0.0.md @@ -0,0 +1,18 @@ + +# Changelog [2.0.0] - 2020-02-27 + + + +### NEW + +- 支持用户目录,可以创建不同的目录隔离组织架构 +- 支持更为详尽的审计信息 +- 支持 LDAP & MAD 用户目录登陆 & 数据同步 + +### OPTIMIZATION + +- 性能飞跃,操作如丝般顺滑 +- 架构升级,API 层和 SaaS 完全分离,调用不再混乱 +- 前端大幅重构优化,更美观合理的 UI 交互 +- 数据导入导出加强,速度提升 + diff --git a/docs/changelogs/CHANGELOG-2.0.10.md b/docs/changelogs/CHANGELOG-2.0.10.md new file mode 100644 index 000000000..007aadffc --- /dev/null +++ b/docs/changelogs/CHANGELOG-2.0.10.md @@ -0,0 +1,16 @@ + +# Changelog [2.0.10] - 2020-06-24 + +## API + +### FIX + +- 修复可能存在的重置密码邮箱爆破问题 +- 修复数据迁移脚本执行报错问题 +- 修复密码过期重置不生效问题 + +### OPTIMIZATION + +- 加强重要字段后端数据校验 +- 初始用户密码修改为 12 位 + diff --git a/docs/changelogs/CHANGELOG-2.0.11.md b/docs/changelogs/CHANGELOG-2.0.11.md new file mode 100644 index 000000000..9f90d3f1c --- /dev/null +++ b/docs/changelogs/CHANGELOG-2.0.11.md @@ -0,0 +1,15 @@ + +# Changelog [2.0.11] - 2020-08-03 + +## API + +### FIX + +- 支持自定义字段唯一性校验 +- 增加自定义目录支持 +- 完善产品后端数据国际化 +- 优化多目录开启时全量数据返回效率 +- 修复搜索用户信息时手机号无法展示问题 +- 修复部门新建后无法在列表中显示的问题 +- 修复旧版数据迁移时密码过期导致人员详情无法展示的问题 + diff --git a/docs/changelogs/CHANGELOG-2.0.12.md b/docs/changelogs/CHANGELOG-2.0.12.md new file mode 100644 index 000000000..097e3465a --- /dev/null +++ b/docs/changelogs/CHANGELOG-2.0.12.md @@ -0,0 +1,10 @@ + +# Changelog [2.0.12] - 2020-08-20 + +## API + +### FIX + +- 修复部门新建后无法在列表中显示的问题 +- 修复旧版数据迁移时密码过期导致人员详情无法展示的问题 + diff --git a/docs/changelogs/CHANGELOG-2.0.13.md b/docs/changelogs/CHANGELOG-2.0.13.md new file mode 100644 index 000000000..3827cf913 --- /dev/null +++ b/docs/changelogs/CHANGELOG-2.0.13.md @@ -0,0 +1,13 @@ + +# Changelog [2.0.13] - 2020-09-15 + +## API + +### OPTIMIZATION + +- 优化项目依赖,提升企业版部署安装速度 + +### FIX + +- 修复获取非默认目录的部门下用户时 username 字段拼接错误问题 + diff --git a/docs/changelogs/CHANGELOG-2.0.4.md b/docs/changelogs/CHANGELOG-2.0.4.md new file mode 100644 index 000000000..1c312354f --- /dev/null +++ b/docs/changelogs/CHANGELOG-2.0.4.md @@ -0,0 +1,17 @@ + +# Changelog [2.0.4] - 2020-03-09 + +## API + +### FIX + +- 修复 v1 API 拉取子部门为空的 bug +- 修复目录数据导入时由于旧数据的异常报错 + +## SaaS + +### FIX + +- 修复旧数据格式的用户搜索错误问题 +- 修复数据导出时,多个部门没有使用正确的分隔符问题 + diff --git a/docs/changelogs/CHANGELOG-2.0.5.md b/docs/changelogs/CHANGELOG-2.0.5.md new file mode 100644 index 000000000..e14ef0e73 --- /dev/null +++ b/docs/changelogs/CHANGELOG-2.0.5.md @@ -0,0 +1,27 @@ + +# Changelog [2.0.5] - 2020-03-10 + +## API + +### FIX + +- 修复从平台解绑微信无效问题 + +### OPTIMIZATION + +- 提升拉取人员列表接口性能 + +## SaaS + +### NEW + +- 支持多种登录 + +### FIX + +- 修复因为 AJAX_URL 变量缺失导致的 502 问题 + +### OPTIMIZATION + +- 前后端分离开发 + diff --git a/docs/changelogs/CHANGELOG-2.0.6.md b/docs/changelogs/CHANGELOG-2.0.6.md new file mode 100644 index 000000000..d4184c048 --- /dev/null +++ b/docs/changelogs/CHANGELOG-2.0.6.md @@ -0,0 +1,16 @@ + +# Changelog [2.0.6] - 2020-03-13 + +## API + +### FIX + +- 修复数据导入时用户名缺失问题 + +## SaaS + +### FIX + +- 修复目录导出时,组织无法展开问题 +- 安全加固 修复可能存在的重置密码邮箱爆破问题 + diff --git a/docs/changelogs/CHANGELOG-2.0.7.md b/docs/changelogs/CHANGELOG-2.0.7.md new file mode 100644 index 000000000..999999d97 --- /dev/null +++ b/docs/changelogs/CHANGELOG-2.0.7.md @@ -0,0 +1,17 @@ + +# Changelog [2.0.7] - 2020-04-13 + +## API + +### FIX + +- 修复拉取子部门-父部门时未隐藏删除部门的问题 +- 修复 fuzzy_lookups 类型校验错误问题 + +## SaaS + +### NEW + +- 自定义目录支持 +- 后端国际化支持 + diff --git a/docs/changelogs/CHANGELOG-2.0.8.md b/docs/changelogs/CHANGELOG-2.0.8.md new file mode 100644 index 000000000..73c307e3b --- /dev/null +++ b/docs/changelogs/CHANGELOG-2.0.8.md @@ -0,0 +1,28 @@ + +# Changelog [2.0.8] - 2020-04-15 + +## API + +### FIX + +- 修复 MAD 类型目录的同步功能 +- 修复当目录同步周期设置为 0 时,定时任务未被删除的问题 + +### OPTIMIZATION + +- 同步写数据时添加事务,保证错误时正常回滚 + +### NEW + +- 增加关系API,加速权限中心同步 + +## SaaS + +### NEW + +- 增加版本日志功能 + +### FIX + +- 修复搜索时更新用户无法及时更新问题 + diff --git a/docs/changelogs/CHANGELOG-2.0.9.md b/docs/changelogs/CHANGELOG-2.0.9.md new file mode 100644 index 000000000..eeb78557d --- /dev/null +++ b/docs/changelogs/CHANGELOG-2.0.9.md @@ -0,0 +1,13 @@ + +# Changelog [2.0.9] - 2020-05-08 + +## API + +### OPTIMIZATION + +- 重构数据同步逻辑,大数据量同步加速 +- 拉取用户全量接口支持多域 +- v1 版旧接口部分支持多域 +- 人员拉取接口支持通过 header 设置 json 或 jsonp +- 完善 healthz 逻辑,支持依赖服务探测 + diff --git a/docs/changelogs/CHANGELOG-2.2.0.md b/docs/changelogs/CHANGELOG-2.2.0.md new file mode 100644 index 000000000..16fcc501d --- /dev/null +++ b/docs/changelogs/CHANGELOG-2.2.0.md @@ -0,0 +1,23 @@ + +# Changelog [2.2.0] - 2020-11-03 + +## API + +### FIX + +- 修复用户删除后审计记录错乱问题 + +### NEW + +- 企业版 3.0 正式接入权限中心 + +## SaaS + +### FIX + +- 修正页面 footer + +### NEW + +- 权限中心社区版完全支持 + diff --git a/docs/changelogs/CHANGELOG-2.2.1.md b/docs/changelogs/CHANGELOG-2.2.1.md new file mode 100644 index 000000000..eaa0bcc6c --- /dev/null +++ b/docs/changelogs/CHANGELOG-2.2.1.md @@ -0,0 +1,21 @@ + +# Changelog [2.2.1] - 2020-11-25 + +## API + +### NEW + +- 增加了权限中心 search instances 回调接口 +- 增加了权限中心回调接口 basic auth 鉴权 + +### FIX + +- 修复了数据迁移时 extras 字段格式未更新问题 + +## SaaS + +### FIX + +- 修复登录续期框高度问题 +- 修复文档链接 + diff --git a/docs/changelogs/CHANGELOG-2.2.2.md b/docs/changelogs/CHANGELOG-2.2.2.md new file mode 100644 index 000000000..a7dd8cec6 --- /dev/null +++ b/docs/changelogs/CHANGELOG-2.2.2.md @@ -0,0 +1,18 @@ + +# Changelog [2.2.2] - 2020-12-03 + +## API + +### NEW + +- 增加了 best_match 用于模糊搜索时最短匹配 +- 增加了社区版数据迁移脚本 +- 增加了 since/until 支持通过创建/更新时间过滤搜索 +- 增加了上云版 leader 同步 + +## SaaS + +### OPTIMIZATION + +- 优化审计记录拉取速度 + diff --git a/docs/changelogs/CHANGELOG-2.2.3.md b/docs/changelogs/CHANGELOG-2.2.3.md new file mode 100644 index 000000000..6c0952134 --- /dev/null +++ b/docs/changelogs/CHANGELOG-2.2.3.md @@ -0,0 +1,16 @@ + +# Changelog [2.2.3] - 2020-12-15 + +## API + +### OPTIMIZATION + +- 删除目录调整为软删除 +- 社区版 admin 初始账号密码支持从环境变量指定 + +## SaaS + +### FIX + +- 修复目录拉取人员数量为零的问题 + diff --git a/docs/changelogs/CHANGELOG-2.2.4.md b/docs/changelogs/CHANGELOG-2.2.4.md new file mode 100644 index 000000000..810a5a3d8 --- /dev/null +++ b/docs/changelogs/CHANGELOG-2.2.4.md @@ -0,0 +1,16 @@ + +# Changelog [2.2.4] - 2020-12-31 + +## API + +### OPTIMIZATION + +- 优化 AD/LDAP 目录数据同步 & 登录 +- 优化上云版数据同步逻辑 + +## SaaS + +### FIX + +- 修复重置密码页面需要登录的问题 + diff --git a/docs/changelogs/CHANGELOG-2.2.5.md b/docs/changelogs/CHANGELOG-2.2.5.md new file mode 100644 index 000000000..65d91efa2 --- /dev/null +++ b/docs/changelogs/CHANGELOG-2.2.5.md @@ -0,0 +1,34 @@ + +# Changelog [2.2.5] - 2021-04-01 + +## API + +### FIX + +- 修复exact_lookups fuzzy_lookups 注入安全问题 +- 修复上云版 API token 失效问题 +- 修复修复本地目录导入时,手机号添加国际号码段异常问题 + +### OPTIMIZATION + +- 优化目录禁用功能逻辑 +- 优化list_users API 添加 extras 默认值填充 +- 优化上云版同步人员信息后刷新缓存 +- 重构 Excel 人员导入逻辑,解析更精准,导入更完整 + +## SaaS + +### FIX + +- 修复用户列表设置表字段后,用户列表的组织列显示异常 +- 修复点击禁用,启用的过程中会出现白框 +- 修复添加'枚举值 & 必填'自定义字段,前端表单无法创建用户 +- 修复用户重置密码时会影响自定义字段 +- 修复目录配置保存失败,需要重复保存一次 +- 修复添加必填的自定义字段后,数据导入的 Excel 显示错位 + +### OPTIMIZATION + +- 优化创建组织时,添加键盘响应 +- 优化 ad 配置指引文字 + diff --git a/docs/changelogs/CHANGELOG-2.2.6.md b/docs/changelogs/CHANGELOG-2.2.6.md new file mode 100644 index 000000000..1ac32b1c1 --- /dev/null +++ b/docs/changelogs/CHANGELOG-2.2.6.md @@ -0,0 +1,22 @@ + +# Changelog [2.2.6] - 2021-05-20 + +## API + +### NEW + +- 支持后台环境变量配置,启停全局邮件通知 + +### OPTIMIZATION + +- 支持 LDAP/AD 数据分页拉取,绕开 1000 条数量限制 + +## SaaS + +### NEW + +- 增加'查看'类权限项 +- 增加用户登录审计 +- 审计信息增加客户端来源 IP +- 支持用户重置密码不能与最近三次密码重复 + diff --git a/docs/changelogs/CHANGELOG-2.3.0.md b/docs/changelogs/CHANGELOG-2.3.0.md new file mode 100644 index 000000000..0093212f7 --- /dev/null +++ b/docs/changelogs/CHANGELOG-2.3.0.md @@ -0,0 +1,36 @@ + +# Changelog [2.3.0] - 2021-10-22 + +## API + +### NEW + +- 支持传递参数,可以拉取已软删除数据 #1 +- 支持恢复已删除的数据 #15 +- 支持记录 LDAP/AD 同步组织架构/人员信息的结构化日志 #27 + +### OPTIMIZATION + +- 优化数据源同步任务为后台执行 #32 + +### FIX + +- 修正 SettingMeta 默认路径参数为 id #45 +- 修正 API /api/v2/batch/profiles/ 中 swagger 参数 query_ids 缺失问题 #26 + +## SaaS + +### NEW + +- 支持数据源同步任务页面查看 #32 +- 支持登录日志导出 #32 + +### OPTIMIZATION + +- 修改页面拉取上级组件,从全量拉取改为类似【人员选择器】的分页拉取组件 #55 + +### FIX + +- 修复数值型自定义字段在页面上输入时没有异常提示 #101 +- 修复“从其他组织拉取用户”异常问题 #102 + diff --git a/docs/changelogs/CHANGELOG-2.3.1.md b/docs/changelogs/CHANGELOG-2.3.1.md new file mode 100644 index 000000000..8b92587ea --- /dev/null +++ b/docs/changelogs/CHANGELOG-2.3.1.md @@ -0,0 +1,32 @@ + +# Changelog [2.3.1] - 2021-11-05 + +## API + +### NEW + +- API 支持通过 POST body 筛选数据 #88 +- 支持审计记录失败内容(仅数据) #71 + +### FIX + +- 修复 ldap/mad 测试连接按钮报错问题 #129 +- 修复手动关闭权限中心时,目录新建关联权限报错问题 #99 +- 修复部门查询接口 ?lookup_field=name,当部门名称中含有 . 时返回 404 问题 #147 +- 修复 Excel 模板字段名与内置字段名不统一,导致导入失败问题 #150 + +### OPTIMIZATION + +- 将「密码过期判断」逻辑调整到「密码校验成功」后,规避可能存在的安全风险 #137 + +## SaaS + +### NEW + +- 支持搜索已删除的数据 #80 +- 支持恢复已删除用户 #15 + +### FIX + +- 增大默认的 CPU 限制,保证容器正常启动 + diff --git a/docs/changelogs/CHANGELOG-2.3.2.md b/docs/changelogs/CHANGELOG-2.3.2.md new file mode 100644 index 000000000..910c42e3f --- /dev/null +++ b/docs/changelogs/CHANGELOG-2.3.2.md @@ -0,0 +1,38 @@ + +# Changelog [2.3.2] - 2022-01-05 + + + +### OPTIMIZATION + +- 重构 Helm Chart + +## API + +### OPTIMIZATION + +- 登录时只查询近期一段时间的审计信息 #116 +- 支持在「密码配置」中设置「密码最大重复次数」 #149 +- 数据同步增加默认重试次数,当所有重试都失败时处理异常 #197 +- LDAP 支持自定义字段 #107 +- 插件支持通过 settings.yaml 增加配置项 #43 + +## SaaS + +### OPTIMIZATION + +- 登录支持多种 bk_token 后端 +- 登录及修改密码页面中的密码使用 base64 编码 #126 + +## Login + +### NEW + +- 支持密码过期时提供密码重置链接 #38 +- 支持初始密码强制修改 + +### OPTIMIZATION + +- 重构 Login 项目部分代码,引入 blue-krill 加密 +- 重构社区登录页面 #144 + diff --git a/docs/release.md b/docs/release.md index f0d200ac3..090278219 100644 --- a/docs/release.md +++ b/docs/release.md @@ -1,364 +1,24 @@ -# Changelog - - -## [Version: 2.3.1] - 2021-11-05 - - -### API - - -- [NEW] API 支持通过 POST body 筛选数据 [#88](https://github.com/TencentBlueKing/bk-user/issues/88) -- [NEW] 支持审计记录失败内容(仅数据) [#71](https://github.com/TencentBlueKing/bk-user/issues/71) -- [FIX] 修复 ldap/mad 测试连接按钮报错问题 [#129](https://github.com/TencentBlueKing/bk-user/issues/129) -- [FIX] 修复手动关闭权限中心时,目录新建关联权限报错问题 [#99](https://github.com/TencentBlueKing/bk-user/issues/99) -- [FIX] 修复部门查询接口 ?lookup_field=name,当部门名称中含有 . 时返回 404 问题 [#147](https://github.com/TencentBlueKing/bk-user/issues/147) -- [FIX] 修复 Excel 模板字段名与内置字段名不统一,导致导入失败问题 [#150](https://github.com/TencentBlueKing/bk-user/issues/150) -- [OPTIMIZATION] 将「密码过期判断」逻辑调整到「密码校验成功」后,规避可能存在的安全风险 [#137](https://github.com/TencentBlueKing/bk-user/issues/137) - - -### SaaS - - -- [NEW] 支持搜索已删除的数据 [#80](https://github.com/TencentBlueKing/bk-user/issues/80) -- [NEW] 支持恢复已删除用户 [#15](https://github.com/TencentBlueKing/bk-user/issues/15) -- [FIX] 增大默认的 CPU 限制,保证容器正常启动 - - - -## [Version: 2.3.0] - 2021-10-22 - - -### API - - -- [NEW] 支持传递参数,可以拉取已软删除数据 [#1](https://github.com/TencentBlueKing/bk-user/issues/1) -- [NEW] 支持恢复已删除的数据 [#15](https://github.com/TencentBlueKing/bk-user/issues/15) -- [NEW] 支持记录 LDAP/AD 同步组织架构/人员信息的结构化日志 [#27](https://github.com/TencentBlueKing/bk-user/issues/27) -- [OPTIMIZATION] 优化数据源同步任务为后台执行 [#32](https://github.com/TencentBlueKing/bk-user/issues/32) -- [FIX] 修正 SettingMeta 默认路径参数为 id [#45](https://github.com/TencentBlueKing/bk-user/issues/45) -- [FIX] 修正 API /api/v2/batch/profiles/ 中 swagger 参数 query_ids 缺失问题 [#26](https://github.com/TencentBlueKing/bk-user/issues/26) - - -### SaaS - - -- [NEW] 支持数据源同步任务页面查看 [#32](https://github.com/TencentBlueKing/bk-user/issues/32) -- [NEW] 支持登录日志导出 [#32](https://github.com/TencentBlueKing/bk-user/issues/32) -- [OPTIMIZATION] 修改页面拉取上级组件,从全量拉取改为类似【人员选择器】的分页拉取组件 [#55](https://github.com/TencentBlueKing/bk-user/issues/55) -- [FIX] 修复数值型自定义字段在页面上输入时没有异常提示 [#101](https://github.com/TencentBlueKing/bk-user/issues/101) -- [FIX] 修复“从其他组织拉取用户”异常问题 [#102](https://github.com/TencentBlueKing/bk-user/issues/102) - - - -## [Version: 2.2.6] - 2021-05-20 - - -### API - - -- [NEW] 支持后台环境变量配置,启停全局邮件通知 -- [OPTIMIZATION] 支持 LDAP/AD 数据分页拉取,绕开 1000 条数量限制 - - -### SaaS - - -- [NEW] 增加'查看'类权限项 -- [NEW] 增加用户登录审计 -- [NEW] 审计信息增加客户端来源 IP -- [NEW] 支持用户重置密码不能与最近三次密码重复 - - - -## [Version: 2.2.5] - 2021-04-01 - - -### API - - -- [FIX] 修复exact_lookups fuzzy_lookups 注入安全问题 -- [FIX] 修复上云版 API token 失效问题 -- [FIX] 修复修复本地目录导入时,手机号添加国际号码段异常问题 -- [OPTIMIZATION] 优化目录禁用功能逻辑 -- [OPTIMIZATION] 优化list_users API 添加 extras 默认值填充 -- [OPTIMIZATION] 优化上云版同步人员信息后刷新缓存 -- [OPTIMIZATION] 重构 Excel 人员导入逻辑,解析更精准,导入更完整 - - -### SaaS - - -- [FIX] 修复用户列表设置表字段后,用户列表的组织列显示异常 -- [FIX] 修复点击禁用,启用的过程中会出现白框 -- [FIX] 修复添加'枚举值 & 必填'自定义字段,前端表单无法创建用户 -- [FIX] 修复用户重置密码时会影响自定义字段 -- [FIX] 修复目录配置保存失败,需要重复保存一次 -- [FIX] 修复添加必填的自定义字段后,数据导入的 Excel 显示错位 -- [OPTIMIZATION] 优化创建组织时,添加键盘响应 -- [OPTIMIZATION] 优化 ad 配置指引文字 - - - -## [Version: 2.2.4] - 2020-12-31 - - -### API - - -- [OPTIMIZATION] 优化 AD/LDAP 目录数据同步 & 登录 -- [OPTIMIZATION] 优化上云版数据同步逻辑 - - -### SaaS - - -- [FIX] 修复重置密码页面需要登录的问题 - - - -## [Version: 2.2.3] - 2020-12-15 - - -### API - - -- [OPTIMIZATION] 删除目录调整为软删除 -- [OPTIMIZATION] 社区版 admin 初始账号密码支持从环境变量指定 - - -### SaaS - - -- [FIX] 修复目录拉取人员数量为零的问题 - - - -## [Version: 2.2.2] - 2020-12-03 - - -### API - - -- [NEW] 增加了 best_match 用于模糊搜索时最短匹配 -- [NEW] 增加了社区版数据迁移脚本 -- [NEW] 增加了 since/until 支持通过创建/更新时间过滤搜索 -- [NEW] 增加了上云版 leader 同步 - - -### SaaS - - -- [OPTIMIZATION] 优化审计记录拉取速度 - - - -## [Version: 2.2.1] - 2020-11-25 - - -### API - - -- [NEW] 增加了权限中心 search instances 回调接口 -- [NEW] 增加了权限中心回调接口 basic auth 鉴权 -- [FIX] 修复了数据迁移时 extras 字段格式未更新问题 - - -### SaaS - - -- [FIX] 修复登录续期框高度问题 -- [FIX] 修复文档链接 - - - -## [Version: 2.2.0] - 2020-11-03 - - -### API - - -- [FIX] 修复用户删除后审计记录错乱问题 -- [NEW] 企业版 3.0 正式接入权限中心 - - -### SaaS - - -- [FIX] 修正页面 footer -- [NEW] 权限中心社区版完全支持 - - - -## [Version: 2.0.13] - 2020-09-15 - - -### API - - -- [OPTIMIZATION] 优化项目依赖,提升企业版部署安装速度 -- [FIX] 修复获取非默认目录的部门下用户时 username 字段拼接错误问题 - - - -## [Version: 2.0.12] - 2020-08-20 - - -### API - - -- [FIX] 修复部门新建后无法在列表中显示的问题 -- [FIX] 修复旧版数据迁移时密码过期导致人员详情无法展示的问题 - - - -## [Version: 2.0.11] - 2020-08-03 - - -### API - - -- [FIX] 支持自定义字段唯一性校验 -- [FIX] 增加自定义目录支持 -- [FIX] 完善产品后端数据国际化 -- [FIX] 优化多目录开启时全量数据返回效率 -- [FIX] 修复搜索用户信息时手机号无法展示问题 -- [FIX] 修复部门新建后无法在列表中显示的问题 -- [FIX] 修复旧版数据迁移时密码过期导致人员详情无法展示的问题 - - - -## [Version: 2.0.10] - 2020-06-24 - - -### API - - -- [FIX] 修复可能存在的重置密码邮箱爆破问题 -- [FIX] 修复数据迁移脚本执行报错问题 -- [FIX] 修复密码过期重置不生效问题 -- [OPTIMIZATION] 加强重要字段后端数据校验 -- [OPTIMIZATION] 初始用户密码修改为 12 位 - - - -## [Version: 2.0.9] - 2020-05-08 - - -### API - - -- [OPTIMIZATION] 重构数据同步逻辑,大数据量同步加速 -- [OPTIMIZATION] 拉取用户全量接口支持多域 -- [OPTIMIZATION] v1 版旧接口部分支持多域 -- [OPTIMIZATION] 人员拉取接口支持通过 header 设置 json 或 jsonp -- [OPTIMIZATION] 完善 healthz 逻辑,支持依赖服务探测 - - - -## [Version: 2.0.8] - 2020-04-15 - - -### API - - -- [FIX] 修复 MAD 类型目录的同步功能 -- [FIX] 修复当目录同步周期设置为 0 时,定时任务未被删除的问题 -- [OPTIMIZATION] 同步写数据时添加事务,保证错误时正常回滚 -- [NEW] 增加关系API,加速权限中心同步 - - -### SaaS - - -- [NEW] 增加版本日志功能 -- [FIX] 修复搜索时更新用户无法及时更新问题 - - - -## [Version: 2.0.7] - 2020-04-13 - - -### API - - -- [FIX] 修复拉取子部门-父部门时未隐藏删除部门的问题 -- [FIX] 修复 fuzzy_lookups 类型校验错误问题 - - -### SaaS - - -- [NEW] 自定义目录支持 -- [NEW] 后端国际化支持 - - - -## [Version: 2.0.6] - 2020-03-13 - - -### API - - -- [FIX] 修复数据导入时用户名缺失问题 - - -### SaaS - - -- [FIX] 修复目录导出时,组织无法展开问题 -- [FIX] 安全加固 修复可能存在的重置密码邮箱爆破问题 - - - -## [Version: 2.0.5] - 2020-03-10 - - -### API - - -- [FIX] 修复从平台解绑微信无效问题 -- [OPTIMIZATION] 提升拉取人员列表接口性能 - - -### SaaS - - -- [NEW] 支持多种登录 -- [FIX] 修复因为 AJAX_URL 变量缺失导致的 502 问题 -- [OPTIMIZATION] 前后端分离开发 - - - -## [Version: 2.0.4] - 2020-03-09 - - -### API - - -- [FIX] 修复 v1 API 拉取子部门为空的 bug -- [FIX] 修复目录数据导入时由于旧数据的异常报错 - - -### SaaS - - -- [FIX] 修复旧数据格式的用户搜索错误问题 -- [FIX] 修复数据导出时,多个部门没有使用正确的分隔符问题 - - - -## [Version: 2.0.0] - 2020-02-27 - - - -- [NEW] 支持用户目录,可以创建不同的目录隔离组织架构 -- [NEW] 支持更为详尽的审计信息 -- [NEW] 支持 LDAP & MAD 用户目录登陆 & 数据同步 -- [OPTIMIZATION] 性能飞跃,操作如丝般顺滑 -- [OPTIMIZATION] 架构升级,API 层和 SaaS 完全分离,调用不再混乱 -- [OPTIMIZATION] 前端大幅重构优化,更美观合理的 UI 交互 -- [OPTIMIZATION] 数据导入导出加强,速度提升 - - - +# CHANGELOGs + +- [CHANGELOG-2.3.2.md](changelogs/CHANGELOG-2.3.2.md) +- [CHANGELOG-2.3.1.md](changelogs/CHANGELOG-2.3.1.md) +- [CHANGELOG-2.3.0.md](changelogs/CHANGELOG-2.3.0.md) +- [CHANGELOG-2.2.6.md](changelogs/CHANGELOG-2.2.6.md) +- [CHANGELOG-2.2.5.md](changelogs/CHANGELOG-2.2.5.md) +- [CHANGELOG-2.2.4.md](changelogs/CHANGELOG-2.2.4.md) +- [CHANGELOG-2.2.3.md](changelogs/CHANGELOG-2.2.3.md) +- [CHANGELOG-2.2.2.md](changelogs/CHANGELOG-2.2.2.md) +- [CHANGELOG-2.2.1.md](changelogs/CHANGELOG-2.2.1.md) +- [CHANGELOG-2.2.0.md](changelogs/CHANGELOG-2.2.0.md) +- [CHANGELOG-2.0.13.md](changelogs/CHANGELOG-2.0.13.md) +- [CHANGELOG-2.0.12.md](changelogs/CHANGELOG-2.0.12.md) +- [CHANGELOG-2.0.11.md](changelogs/CHANGELOG-2.0.11.md) +- [CHANGELOG-2.0.10.md](changelogs/CHANGELOG-2.0.10.md) +- [CHANGELOG-2.0.9.md](changelogs/CHANGELOG-2.0.9.md) +- [CHANGELOG-2.0.8.md](changelogs/CHANGELOG-2.0.8.md) +- [CHANGELOG-2.0.7.md](changelogs/CHANGELOG-2.0.7.md) +- [CHANGELOG-2.0.6.md](changelogs/CHANGELOG-2.0.6.md) +- [CHANGELOG-2.0.5.md](changelogs/CHANGELOG-2.0.5.md) +- [CHANGELOG-2.0.4.md](changelogs/CHANGELOG-2.0.4.md) +- [CHANGELOG-2.0.0.md](changelogs/CHANGELOG-2.0.0.md) diff --git a/pyproject.toml b/pyproject.toml index 2ddaf64e4..727e5db00 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "蓝鲸用户管理" -version = "2.3.1" +version = "2.3.2" description = "project description file for ci" authors = ["IMBlues "] @@ -28,6 +28,7 @@ types-pyyaml = "^5.4.3" types-pymysql = "^1.0.0" types-redis = "^3.5.4" types-toml = "^0.1.3" +types-cachetools = "^4.2.4" [tool.black] line-length = 119 diff --git a/src/api/Dockerfile b/src/api/Dockerfile index a43cd5c38..baa5cd1b9 100644 --- a/src/api/Dockerfile +++ b/src/api/Dockerfile @@ -9,7 +9,7 @@ RUN rm /etc/apt/sources.list && \ RUN mkdir ~/.pip && printf '[global]\nindex-url = https://mirrors.tencent.com/pypi/simple/' > ~/.pip/pip.conf -RUN apt-get update && apt-get install -y gcc +RUN apt-get update && apt-get install -y gcc gettext ENV LC_ALL=C.UTF-8 \ LANG=C.UTF-8 @@ -20,7 +20,7 @@ RUN pip install poetry==1.1.7 WORKDIR /app COPY src/api/pyproject.toml /app COPY src/api/poetry.lock /app -RUN poetry config virtualenvs.create false && poetry install --no-dev +RUN poetry config experimental.new-installer false && poetry config virtualenvs.create false && poetry install --no-dev COPY src/api/wsgi.py /app COPY src/api/bkuser_core /app/bkuser_core diff --git a/src/api/bin/install_ci_dependencies.sh b/src/api/bin/install_ci_dependencies.sh new file mode 100644 index 000000000..894de6074 --- /dev/null +++ b/src/api/bin/install_ci_dependencies.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +poetry export -f requirements.txt --dev --without-hashes -o requirements.txt --no-ansi + +sed -i '/^--extra-index-url*/d' requirements.txt +sed -i '/gevent/d' requirements.txt +sed -i '/greenlet/d' requirements.txt +sed -i '/gunicorn/d' requirements.txt + +pip install -r requirements.txt -i https://pypi.org/simple/ diff --git a/src/api/bin/start.sh b/src/api/bin/start.sh index c58bb1660..935b6dadf 100755 --- a/src/api/bin/start.sh +++ b/src/api/bin/start.sh @@ -1,2 +1,5 @@ #!/bin/bash -gunicorn wsgi -w 8 --threads 2 --max-requests 1024 --max-requests-jitter 50 --worker-class gevent -b :8000 --access-logfile - --error-logfile - --access-logformat '[%(h)s] %({request_id}i)s %(u)s %(t)s "%(r)s" %(s)s %(D)s %(b)s "%(f)s" "%(a)s"' +python manage.py compilemessages + +LISTEN_PORT="${PORT:=8000}" +gunicorn wsgi -w 8 --threads 2 --max-requests 1024 --max-requests-jitter 50 --worker-class gevent -b :$LISTEN_PORT --access-logfile - --error-logfile - --access-logformat '[%(h)s] %({request_id}i)s %(u)s %(t)s "%(r)s" %(s)s %(D)s %(b)s "%(f)s" "%(a)s"' diff --git a/src/api/bkuser_core/config/overlays/stag.py b/src/api/bkuser_core/apis/serializers.py similarity index 55% rename from src/api/bkuser_core/config/overlays/stag.py rename to src/api/bkuser_core/apis/serializers.py index 70cd73e37..0a0b34ebf 100644 --- a/src/api/bkuser_core/config/overlays/stag.py +++ b/src/api/bkuser_core/apis/serializers.py @@ -8,22 +8,23 @@ 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 bkuser_core.config.common.django_basic import * # noqa -from bkuser_core.config.common.logging import * # noqa -from bkuser_core.config.common.platform import * # noqa -from bkuser_core.config.common.storage import * # noqa -from bkuser_core.config.common.system import * # noqa +from rest_framework import fields -from bkuser_global.config import get_logging_config_dict -LOG_LEVEL = "DEBUG" +class StringArrayField(fields.ListField): + """ + String representation of an array field. + """ -LOGGING = get_logging_config_dict( - log_level=LOG_LEVEL, - logging_dir=LOGGING_DIR, - log_class=LOG_CLASS, - file_name=APP_ID, - package_name="bkuser_core", -) + def __init__(self, **kwargs): + super().__init__(**kwargs) -SAAS_URL = urllib.parse.urljoin(BK_PAAS_URL, f"/t/{SAAS_CODE}/") + self.delimiter = kwargs.get("delimiter", ",") + + def to_internal_value(self, data): + # convert string to list + target = [] + for e in data: + target.extend(e.split(self.delimiter)) + + return super().to_internal_value(target) diff --git a/src/api/bkuser_core/tests/apis/audits/__init__.py b/src/api/bkuser_core/apis/v2/__init__.py similarity index 100% rename from src/api/bkuser_core/tests/apis/audits/__init__.py rename to src/api/bkuser_core/apis/v2/__init__.py diff --git a/src/api/bkuser_core/common/constants.py b/src/api/bkuser_core/apis/v2/constants.py similarity index 100% rename from src/api/bkuser_core/common/constants.py rename to src/api/bkuser_core/apis/v2/constants.py diff --git a/src/api/bkuser_core/common/serializers.py b/src/api/bkuser_core/apis/v2/serializers.py similarity index 83% rename from src/api/bkuser_core/common/serializers.py rename to src/api/bkuser_core/apis/v2/serializers.py index 04bfb78db..23d4646e3 100644 --- a/src/api/bkuser_core/common/serializers.py +++ b/src/api/bkuser_core/apis/v2/serializers.py @@ -10,8 +10,7 @@ """ import datetime -from django.conf import settings -from django.utils import timezone +from bkuser_core.apis.serializers import StringArrayField from django.utils.translation import ugettext as _ from rest_framework import fields, serializers @@ -70,43 +69,6 @@ def is_custom_fields_enabled(slz: serializers.Serializer) -> bool: return False -def patch_datetime_field(): - """Patch DateTimeField which respect current timezone - See also: https://github.com/encode/django-rest-framework/issues/3732 - """ - - def to_representation(self, value): - # This is MAGIC! - if value and settings.USE_TZ: - try: - value = timezone.localtime(value) - except ValueError: - pass - return orig_to_representation(self, value) - - orig_to_representation = fields.DateTimeField.to_representation - fields.DateTimeField.to_representation = to_representation - - -class StringArrayField(fields.ListField): - """ - String representation of an array field. - """ - - def __init__(self, **kwargs): - super().__init__(**kwargs) - - self.delimiter = kwargs.get("delimiter", ",") - - def to_internal_value(self, data): - # convert string to list - target = [] - for e in data: - target.extend(e.split(self.delimiter)) - - return super().to_internal_value(target) - - class AdvancedListSerializer(serializers.Serializer): fields = StringArrayField(required=False, help_text=_("指定对象返回字段,支持多选,以逗号分隔,例如: username,status,id")) lookup_field = serializers.CharField(required=False, help_text=_("查询字段,针对 exact_lookups,fuzzy_lookups 生效")) diff --git a/src/api/bkuser_core/common/viewset.py b/src/api/bkuser_core/apis/v2/viewset.py similarity index 99% rename from src/api/bkuser_core/common/viewset.py rename to src/api/bkuser_core/apis/v2/viewset.py index c4e353950..dcc63f2e7 100644 --- a/src/api/bkuser_core/common/viewset.py +++ b/src/api/bkuser_core/apis/v2/viewset.py @@ -21,12 +21,6 @@ from bkuser_core.bkiam.permissions import IAMPermission, IAMPermissionExtraInfo from bkuser_core.common.cache import clear_cache_if_succeed from bkuser_core.common.error_codes import error_codes -from bkuser_core.common.serializers import ( - AdvancedListSerializer, - AdvancedRetrieveSerialzier, - EmptySerializer, - is_custom_fields_enabled, -) from django.conf import settings from django.core.exceptions import FieldError, ObjectDoesNotExist from django.db.models import ManyToOneRel, Q, QuerySet @@ -41,6 +35,7 @@ from bkuser_global.utils import force_str_2_bool from .constants import LOOKUP_FIELD_NAME, LOOKUP_PARAM +from .serializers import AdvancedListSerializer, AdvancedRetrieveSerialzier, EmptySerializer, is_custom_fields_enabled logger = logging.getLogger(__name__) diff --git a/src/api/bkuser_core/tests/apis/categories/__init__.py b/src/api/bkuser_core/apis/v3/__init__.py similarity index 100% rename from src/api/bkuser_core/tests/apis/categories/__init__.py rename to src/api/bkuser_core/apis/v3/__init__.py diff --git a/src/api/bkuser_core/apis/v3/serializers.py b/src/api/bkuser_core/apis/v3/serializers.py new file mode 100644 index 000000000..7410bedc8 --- /dev/null +++ b/src/api/bkuser_core/apis/v3/serializers.py @@ -0,0 +1,67 @@ +# -*- 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 base64 import b64encode +from collections import OrderedDict +from typing import TYPE_CHECKING +from urllib import parse + +from rest_framework import fields +from rest_framework.pagination import CursorPagination + +if TYPE_CHECKING: + from django.db.models import QuerySet + + +class AdvancedPagination(CursorPagination): + page_size = 10 + page_size_query_param = "page_size" + max_page_size = 100 + + def encode_cursor(self, cursor): + """ + Given a Cursor instance, return an url with encoded cursor. + """ + tokens = {} + if cursor.offset != 0: + tokens["o"] = str(cursor.offset) + if cursor.reverse: + tokens["r"] = "1" + if cursor.position is not None: + tokens["p"] = cursor.position + + querystring = parse.urlencode(tokens, doseq=True) + return b64encode(querystring.encode("ascii")).decode("ascii") + + def get_paginated_response(self, data: "QuerySet"): + return OrderedDict( + [ + ("count", len(data)), + ("next", self.get_next_link()), + ("previous", self.get_previous_link()), + ("results", data), + ] + ) + + +class StringArrayField(fields.CharField): + """ + String representation of an array field. + """ + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + self.delimiter = kwargs.get("delimiter", ",") + + def to_internal_value(self, data): + # convert string to list + data = super().to_internal_value(data) + return data.split(self.delimiter) diff --git a/src/api/bkuser_core/tests/apis/departments/__init__.py b/src/api/bkuser_core/apis/v3/viewset.py similarity index 100% rename from src/api/bkuser_core/tests/apis/departments/__init__.py rename to src/api/bkuser_core/apis/v3/viewset.py diff --git a/src/api/bkuser_core/audit/constants.py b/src/api/bkuser_core/audit/constants.py index 4073b0804..e50e9efe5 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() + SHOULD_CHANGE_INITIAL_PASSWORD = auto() _choices_labels = ( (BAD_PASSWORD, "密码错误"), @@ -28,6 +29,7 @@ class LogInFailReason(AutoLowerEnum): (TOO_MANY_FAILURE, "密码错误次数过多"), (LOCKED_USER, "用户已锁定"), (DISABLED_USER, "用户已删除"), + (SHOULD_CHANGE_INITIAL_PASSWORD, "需要修改初始密码"), ) diff --git a/src/api/bkuser_core/audit/handlers.py b/src/api/bkuser_core/audit/handlers.py index c15093696..5163fb02a 100644 --- a/src/api/bkuser_core/audit/handlers.py +++ b/src/api/bkuser_core/audit/handlers.py @@ -16,7 +16,7 @@ from bkuser_core.categories.signals import post_category_create from bkuser_core.departments.signals import post_department_create from bkuser_core.profiles.signals import post_field_create, post_profile_create, post_profile_update -from bkuser_core.user_settings.signals import post_setting_create +from bkuser_core.user_settings.signals import post_setting_create, post_setting_update from django.dispatch import receiver if TYPE_CHECKING: @@ -51,3 +51,14 @@ def create_audit_log(sender, instance: "Profile", operator: str, extra_values: d operator_obj=instance, request=extra_values["request"], ) + + +@receiver([post_setting_update]) +def update_audit_log(sender, instance: "Profile", operator: str, extra_values: dict, **kwargs): + """Create an audit log for instance""" + create_general_log( + operator=operator, + operate_type=OperationType.UPDATE.value, + operator_obj=instance, + request=extra_values["request"], + ) diff --git a/src/api/bkuser_core/audit/managers.py b/src/api/bkuser_core/audit/managers.py index 80ef95e03..d12ee78fd 100644 --- a/src/api/bkuser_core/audit/managers.py +++ b/src/api/bkuser_core/audit/managers.py @@ -9,8 +9,12 @@ specific language governing permissions and limitations under the License. """ +import datetime + +from django.conf import settings from django.core.exceptions import ObjectDoesNotExist from django.db import models +from django.utils.timezone import now from .constants import LogInFailReason @@ -22,12 +26,21 @@ class ResetPasswordManager(models.Manager): class LogInManager(models.Manager): def latest_failed_count(self) -> int: """获取上一次成功登陆前最近登陆失败次数""" + # 如果服务运行的时间足够长,单个用户登录记录条目数将会非常多,统计可能会产生慢查询 + # 所以服务维护者可以根据用户登录频次来调整最远统计时间(默认为一个月) + farthest_count_time = now() - datetime.timedelta(seconds=settings.LOGIN_RECORD_COUNT_SECONDS) try: - create_time = self.filter(is_success=True).latest().create_time + latest_success_time = ( + self.filter(is_success=True, create_time__gte=farthest_count_time).latest().create_time + ) + except ObjectDoesNotExist: + # 当没有任何成功记录时,直接统计时间区域内的错误次数 + return self.filter( + is_success=False, reason=LogInFailReason.BAD_PASSWORD.value, create_time__gte=farthest_count_time + ).count() + else: return self.filter( is_success=False, - reason=LogInFailReason.BAD_PASSWORD.value, # type: ignore - create_time__gt=create_time, + reason=LogInFailReason.BAD_PASSWORD.value, + create_time__gte=latest_success_time, ).count() - except ObjectDoesNotExist: - return self.filter(is_success=False, reason=LogInFailReason.BAD_PASSWORD.value).count() # type: ignore diff --git a/src/api/bkuser_core/audit/serializers.py b/src/api/bkuser_core/audit/serializers.py index eaf0c24e6..6319e98ba 100644 --- a/src/api/bkuser_core/audit/serializers.py +++ b/src/api/bkuser_core/audit/serializers.py @@ -8,7 +8,7 @@ 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 bkuser_core.common.serializers import CustomFieldsMixin +from bkuser_core.apis.v2.serializers import CustomFieldsMixin from django.utils.translation import gettext_lazy as _ from rest_framework import serializers diff --git a/src/api/bkuser_core/audit/urls.py b/src/api/bkuser_core/audit/urls.py index 05f70b9d5..40f209d3c 100644 --- a/src/api/bkuser_core/audit/urls.py +++ b/src/api/bkuser_core/audit/urls.py @@ -8,7 +8,7 @@ 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 bkuser_core.common.constants import LOOKUP_FIELD_NAME +from bkuser_core.apis.v2.constants import LOOKUP_FIELD_NAME from django.conf.urls import url from . import views diff --git a/src/api/bkuser_core/audit/views.py b/src/api/bkuser_core/audit/views.py index b9ef44f16..788914e63 100644 --- a/src/api/bkuser_core/audit/views.py +++ b/src/api/bkuser_core/audit/views.py @@ -8,7 +8,7 @@ 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 bkuser_core.common.viewset import AdvancedListAPIView, AdvancedModelViewSet +from bkuser_core.apis.v2.viewset import AdvancedListAPIView, AdvancedModelViewSet from . import serializers as local_serializers from .models import GeneralLog, LogIn, ResetPassword diff --git a/src/api/bkuser_core/bkiam/views.py b/src/api/bkuser_core/bkiam/views.py index a7197d0bf..054042db2 100644 --- a/src/api/bkuser_core/bkiam/views.py +++ b/src/api/bkuser_core/bkiam/views.py @@ -13,7 +13,7 @@ from bkuser_core.departments.models import Department from bkuser_core.departments.serializers import DepartmentSerializer from bkuser_core.profiles.models import DynamicFieldInfo -from bkuser_core.profiles.serializers import DynamicFieldsSerializer +from bkuser_core.profiles.v2.serializers import DynamicFieldsSerializer from .base import BaseIAMViewSet from .constants import ResourceType diff --git a/src/api/bkuser_core/categories/apps.py b/src/api/bkuser_core/categories/apps.py index 48f529472..0d24e920c 100644 --- a/src/api/bkuser_core/categories/apps.py +++ b/src/api/bkuser_core/categories/apps.py @@ -44,5 +44,5 @@ def import_plugins(base_file_path: str, module_prefix: str): module_path = module_prefix + d try: importlib.import_module(module_path) - except Exception as e: # pylint: disable=broad-except - logger.warning("⚠️ failed to import plugin: %s, for %s", module_path, e) + except Exception: # pylint: disable=broad-except + logger.exception("⚠️ failed to import plugin: path[%s]", module_path) diff --git a/src/api/bkuser_core/categories/constants.py b/src/api/bkuser_core/categories/constants.py index 3778939cb..e4dd07657 100644 --- a/src/api/bkuser_core/categories/constants.py +++ b/src/api/bkuser_core/categories/constants.py @@ -36,7 +36,6 @@ class CategoryType(AutoLowerEnum): LOCAL = auto() MAD = auto() LDAP = auto() - TOF = auto() CUSTOM = auto() # 特殊的类型,仅在未解耦前桥接 PLUGGABLE = auto() @@ -45,7 +44,6 @@ class CategoryType(AutoLowerEnum): (LOCAL, _("本地目录")), (MAD, _("Microsoft Active Directory")), (LDAP, _("OpenLDAP")), - (TOF, _("TOF")), (CUSTOM, "自定义目录"), (PLUGGABLE, "可插拔目录"), ) @@ -56,12 +54,10 @@ def get_description(cls, value: "CategoryType"): cls.LOCAL: _("本地支持用户的新增、删除、编辑、查询,以及用户的登录认证。"), cls.MAD: _("支持对接 Microsoft Active Directory,将用户信息同步到本地或者直接通过接口完成用户登录验证。"), cls.LDAP: _("支持对接 OpenLDAP,将用户信息同步到本地或者直接通过接口完成用户登录验证。"), - cls.TOF: _("支持 TOF 信息同步"), cls.CUSTOM: _("支持对接任意符合自定义数据拉取协议的用户系统。"), cls.LOCAL.value: _("本地支持用户的新增、删除、编辑、查询,以及用户的登录认证。"), cls.MAD.value: _("支持对接 Microsoft Active Directory,将用户信息同步到本地或者直接通过接口完成用户登录验证。"), cls.LDAP.value: _("支持对接 OpenLDAP,将用户信息同步到本地或者直接通过接口完成用户登录验证。"), - cls.TOF.value: _("支持 TOF 信息同步"), cls.CUSTOM.value: _("支持对接任意符合自定义数据拉取协议的用户系统。"), } return _map[value] @@ -92,5 +88,6 @@ class SyncTaskStatus(AutoLowerEnum): SUCCESSFUL = auto() FAILED = auto() RUNNING = auto() + RETRYING = auto() - _choices_labels = ((SUCCESSFUL, _("成功")), (FAILED, _("失败")), (RUNNING, _("同步中"))) + _choices_labels = ((SUCCESSFUL, _("成功")), (FAILED, _("失败")), (RUNNING, _("同步中")), (RETRYING, _("失败重试中"))) diff --git a/src/api/bkuser_core/categories/handlers.py b/src/api/bkuser_core/categories/handlers.py index cfc744aa6..1b43c091a 100644 --- a/src/api/bkuser_core/categories/handlers.py +++ b/src/api/bkuser_core/categories/handlers.py @@ -16,9 +16,6 @@ from django.conf import settings from django.dispatch import receiver -from .plugins.ldap.handlers import create_sync_tasks, delete_sync_tasks, update_sync_tasks # noqa -from .plugins.local.handlers import make_local_default_settings # noqa - logger = logging.getLogger(__name__) diff --git a/src/api/bkuser_core/categories/loader.py b/src/api/bkuser_core/categories/loader.py index 6733be399..70a043a1a 100644 --- a/src/api/bkuser_core/categories/loader.py +++ b/src/api/bkuser_core/categories/loader.py @@ -56,8 +56,8 @@ def get_plugin_by_name(name: str) -> "DataSourcePlugin": def register_plugin(plugin: "DataSourcePlugin"): try: - get_plugin_by_name(plugin.name) - raise PluginAlreadyExisted(f"Plugin with name: {plugin.name} already existed") + if get_plugin_by_name(plugin.name): + logger.warning(f"Plugin with name: {plugin.name} already existed") except PluginDoesNotExist: _global_plugins[plugin.name] = plugin - logger.info("➕Plugin[%s] added.", plugin.name) + logger.info("➕Plugin[%s] loaded.", plugin.name) diff --git a/src/api/bkuser_core/categories/migrations/0009_auto_20210413_1702.py b/src/api/bkuser_core/categories/migrations/0009_auto_20210413_1702.py index 2d8467a5c..3ee8211d9 100644 --- a/src/api/bkuser_core/categories/migrations/0009_auto_20210413_1702.py +++ b/src/api/bkuser_core/categories/migrations/0009_auto_20210413_1702.py @@ -43,7 +43,6 @@ class Migration(migrations.Migration): ("local", "本地目录"), ("mad", "Microsoft Active Directory"), ("ldap", "OpenLDAP"), - ("tof", "TOF"), ("custom", "自定义目录"), ], max_length=32, diff --git a/src/api/bkuser_core/categories/migrations/0010_auto_20210803_1026.py b/src/api/bkuser_core/categories/migrations/0010_auto_20210803_1026.py index f70a1df77..eecc20f1b 100644 --- a/src/api/bkuser_core/categories/migrations/0010_auto_20210803_1026.py +++ b/src/api/bkuser_core/categories/migrations/0010_auto_20210803_1026.py @@ -17,49 +17,86 @@ class Migration(migrations.Migration): dependencies = [ - ('categories', '0009_auto_20210413_1702'), + ("categories", "0009_auto_20210413_1702"), ] operations = [ migrations.CreateModel( - name='SyncProgress', + name="SyncProgress", 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)), - ('task_id', models.UUIDField(db_index=True, verbose_name='任务id')), - ('step', models.CharField(choices=[('users', '用户数据更新'), ('departments', '组织数据更新'), ('users_relationship', '用户间关系数据更新'), ('dept_user_relationship', '用户和组织关系数据更新')], max_length=32, verbose_name='同步步骤')), - ('status', models.CharField(choices=[('successful', '成功'), ('failed', '失败'), ('running', '同步中')], default='running', max_length=16, verbose_name='状态')), - ('successful_count', models.IntegerField(verbose_name='同步成功数量', default=0)), - ('failed_count', models.IntegerField(verbose_name='同步失败数量', default=0)), + ("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)), + ("task_id", models.UUIDField(db_index=True, verbose_name="任务id")), + ( + "step", + models.CharField( + choices=[ + ("users", "用户数据更新"), + ("departments", "组织数据更新"), + ("users_relationship", "用户间关系数据更新"), + ("dept_user_relationship", "用户和组织关系数据更新"), + ], + max_length=32, + verbose_name="同步步骤", + ), + ), + ( + "status", + models.CharField( + choices=[("successful", "成功"), ("failed", "失败"), ("running", "同步中")], + default="running", + max_length=16, + verbose_name="状态", + ), + ), + ("successful_count", models.IntegerField(verbose_name="同步成功数量", default=0)), + ("failed_count", models.IntegerField(verbose_name="同步失败数量", default=0)), ], ), migrations.AlterField( - model_name='profilecategory', - name='type', - field=models.CharField(choices=[('local', '本地目录'), ('mad', 'Microsoft Active Directory'), ('ldap', 'OpenLDAP'), ('tof', 'TOF'), ('custom', '自定义目录'), ('pluggable', '可插拔目录')], max_length=32, verbose_name='类型'), + model_name="profilecategory", + name="type", + field=models.CharField( + choices=[ + ("local", "本地目录"), + ("mad", "Microsoft Active Directory"), + ("ldap", "OpenLDAP"), + ("custom", "自定义目录"), + ("pluggable", "可插拔目录"), + ], + max_length=32, + verbose_name="类型", + ), ), migrations.CreateModel( - name='SyncProgressLog', + name="SyncProgressLog", 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)), - ('logs', models.TextField(verbose_name='日志')), - ('failed_records', models.JSONField(default=list)), - ('progress', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='log', to='categories.syncprogress')), + ("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)), + ("logs", models.TextField(verbose_name="日志")), + ("failed_records", models.JSONField(default=list)), + ( + "progress", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, related_name="log", to="categories.syncprogress" + ), + ), ], options={ - 'abstract': False, + "abstract": False, }, ), migrations.AddField( - model_name='syncprogress', - name='category', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='categories.profilecategory', verbose_name='用户目录'), + model_name="syncprogress", + name="category", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="categories.profilecategory", verbose_name="用户目录" + ), ), migrations.AlterUniqueTogether( - name='syncprogress', - unique_together={('category', 'step', 'task_id')}, + name="syncprogress", + unique_together={("category", "step", "task_id")}, ), ] diff --git a/src/api/bkuser_core/categories/migrations/0012_auto_20211209_1604.py b/src/api/bkuser_core/categories/migrations/0012_auto_20211209_1604.py new file mode 100644 index 000000000..c4add8523 --- /dev/null +++ b/src/api/bkuser_core/categories/migrations/0012_auto_20211209_1604.py @@ -0,0 +1,38 @@ +# Generated by Django 3.2.5 on 2021-12-09 08:04 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("categories", "0011_synctask"), + ] + + operations = [ + migrations.AddField( + model_name="synctask", + name="retried_count", + field=models.IntegerField(default=0, verbose_name="重试次数"), + ), + migrations.AlterField( + model_name="syncprogress", + name="status", + field=models.CharField( + choices=[("successful", "成功"), ("failed", "失败"), ("running", "同步中"), ("retrying", "失败重试中")], + default="running", + max_length=16, + verbose_name="状态", + ), + ), + migrations.AlterField( + model_name="synctask", + name="status", + field=models.CharField( + choices=[("successful", "成功"), ("failed", "失败"), ("running", "同步中"), ("retrying", "失败重试中")], + default="running", + max_length=16, + verbose_name="状态", + ), + ), + ] diff --git a/src/api/bkuser_core/categories/models.py b/src/api/bkuser_core/categories/models.py index 8c77be765..87995d457 100644 --- a/src/api/bkuser_core/categories/models.py +++ b/src/api/bkuser_core/categories/models.py @@ -13,14 +13,7 @@ from uuid import UUID, uuid4 from bkuser_core.audit.models import AuditObjMetaInfo -from bkuser_core.categories.constants import ( - TIMEOUT_THRESHOLD, - CategoryStatus, - CategoryType, - SyncStep, - SyncTaskStatus, - SyncTaskType, -) +from bkuser_core.categories.constants import TIMEOUT_THRESHOLD, CategoryStatus, SyncStep, SyncTaskStatus, SyncTaskType from bkuser_core.categories.db_managers import ProfileCategoryManager from bkuser_core.categories.exceptions import ExistsSyncingTaskError from bkuser_core.common.models import TimestampedModel @@ -38,7 +31,7 @@ class ProfileCategory(TimestampedModel): """用户目录""" - type = models.CharField(verbose_name="类型", max_length=32, choices=CategoryType.get_choices()) + type = models.CharField(verbose_name="类型", max_length=32) description = models.TextField("描述文字", null=True, blank=True) display_name = models.CharField(verbose_name="展示名称", max_length=64) domain = models.CharField(verbose_name="登陆域", max_length=64, db_index=True, unique=True) @@ -118,7 +111,7 @@ def get_required_metas(self): def get_unfilled_settings(self): """获取未就绪的配置""" required_metas = self.get_required_metas() - configured_meta_ids = self.settings.filter(enabled=True).values_list("meta", flat=True) + configured_meta_ids = self.settings.all().values_list("meta", flat=True) return required_metas.exclude(id__in=configured_meta_ids) def mark_synced(self): @@ -144,7 +137,7 @@ def to_audit_info(self): class SyncTaskManager(models.Manager): def register_task( self, category: ProfileCategory, operator: str, type_: SyncTaskType = SyncTaskType.MANUAL - ) -> 'SyncTask': + ) -> "SyncTask": qs = self.filter(category=category, status=SyncTaskStatus.RUNNING.value).order_by("-create_time") running = qs.first() if not running: @@ -164,7 +157,7 @@ def register_task( class SyncTask(TimestampedModel): - id = models.UUIDField('UUID', default=uuid4, primary_key=True, editable=False, auto_created=True, unique=True) + id = models.UUIDField("UUID", default=uuid4, primary_key=True, editable=False, auto_created=True, unique=True) category = models.ForeignKey(ProfileCategory, verbose_name="用户目录", on_delete=models.CASCADE, db_index=True) status = models.CharField( verbose_name="状态", max_length=16, choices=SyncTaskStatus.get_choices(), default=SyncTaskStatus.RUNNING.value @@ -173,6 +166,7 @@ class SyncTask(TimestampedModel): verbose_name="触发类型", max_length=16, choices=SyncTaskType.get_choices(), default=SyncTaskType.MANUAL.value ) operator = models.CharField(max_length=255, verbose_name="操作人", default="nobody") + retried_count = models.IntegerField(verbose_name="重试次数", default=0) objects = SyncTaskManager() @@ -180,14 +174,6 @@ class SyncTask(TimestampedModel): def required_time(self) -> datetime.timedelta: return self.update_time - self.create_time - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - if exc_val is not None: - self.status = SyncTaskStatus.FAILED.value - self.save(update_fields=["status", "update_time"]) - @property def progresses(self): # 由于建表顺序的原因, SyncProgress 的 task_id 未设置成外键.... @@ -196,8 +182,8 @@ def progresses(self): class SyncProgressManager(models.Manager): - def init_progresses(self, category: ProfileCategory, task_id: UUID) -> Dict[SyncStep, 'SyncProgress']: - progresses: Dict[SyncStep, 'SyncProgress'] = {} + def init_progresses(self, category: ProfileCategory, task_id: UUID) -> Dict[SyncStep, "SyncProgress"]: + progresses: Dict[SyncStep, "SyncProgress"] = {} for step in [ SyncStep.DEPARTMENTS, SyncStep.USERS, @@ -211,7 +197,7 @@ def init_progresses(self, category: ProfileCategory, task_id: UUID) -> Dict[Sync class SyncProgress(TimestampedModel): - task_id = models.UUIDField(db_index=True, verbose_name='任务id') + task_id = models.UUIDField(db_index=True, verbose_name="任务id") category = models.ForeignKey(ProfileCategory, verbose_name="用户目录", on_delete=models.CASCADE) step = models.CharField(verbose_name="同步步骤", max_length=32, choices=SyncStep.get_choices()) status = models.CharField( diff --git a/src/api/bkuser_core/categories/plugins/README.md b/src/api/bkuser_core/categories/plugins/README.md index f2e710ed3..e3cc8c43d 100644 --- a/src/api/bkuser_core/categories/plugins/README.md +++ b/src/api/bkuser_core/categories/plugins/README.md @@ -54,6 +54,43 @@ python manage.py create_pluggable_category --name 插件化目录 --domain some- ``` 默认地,我们会创建一个 key 为 `plugin_name` 的 `Setting` 绑定到该目录。 +## 配置 + +我们为插件增加了申明配置的能力,开发者可以通过 yaml 文件定义面向使用者的配置列表 + +```python +DataSourcePlugin( + name="custom", + syncer_cls=CustomSyncer, + login_handler_cls=LoginHandler, + allow_client_write=True, + category_type="custom", + hooks={HookType.POST_SYNC: AlertIfFailedHook}, + # 在这里显式地告之插件配置的文件路径 + settings_path=os.path.dirname(__file__) / Path("settings.yaml"), +).register() +``` +```yaml +# 默认第一层 +settings: + # namespace + general: + # region + default: + # 具体配置的 key 值 + paths: + # SettingMeta 具体内容 + default: + # 可以以 yaml 原生写法定义 JSON 内容 + profile: "profiles" + department: "departments" + api_host: + default: "" + example: "https://example.com" +``` +当插件加载时,我们会做两件事: +- 创建或更新 SettingMeta +- 当 SettingMeta 的默认值存在时(所有不为 `None` 的内容,空字符串、空字典均被视作 **存在**), 使用默认值为所有已经存在对应目录初始化 Setting ## 同步类 Syncer 实现 diff --git a/src/api/bkuser_core/categories/plugins/constants.py b/src/api/bkuser_core/categories/plugins/constants.py index e65514257..7b3647854 100644 --- a/src/api/bkuser_core/categories/plugins/constants.py +++ b/src/api/bkuser_core/categories/plugins/constants.py @@ -8,10 +8,15 @@ 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 enum import auto + from bkuser_core.categories.constants import SyncStep +from bkuser_core.common.enum import AutoNameEnum from django.utils.translation import gettext_lazy as _ PLUGIN_NAME_SETTING_KEY = "plugin_name" +DYNAMIC_FIELDS_SETTING_KEY = "dynamic_fields_mapping" + SYNC_LOG_TEMPLATE_MAP = { (SyncStep.USERS, True): _("同步用户【{username}】成功"), (SyncStep.USERS, False): _("同步用户【{username}】失败, 失败原因: {error}"), @@ -22,3 +27,8 @@ (SyncStep.USERS_RELATIONSHIP, True): _("同步用户【{username}】上级成功"), (SyncStep.USERS_RELATIONSHIP, False): _("同步用户【{username}】上级失败, 失败原因: {error}"), } + + +class HookType(AutoNameEnum): + POST_SYNC = auto() + PRE_SYNC = auto() diff --git a/src/api/bkuser_core/categories/plugins/custom/__init__.py b/src/api/bkuser_core/categories/plugins/custom/__init__.py index 5e51cdce6..08e86af96 100644 --- a/src/api/bkuser_core/categories/plugins/custom/__init__.py +++ b/src/api/bkuser_core/categories/plugins/custom/__init__.py @@ -8,8 +8,12 @@ an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. """ -from bkuser_core.categories.plugins.plugin import DataSourcePlugin +import os +from pathlib import Path +from bkuser_core.categories.plugins.plugin import DataSourcePlugin, HookType + +from .hooks import AlertIfFailedHook from .login import LoginHandler from .sycner import CustomSyncer @@ -19,4 +23,6 @@ login_handler_cls=LoginHandler, allow_client_write=True, category_type="custom", + hooks={HookType.POST_SYNC: AlertIfFailedHook}, + settings_path=os.path.dirname(__file__) / Path("settings.yaml"), ).register() diff --git a/src/api/bkuser_core/categories/plugins/custom/hooks.py b/src/api/bkuser_core/categories/plugins/custom/hooks.py new file mode 100644 index 000000000..9ca53e72e --- /dev/null +++ b/src/api/bkuser_core/categories/plugins/custom/hooks.py @@ -0,0 +1,27 @@ +# -*- 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 logging + +from celery.states import FAILURE + +logger = logging.getLogger(__name__) + + +class AlertIfFailedHook: + """当所有重试都失败时将告警通知""" + + def trigger(self, status: str, params: dict): + if status == FAILURE: + logger.error( + "failed to sync data for category<%s> after %s retries", params["category"], params["retries"] + ) + # 目前该 hook 更多是一个示例,并未实际实现告警通知功能 + # TODO: 使用 ESB 通知到平台管理员 diff --git a/src/api/bkuser_core/categories/plugins/custom/settings.yaml b/src/api/bkuser_core/categories/plugins/custom/settings.yaml new file mode 100644 index 000000000..c26654ed3 --- /dev/null +++ b/src/api/bkuser_core/categories/plugins/custom/settings.yaml @@ -0,0 +1,7 @@ +paths: + default: + profile: "profiles" + department: "departments" +api_host: + default: "" + example: "http://example.com" \ No newline at end of file diff --git a/src/api/bkuser_core/categories/plugins/ldap/__init__.py b/src/api/bkuser_core/categories/plugins/ldap/__init__.py index eec33ab81..ed50863e0 100644 --- a/src/api/bkuser_core/categories/plugins/ldap/__init__.py +++ b/src/api/bkuser_core/categories/plugins/ldap/__init__.py @@ -8,6 +8,9 @@ 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 os +from pathlib import Path + from bkuser_core.categories.plugins.plugin import DataSourcePlugin from .login import LoginHandler @@ -19,5 +22,5 @@ login_handler_cls=LoginHandler, allow_client_write=False, category_type="ldap", - extra_config={"default_sync_period": 60, "min_sync_period": 60, "ldap_max_paged_size": 1000}, + settings_path=os.path.dirname(__file__) / Path("settings.yaml"), ).register() diff --git a/src/api/bkuser_core/categories/plugins/ldap/adaptor.py b/src/api/bkuser_core/categories/plugins/ldap/adaptor.py index 1f85712e1..b79fbdd38 100644 --- a/src/api/bkuser_core/categories/plugins/ldap/adaptor.py +++ b/src/api/bkuser_core/categories/plugins/ldap/adaptor.py @@ -8,70 +8,117 @@ an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. """ -from dataclasses import dataclass +import logging +from dataclasses import dataclass, field from typing import Any, Dict, List, NamedTuple, Optional +from bkuser_core.categories.plugins.constants import DYNAMIC_FIELDS_SETTING_KEY from bkuser_core.categories.plugins.ldap.models import LdapDepartment, LdapUserProfile from bkuser_core.user_settings.loader import ConfigProvider from django.utils.encoding import force_str from ldap3.utils import dn as dn_utils +logger = logging.getLogger(__name__) + @dataclass class ProfileFieldMapper: """从 ldap 对象属性中获取用户字段""" config_loader: ConfigProvider - setting_field_map: dict + embed_fields = [ + "username", + "display_name", + "email", + "telephone", + ] + dynamic_fields: List = field(default_factory=list) + + def __post_init__(self): + self.dynamic_fields_mapping = self.config_loader.get(DYNAMIC_FIELDS_SETTING_KEY) + + self.dynamic_fields = list(self.dynamic_fields_mapping.keys()) if self.dynamic_fields_mapping else [] + + def get_value( + self, field_name: str, user_meta: Dict[str, List[bytes]], remain_raw: bool = False, dynamic_field: bool = False + ) -> Any: + """通过 field_name 从 ldap 数据中获取具体值""" + + # 获取自定义字段对应的属性值 + 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) + 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) + 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) + return "" + + # 2. 类似 memberOf 字段,将会返回原始列表 + if remain_raw: + return user_meta[ldap_field_name] - def get_field(self, user_meta: Dict[str, List[bytes]], field_name: str, raise_exception: bool = False) -> str: + return force_str(user_meta[ldap_field_name][0]) + + def get_values(self, user_meta: Dict[str, List[bytes]]) -> Dict[str, Any]: """根据字段映射关系, 从 ldap 中获取 `field_name` 的值""" - try: - setting_name = self.setting_field_map[field_name] - except KeyError: - if raise_exception: - raise ValueError("该用户字段没有在配置中有对应项,无法同步") - return "" - try: - ldap_field_name = self.config_loader[setting_name] - except KeyError: - if raise_exception: - raise ValueError(f"用户目录配置中缺失字段 {setting_name}") - return "" + values = {} + for field_name in self.embed_fields: + values.update({field_name: self.get_value(field_name, user_meta)}) - try: - if user_meta[ldap_field_name]: - return force_str(user_meta[ldap_field_name][0]) + return values - return "" - except KeyError: - if raise_exception: - raise ValueError(f"搜索数据中没有对应的字段 {ldap_field_name}") - return "" + def get_dynamic_values(self, user_meta: Dict[str, List[bytes]]) -> Dict[str, Any]: + """获取自定义字段 在ldap中的对应值""" + values = {} + + if self.dynamic_fields: + values.update( + { + field_name: self.get_value( + field_name=self.dynamic_fields_mapping[field_name], user_meta=user_meta, dynamic_field=True + ) + for field_name in self.dynamic_fields + } + ) + + return values def get_user_attributes(self) -> list: """获取远端属性名列表""" - return [self.config_loader[x] for x in self.setting_field_map.values() if self.config_loader[x]] + user_attributes = [self.config_loader[x] for x in self.embed_fields if self.config_loader.get(x)] + user_attributes.extend( + [self.dynamic_fields_mapping[x] for x in self.dynamic_fields if self.dynamic_fields_mapping.get(x)] + ) + + return user_attributes def user_adapter( code: str, user_meta: Dict[str, Any], field_mapper: ProfileFieldMapper, restrict_types: List[str] ) -> LdapUserProfile: - groups = user_meta["attributes"][field_mapper.config_loader["user_member_of"]] + groups = field_mapper.get_value("user_member_of", user_meta["raw_attributes"], True) or [] return LdapUserProfile( - username=field_mapper.get_field(user_meta=user_meta["raw_attributes"], field_name="username"), - email=field_mapper.get_field(user_meta=user_meta["raw_attributes"], field_name="email"), - telephone=field_mapper.get_field(user_meta=user_meta["raw_attributes"], field_name="telephone"), - display_name=field_mapper.get_field(user_meta=user_meta["raw_attributes"], field_name="display_name"), + **field_mapper.get_values(user_meta["raw_attributes"]), code=code, + extras=field_mapper.get_dynamic_values(user_meta["raw_attributes"]), # TODO: 完成转换 departments 的逻辑 departments=[ # 根据约定, dn 中除去第一个成分以外的部分即为用户所在的部门, 因此需要取 [1:] list(reversed(parse_dn_value_list(user_meta["dn"], restrict_types)[1:])), # 用户与用户组之间的关系 - *[list(reversed(parse_dn_value_list(group, restrict_types))) for group in groups], + *[list(reversed(parse_dn_value_list(force_str(group), restrict_types))) for group in groups], ], ) diff --git a/src/api/bkuser_core/categories/plugins/ldap/client.py b/src/api/bkuser_core/categories/plugins/ldap/client.py index 77c383186..85d339280 100644 --- a/src/api/bkuser_core/categories/plugins/ldap/client.py +++ b/src/api/bkuser_core/categories/plugins/ldap/client.py @@ -13,7 +13,6 @@ from typing import TYPE_CHECKING, Dict, List import ldap3 -from bkuser_core.categories.loader import get_plugin_by_name from django.conf import settings from ldap3 import ALL, SIMPLE, Connection, Server @@ -97,7 +96,7 @@ def search( search_filter=search_filter, get_operational_attributes=True, attributes=attributes or [], - paged_size=get_plugin_by_name("ldap").extra_config["ldap_max_paged_size"], + paged_size=self.config_provider.get("ldap_max_paged_size"), generator=False, ) diff --git a/src/api/bkuser_core/categories/plugins/ldap/handlers.py b/src/api/bkuser_core/categories/plugins/ldap/handlers.py index 645355ff6..518653616 100644 --- a/src/api/bkuser_core/categories/plugins/ldap/handlers.py +++ b/src/api/bkuser_core/categories/plugins/ldap/handlers.py @@ -12,34 +12,54 @@ from typing import TYPE_CHECKING from bkuser_core.categories.constants import CategoryType -from bkuser_core.categories.loader import get_plugin_by_category from bkuser_core.categories.plugins.utils import ( + delete_dynamic_filed, delete_periodic_sync_task, - make_periodic_sync_task, update_periodic_sync_task, ) -from bkuser_core.categories.signals import post_category_create, post_category_delete +from bkuser_core.categories.signals import post_category_delete, post_dynamic_field_delete +from bkuser_core.user_settings.loader import ConfigProvider from bkuser_core.user_settings.signals import post_setting_create, post_setting_update from django.dispatch import receiver if TYPE_CHECKING: from bkuser_core.categories.models import ProfileCategory + from bkuser_core.profiles.models import DynamicFieldInfo from bkuser_core.user_settings.models import Setting logger = logging.getLogger(__name__) -@receiver(post_category_create) -def create_sync_tasks(sender, instance: "ProfileCategory", operator: str, **kwargs): - if instance.type not in [CategoryType.LDAP.value, CategoryType.MAD.value]: +PULL_INTERVAL_SETTING_KEY = "pull_cycle" + + +def update_or_create_sync_tasks(instance: "Setting", operator: str): + """尝试创建或更新同步数据任务""" + if not instance.meta.key == PULL_INTERVAL_SETTING_KEY: + return + + cycle_value = int(instance.value) + config_provider = ConfigProvider(instance.category_id) + + min_sync_period = config_provider.get("min_sync_period") + if cycle_value <= 0: + # 特殊约定,当设置 <= 0 时,删除周期任务 + delete_periodic_sync_task(category_id=instance.category_id) return + # 保证不会用户配置不会低于插件的最低间隔限制 + elif cycle_value < min_sync_period: + cycle_value = min_sync_period - logger.info("going to add periodic task for Category<%s>", instance.id) - make_periodic_sync_task( - category_id=instance.id, - operator=operator, - interval_seconds=get_plugin_by_category(instance).extra_config["default_sync_period"], + # 尝试更新周期任务周期 + logger.info( + "going to update category<%s> sync interval to %s", + instance.category_id, + cycle_value, ) + try: + update_periodic_sync_task(category_id=instance.category_id, operator=operator, interval_seconds=cycle_value) + except Exception: # pylint: disable=broad-except + logger.exception("failed to update periodic task schedule") @receiver(post_category_delete) @@ -57,25 +77,12 @@ def update_sync_tasks(sender, instance: "Setting", operator: str, **kwargs): if instance.category.type not in [CategoryType.LDAP.value, CategoryType.MAD.value]: return - if not instance.meta.key == "pull_cycle": - return + # 针对 pull_cycle 配置更新同步任务 + update_or_create_sync_tasks(instance, operator) - cycle_value = int(instance.value) - category_config = get_plugin_by_category(instance.category) - if cycle_value <= 0: - delete_periodic_sync_task(category_id=instance.category_id) - return - - elif cycle_value < category_config.extra_config["min_sync_period"]: - cycle_value = category_config.extra_config["min_sync_period"] - # 尝试更新周期任务周期 - logger.info( - "going to update category<%s> sync interval to %s", - instance.category_id, - cycle_value, - ) - try: - update_periodic_sync_task(category_id=instance.category_id, operator=operator, interval_seconds=cycle_value) - except Exception: # pylint: disable=broad-except - logger.exception("failed to update periodic task schedule") +@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) diff --git a/src/api/bkuser_core/categories/plugins/ldap/helper.py b/src/api/bkuser_core/categories/plugins/ldap/helper.py index e1e621fe9..c1e5b32e5 100644 --- a/src/api/bkuser_core/categories/plugins/ldap/helper.py +++ b/src/api/bkuser_core/categories/plugins/ldap/helper.py @@ -165,6 +165,7 @@ def _load_base_info(self): "code": info.code, "telephone": info.telephone, "status": ProfileStatus.NORMAL.value, + "extras": info.extras, } # 2. 更新或创建 Profile 对象 diff --git a/src/api/bkuser_core/categories/plugins/ldap/login.py b/src/api/bkuser_core/categories/plugins/ldap/login.py index 1e48384de..3983606c2 100644 --- a/src/api/bkuser_core/categories/plugins/ldap/login.py +++ b/src/api/bkuser_core/categories/plugins/ldap/login.py @@ -11,7 +11,6 @@ from bkuser_core.categories.plugins.ldap.adaptor import ProfileFieldMapper from bkuser_core.categories.plugins.ldap.client import LDAPClient from bkuser_core.categories.plugins.ldap.exceptions import FetchUserMetaInfoFailed -from bkuser_core.categories.plugins.ldap.syncer import SETTING_FIELD_MAP from bkuser_core.user_settings.loader import ConfigProvider from django.utils.encoding import force_str @@ -19,7 +18,7 @@ class LoginHandler: @staticmethod def fetch_username(field_fetcher, user_info: dict) -> str: - return force_str(field_fetcher.get_field(user_meta=user_info["raw_attributes"], field_name="username")) + return force_str(field_fetcher.get_value(field_name="username", user_meta=user_info["raw_attributes"])) @staticmethod def fetch_dn(user_info: dict) -> str: @@ -29,7 +28,7 @@ def check(self, profile, password): category_id = profile.category_id config_loader = ConfigProvider(category_id=category_id) client = LDAPClient(config_loader) - field_fetcher = ProfileFieldMapper(config_loader, SETTING_FIELD_MAP) + field_fetcher = ProfileFieldMapper(config_loader) users = client.search( object_class=config_loader["user_class"], diff --git a/src/api/bkuser_core/categories/plugins/ldap/models.py b/src/api/bkuser_core/categories/plugins/ldap/models.py index ba7cdc34e..db9157a3e 100644 --- a/src/api/bkuser_core/categories/plugins/ldap/models.py +++ b/src/api/bkuser_core/categories/plugins/ldap/models.py @@ -9,7 +9,7 @@ specific language governing permissions and limitations under the License. """ from dataclasses import dataclass -from typing import List, Optional +from typing import Dict, List, Optional from django.utils.functional import cached_property @@ -21,8 +21,8 @@ class LdapUserProfile: email: str telephone: str code: str - departments: List[List[str]] + extras: Dict @property def key_field(self): diff --git a/src/api/bkuser_core/categories/plugins/ldap/settings.yaml b/src/api/bkuser_core/categories/plugins/ldap/settings.yaml new file mode 100644 index 000000000..b7cefd8e0 --- /dev/null +++ b/src/api/bkuser_core/categories/plugins/ldap/settings.yaml @@ -0,0 +1,122 @@ +ldap_max_paged_size: + default: 1000 +min_sync_period: + default: 60 +default_sync_period: + default: 120 + +user_member_of: + default: memberOf + example: memberOf + region: group + namespace: fields +user_group_description: + default: description + example: description + required: true + region: group + namespace: fields +user_group_name: + default: cn + example: cn + required: true + region: group + namespace: fields +user_group_filter: + default: (objectclass=groupOfUniqueNames) + example: (objectclass=groupOfUniqueNames) + required: true + region: group + namespace: fields +user_group_class: + default: groupOfUniqueNames + example: groupOfUniqueNames + required: true + region: group + namespace: fields + +telephone: + default: telephonenumber + example: telephonenumber + required: true + region: basic + namespace: fields +email: + default: email + example: email + required: true + region: basic + namespace: fields +display_name: + default: displayName + example: displayName + required: true + region: basic + namespace: fields +username: + default: cn + required: true + region: basic + namespace: fields +organization_class: + default: organizationalUnit + example: organizationalUnit + required: true + region: basic + namespace: fields +user_filter: + default: (objectclass=inetorgperson) + example: (objectclass=inetorgperson) + required: true + region: basic + namespace: fields +user_class: + default: inetorgperson + example: inetorgperson + required: true + region: basic + namespace: fields +basic_pull_node: + required: true + region: basic + namespace: fields + +dynamic_fields_mapping: + required: false + default: {} + region: extend + namespace: fields + +password: + example: password + required: true + namespace: connection +user: + example: username + required: true + namespace: connection +base_dn: + example: CN + required: true + namespace: connection +pull_cycle: + default: 60 + example: 60 + required: true + namespace: connection +timeout_setting: + default: 120 + example: 120 + required: true + namespace: connection +ssl_encryption: + choices: + - "\u65E0" + - SSL + default: "\u65E0" + required: true + namespace: connection +connection_url: + example: ldap://localhost:389 + required: true + namespace: connection diff --git a/src/api/bkuser_core/categories/plugins/ldap/syncer.py b/src/api/bkuser_core/categories/plugins/ldap/syncer.py index 3ea987472..e80c7422a 100644 --- a/src/api/bkuser_core/categories/plugins/ldap/syncer.py +++ b/src/api/bkuser_core/categories/plugins/ldap/syncer.py @@ -29,14 +29,6 @@ logger = logging.getLogger(__name__) -SETTING_FIELD_MAP = { - "username": "username", - "display_name": "display_name", - "email": "email", - "telephone": "telephone", - "user_member_of": "user_member_of", -} - @dataclass class LDAPFetcher(Fetcher): @@ -46,7 +38,7 @@ class LDAPFetcher(Fetcher): def __post_init__(self): self.client = LDAPClient(self.config_loader) - self.field_mapper = ProfileFieldMapper(config_loader=self.config_loader, setting_field_map=SETTING_FIELD_MAP) + self.field_mapper = ProfileFieldMapper(config_loader=self.config_loader) self._data: Tuple[List, List, List] = None def fetch(self): @@ -55,7 +47,7 @@ def fetch(self): basic_pull_node=self.config_loader["basic_pull_node"], user_filter=self.config_loader["user_filter"], organization_class=self.config_loader["organization_class"], - user_group_filter=self.config_loader["user_group_filter"], + user_group_filter=self.config_loader.get("user_group_filter"), attributes=self.field_mapper.get_user_attributes(), ) @@ -65,7 +57,7 @@ def test_fetch_data(self, configs: dict): basic_pull_node=configs["basic_pull_node"], user_filter=configs["user_filter"], organization_class=configs["organization_class"], - user_group_filter=configs["user_group_filter"], + user_group_filter=configs.get("user_group_filter"), ) def _fetch_data( @@ -77,7 +69,10 @@ def _fetch_data( attributes: Optional[List] = None, ) -> tuple: try: - groups = self.client.search(start_root=basic_pull_node, force_filter_str=user_group_filter) + if user_group_filter: + groups = self.client.search(start_root=basic_pull_node, force_filter_str=user_group_filter) + else: + groups = [] except Exception: logger.exception("failed to get groups from remote server") raise FetchDataFromRemoteFailed("无法获取用户组,请检查配置") diff --git a/src/api/bkuser_core/categories/plugins/local/__init__.py b/src/api/bkuser_core/categories/plugins/local/__init__.py index 400992ac1..a9d0a872c 100644 --- a/src/api/bkuser_core/categories/plugins/local/__init__.py +++ b/src/api/bkuser_core/categories/plugins/local/__init__.py @@ -13,6 +13,8 @@ from .login import LoginHandler from .syncer import ExcelSyncer +# Q: 为什么 local 插件不使用 PluginConfig 注册 SettingMeta ? +# A: 因为目前与 local 插件相关的大部分配置在整个登录流程中都有使用,相当于全局配置,所以暂不放在插件配置中 DataSourcePlugin( name="local", syncer_cls=ExcelSyncer, diff --git a/src/api/bkuser_core/categories/plugins/mad/__init__.py b/src/api/bkuser_core/categories/plugins/mad/__init__.py index 955100b35..fefbdbc59 100644 --- a/src/api/bkuser_core/categories/plugins/mad/__init__.py +++ b/src/api/bkuser_core/categories/plugins/mad/__init__.py @@ -8,6 +8,9 @@ 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 os +from pathlib import Path + from bkuser_core.categories.plugins.plugin import DataSourcePlugin from .login import LoginHandler @@ -19,9 +22,5 @@ login_handler_cls=LoginHandler, allow_client_write=False, category_type="mad", - extra_config={ - "default_sync_period": 60, - "min_sync_period": 60, - "ldap_client": "bkuser_core.categories.plugins.ldap.client.LDAPClient", - }, + settings_path=os.path.dirname(__file__) / Path("settings.yaml"), ).register() diff --git a/src/api/bkuser_core/categories/plugins/mad/settings.yaml b/src/api/bkuser_core/categories/plugins/mad/settings.yaml new file mode 100644 index 000000000..09c5f99f9 --- /dev/null +++ b/src/api/bkuser_core/categories/plugins/mad/settings.yaml @@ -0,0 +1,122 @@ +ldap_max_paged_size: + default: 1000 +min_sync_period: + default: 60 +default_sync_period: + default: 120 + +user_member_of: + default: memberOf + example: memberOf + region: group + namespace: fields +user_group_description: + default: description + example: description + required: true + region: group + namespace: fields +user_group_name: + default: cn + example: cn + required: true + region: group + namespace: fields +user_group_filter: + default: (objectclass=groupOfUniqueNames) + example: (objectclass=groupOfUniqueNames) + required: true + region: group + namespace: fields +user_group_class: + default: groupOfUniqueNames + example: groupOfUniqueNames + required: true + region: group + namespace: fields + +telephone: + default: "Telephone" + example: "Telephone" + required: true + region: basic + namespace: fields +email: + default: email + example: email + required: true + region: basic + namespace: fields +display_name: + default: displayName + example: displayName + required: true + region: basic + namespace: fields +username: + default: "sAMAccountName" + required: true + region: basic + namespace: fields +organization_class: + default: organizationalUnit + example: organizationalUnit + required: true + region: basic + namespace: fields +user_filter: + default: "(&(objectCategory=Person)(sAMAccountName=*))" + example: "(&(objectCategory=Person)(sAMAccountName=*))" + required: true + region: basic + namespace: fields +user_class: + default: user + example: user + required: true + region: basic + namespace: fields +basic_pull_node: + required: true + region: basic + namespace: fields + +dynamic_fields_mapping: + required: false + default: {} + region: extend + namespace: fields + +password: + example: password + required: true + namespace: connection +user: + example: user + required: true + namespace: connection +base_dn: + example: CN + required: true + namespace: connection +pull_cycle: + default: 60 + example: 60 + required: true + namespace: connection +timeout_setting: + default: 120 + example: 120 + required: true + namespace: connection +ssl_encryption: + choices: + - "\u65E0" + - SSL + default: "\u65E0" + required: true + namespace: connection +connection_url: + example: ldap://localhost:389 + required: true + namespace: connection diff --git a/src/api/bkuser_core/categories/plugins/plugin.py b/src/api/bkuser_core/categories/plugins/plugin.py index f5fb694b9..2cf8d4d1b 100644 --- a/src/api/bkuser_core/categories/plugins/plugin.py +++ b/src/api/bkuser_core/categories/plugins/plugin.py @@ -8,15 +8,24 @@ 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 dataclasses import dataclass, field -from typing import Optional, Type +from pathlib import Path +from typing import Dict, Optional, Type from uuid import UUID +import yaml from bkuser_core.categories.constants import SyncTaskStatus from bkuser_core.categories.loader import register_plugin -from bkuser_core.categories.models import SyncProgress, SyncTask +from bkuser_core.categories.models import ProfileCategory, SyncProgress, SyncTask from bkuser_core.categories.plugins.base import LoginHandler, Syncer +from bkuser_core.categories.plugins.constants import HookType +from bkuser_core.common.models import is_obj_needed_update +from bkuser_core.user_settings.models import Setting, SettingMeta from rest_framework import serializers +from typing_extensions import Protocol + +logger = logging.getLogger(__name__) class SyncRecordSLZ(serializers.Serializer): @@ -25,6 +34,13 @@ class SyncRecordSLZ(serializers.Serializer): dt = serializers.DateTimeField() +class PluginHook(Protocol): + """插件钩子,用于各种事件后的回调""" + + def trigger(self, status: str, params: dict): + raise NotImplementedError + + @dataclass class DataSourcePlugin: """数据源插件,定义不同的数据源""" @@ -41,12 +57,62 @@ class DataSourcePlugin: # 是否允许通过 SaaS 修改,默认不允许 allow_client_write: bool = field(default_factory=lambda: False) login_handler_cls: Optional[Type[LoginHandler]] = None + settings_path: Optional[Path] = None # 其他额外配置 extra_config: dict = field(default_factory=dict) + hooks: Dict[HookType, Type[PluginHook]] = field(default_factory=dict) + def register(self): """注册插件""" register_plugin(self) + if self.settings_path is not None: + self.load_settings_from_yaml() + + def init_settings(self, setting_meta_key: str, meta_info: dict): + namespace = meta_info.pop("namespace", "general") + + try: + meta, created = SettingMeta.objects.get_or_create( + key=setting_meta_key, category_type=self.name, namespace=namespace, defaults=meta_info + ) + if created: + logger.debug("\n------ SettingMeta<%s> of plugin<%s> created.", setting_meta_key, self.name) + except Exception: # pylint: disable=broad-except + logger.exception("SettingMeta<%s> of plugin<%s> can not been created.", setting_meta_key, self.name) + return + + if is_obj_needed_update(meta, meta_info): + for k, v in meta_info.items(): + setattr(meta, k, v) + + try: + meta.save() + except Exception: # pylint: disable=broad-except + logger.exception("SettingMeta<%s> of plugin<%s> can not been updated.", setting_meta_key, self.name) + return + logger.debug("\n------ SettingMeta<%s> of plugin<%s> updated.", setting_meta_key, self.name) + + # 默认在创建 meta 后创建 settings,保证新增的配置能够被正确初始化 + if meta.default is not None: + # 理论上目录不能够被直接恢复, 所以已经被删除的目录不会被更新 + # 仅做新增,避免覆盖已有配置 + for category in ProfileCategory.objects.filter(type=self.category_type, enabled=True): + ins, created = Setting.objects.get_or_create( + meta=meta, category_id=category.id, defaults={"value": meta.default} + ) + if created: + logger.debug("\n------ Setting<%s> of category<%s> created.", ins, category) + + def load_settings_from_yaml(self): + """从 yaml 中加载 SettingMeta 配置""" + with self.settings_path.open(mode="r") as f: + for key, meta_info in yaml.safe_load(f).items(): + self.init_settings(key, meta_info) + + def get_hook(self, type_: HookType) -> Optional[PluginHook]: + hook_cls = self.hooks.get(type_) + return hook_cls() if hook_cls else None def sync(self, instance_id: int, task_id: UUID, *args, **kwargs): """同步数据""" diff --git a/src/api/bkuser_core/categories/plugins/utils.py b/src/api/bkuser_core/categories/plugins/utils.py index ea11c8ed7..f137237af 100644 --- a/src/api/bkuser_core/categories/plugins/utils.py +++ b/src/api/bkuser_core/categories/plugins/utils.py @@ -12,7 +12,9 @@ import logging from bkuser_core.categories.plugins.base import TypeList, TypeProtocol +from bkuser_core.categories.plugins.constants import DYNAMIC_FIELDS_SETTING_KEY from bkuser_core.common.progress import progress +from bkuser_core.user_settings.models import Setting from django_celery_beat.models import IntervalSchedule, PeriodicTask logger = logging.getLogger(__name__) @@ -43,7 +45,6 @@ def update_periodic_sync_task(category_id: int, operator: str, interval_seconds: except IntervalSchedule.MultipleObjectsReturned: schedule = IntervalSchedule.objects.filter(every=interval_seconds, period=IntervalSchedule.SECONDS)[0] - # 通过 category_id 来做任务名 kwargs = json.dumps({"instance_id": category_id, "operator": operator}) try: p: PeriodicTask = PeriodicTask.objects.get(name=str(category_id)) @@ -53,7 +54,7 @@ def update_periodic_sync_task(category_id: int, operator: str, interval_seconds: except PeriodicTask.DoesNotExist: create_params = { "interval": schedule, - "name": str(category_id), + "name": f"plugin-sync-data-{category_id}", "task": "bkuser_core.categories.tasks.adapter_sync", "enabled": True, "kwargs": kwargs, @@ -63,15 +64,26 @@ def update_periodic_sync_task(category_id: int, operator: str, interval_seconds: def delete_periodic_sync_task(category_id: int): """删除同步周期任务""" + guess_names = [f"plugin-sync-data-{category_id}", str(category_id)] # 通过 category_id 来做任务名 try: - PeriodicTask.objects.get(name=str(category_id)).delete() + PeriodicTask.objects.filter(name__in=guess_names).delete() except PeriodicTask.DoesNotExist: logger.warning("PeriodicTask %s has been deleted, skip it...", str(category_id)) return +def delete_dynamic_filed(dynamic_field: str): + """删除指定自定义字段配置""" + settings = Setting.objects.filter(meta__key=DYNAMIC_FIELDS_SETTING_KEY) + + for setting in settings: + if dynamic_field in setting.value: + setting.value.pop(dynamic_field) + setting.save() + + def handle_with_progress_info( item_list: TypeList[TypeProtocol], progress_title: str, continue_if_exception: bool = True ): diff --git a/src/api/bkuser_core/categories/serializers.py b/src/api/bkuser_core/categories/serializers.py index 632666a75..b05fa473e 100644 --- a/src/api/bkuser_core/categories/serializers.py +++ b/src/api/bkuser_core/categories/serializers.py @@ -10,10 +10,10 @@ """ from typing import List +from bkuser_core.apis.v2.serializers import CustomFieldsModelSerializer, DurationTotalSecondField from bkuser_core.bkiam.serializers import AuthInfoSLZ from bkuser_core.categories import constants from bkuser_core.categories.models import ProfileCategory -from bkuser_core.common.serializers import CustomFieldsModelSerializer, DurationTotalSecondField from bkuser_core.profiles.validators import validate_domain from django.utils.translation import ugettext_lazy as _ from rest_framework.serializers import ( @@ -98,7 +98,8 @@ class CategoryTestFetchDataSerializer(Serializer): basic_pull_node = CharField() user_filter = CharField() organization_class = CharField() - user_group_filter = CharField() + user_group_filter = CharField(required=False, allow_blank=True, allow_null=True) + user_member_of = CharField(required=False, allow_blank=True, allow_null=True) class SyncTaskSerializer(Serializer): @@ -109,6 +110,7 @@ class SyncTaskSerializer(Serializer): operator = CharField(help_text="操作人") create_time = DateTimeField(help_text="开始时间") required_time = DurationTotalSecondField(help_text="耗时") + retried_count = IntegerField(help_text="重试次数") class SyncTaskProcessSerializer(Serializer): diff --git a/src/api/bkuser_core/categories/signals.py b/src/api/bkuser_core/categories/signals.py index 77f5a1257..85610d739 100644 --- a/src/api/bkuser_core/categories/signals.py +++ b/src/api/bkuser_core/categories/signals.py @@ -12,3 +12,4 @@ post_category_create = django.dispatch.Signal(providing_args=["instance", "operator", "extra_values"]) post_category_delete = django.dispatch.Signal(providing_args=["instance", "operator", "extra_values"]) +post_dynamic_field_delete = django.dispatch.Signal(providing_args=["instance", "operator", "extra_values"]) diff --git a/src/api/bkuser_core/categories/tasks.py b/src/api/bkuser_core/categories/tasks.py index db7d29449..635af42b3 100644 --- a/src/api/bkuser_core/categories/tasks.py +++ b/src/api/bkuser_core/categories/tasks.py @@ -10,53 +10,96 @@ """ import logging import uuid -from typing import Optional +from contextlib import contextmanager +from typing import Any, Optional, Union -from bkuser_core.categories.constants import SyncTaskType +from bkuser_core.categories.constants import SyncTaskStatus, SyncTaskType from bkuser_core.categories.exceptions import ExistsSyncingTaskError from bkuser_core.categories.loader import get_plugin_by_category from bkuser_core.categories.models import ProfileCategory, SyncTask +from bkuser_core.categories.plugins.constants import HookType from bkuser_core.categories.utils import catch_time from bkuser_core.celery import app from bkuser_core.common.cache import clear_cache from bkuser_core.common.error_codes import error_codes +from celery import Task +from django.conf import settings logger = logging.getLogger(__name__) -@app.task +class RetryWithHookTask(Task): + """A task will retry automatically, with plugin hook executing""" + + autoretry_for = (Exception,) + retry_kwargs = {"max_retries": settings.TASK_MAX_RETRIES} + retry_backoff = settings.RETRY_BACKOFF + retry_jitter = True + + def after_return(self, status, retval, task_id, args, kwargs, einfo): + category = ProfileCategory.objects.get(pk=kwargs["instance_id"]) + logger.info("Sync data task<%s> of category<%s> got result: %s", task_id, category, status) + + plugin = get_plugin_by_category(category) + post_sync_hook = plugin.get_hook(HookType.POST_SYNC) + if post_sync_hook: + kwargs.update({"retries": self.request.retries, "category": category}) + post_sync_hook.trigger(status, kwargs) + + +@contextmanager +def sync_data_task(category: ProfileCategory, task_id: Union[uuid.UUID, Any], should_retry: bool): + """同步数据任务,支持标记重试、失败、成功""" + sync_task = SyncTask.objects.get(id=task_id) + try: + yield + except Exception: + if should_retry: + status = SyncTaskStatus.RETRYING.value + sync_task.retried_count += 1 + else: + status = SyncTaskStatus.FAILED.value + + sync_task.status = status + sync_task.save(update_fields=["retried_count", "status", "update_time"]) + raise + else: + # 标记同步 + category.mark_synced() + sync_task.status = SyncTaskStatus.SUCCESSFUL.value + sync_task.save(update_fields=["status", "update_time"]) + + # 同步成功后,清理当前的缓存 + clear_cache() + + +@app.task(base=RetryWithHookTask) def adapter_sync(instance_id: int, operator: str, task_id: Optional[uuid.UUID] = None, *args, **kwargs): logger.info("going to sync Category<%s>", instance_id) - instance = ProfileCategory.objects.get(pk=instance_id) + category = ProfileCategory.objects.get(pk=instance_id) if task_id is None: # 只有定时任务未传递 task_id try: - task_id = SyncTask.objects.register_task(category=instance, operator=operator, type_=SyncTaskType.AUTO).id + task_id = SyncTask.objects.register_task(category=category, operator=operator, type_=SyncTaskType.AUTO).id except ExistsSyncingTaskError as e: raise error_codes.LOAD_DATA_FAILED.f(str(e)) - with SyncTask.objects.get(id=task_id): - try: - plugin = get_plugin_by_category(instance) - except ValueError: - logger.exception("category type<%s> is not support", instance.type) - raise error_codes.CATEGORY_TYPE_NOT_SUPPORTED - except Exception: - logger.exception( - "load adapter<%s-%s-%s> failed", - instance.type, - instance.display_name, - instance.id, - ) - raise error_codes.LOAD_DATA_ADAPTER_FAILED + try: + plugin = get_plugin_by_category(category) + except ValueError: + logger.exception("category type<%s> is not support", category.type) + raise error_codes.CATEGORY_TYPE_NOT_SUPPORTED + except Exception: + logger.exception( + "load adapter<%s-%s-%s> failed", + category.type, + category.display_name, + category.id, + ) + raise error_codes.LOAD_DATA_ADAPTER_FAILED + with sync_data_task(category, task_id, adapter_sync.request.retries < adapter_sync.max_retries): with catch_time() as context: plugin.sync(instance_id=instance_id, task_id=task_id, *args, **kwargs) logger.info(f"同步总耗时: {context.time_delta}s, 消耗总CPU时间: {context.clock_delta}s.") - - # 标记同步 - instance.mark_synced() - - # 同步成功后,清理当前的缓存 - clear_cache() diff --git a/src/api/bkuser_core/categories/urls.py b/src/api/bkuser_core/categories/urls.py index 9d10a0a1d..a76530ce1 100644 --- a/src/api/bkuser_core/categories/urls.py +++ b/src/api/bkuser_core/categories/urls.py @@ -8,7 +8,7 @@ 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 bkuser_core.common.constants import LOOKUP_FIELD_NAME +from bkuser_core.apis.v2.constants import LOOKUP_FIELD_NAME from django.urls.conf import path, re_path from . import views diff --git a/src/api/bkuser_core/categories/views.py b/src/api/bkuser_core/categories/views.py index ec93b44b4..875e02584 100644 --- a/src/api/bkuser_core/categories/views.py +++ b/src/api/bkuser_core/categories/views.py @@ -11,6 +11,8 @@ import logging from typing import List +from bkuser_core.apis.v2.serializers import EmptySerializer +from bkuser_core.apis.v2.viewset import AdvancedListAPIView, AdvancedModelViewSet, AdvancedSearchFilter from bkuser_core.audit.constants import OperationType from bkuser_core.audit.utils import audit_general_log from bkuser_core.bkiam.permissions import IAMAction, IAMHelper, IAMPermissionExtraInfo, need_iam @@ -34,8 +36,6 @@ from bkuser_core.categories.tasks import adapter_sync from bkuser_core.common.cache import clear_cache_if_succeed from bkuser_core.common.error_codes import CoreAPIError, error_codes -from bkuser_core.common.serializers import EmptySerializer -from bkuser_core.common.viewset import AdvancedListAPIView, AdvancedModelViewSet, AdvancedSearchFilter from django.utils.decorators import method_decorator from drf_yasg.utils import swagger_auto_schema from rest_framework import filters, status @@ -81,7 +81,6 @@ def make_meta(type_: CategoryType): try: action_id = IAMAction.get_action_by_category_type(type_) except KeyError: - # tof 属于隐藏目录,这里直接忽略掉 continue _meta = make_meta(type_) diff --git a/src/api/bkuser_core/common/db_sync.py b/src/api/bkuser_core/common/db_sync.py index 41e7d83e5..f8cce6e03 100644 --- a/src/api/bkuser_core/common/db_sync.py +++ b/src/api/bkuser_core/common/db_sync.py @@ -32,7 +32,7 @@ class SyncModelMeta: 例如, excel 导入数据时,是用 username 而不是 code 作为唯一标识 - ldap or mad 是有 uuid 作为 code 的,tof 是有也有数字 id 作为 code + ldap or mad 是有 uuid 作为 code 的 """ target_model: ClassVar[Type[models.Model]] @@ -94,7 +94,7 @@ def add(self, db_obj: models.Model, operation: SyncOperation = None) -> None: if self.meta.unique_key_field: unique_key = getattr(db_obj, self.meta.unique_key_field) if unique_key and unique_key in self._action_map_cache: - logger.info("action (%s-%s) already in item cache, skipping", operation, db_obj) + logger.debug("action (%s-%s) already in item cache, skipping", operation, db_obj) return # 尚不存在,添加 unique_key cache diff --git a/src/api/bkuser_core/common/error_codes.py b/src/api/bkuser_core/common/error_codes.py index 8243472aa..c0dd3445c 100644 --- a/src/api/bkuser_core/common/error_codes.py +++ b/src/api/bkuser_core/common/error_codes.py @@ -9,8 +9,8 @@ specific language governing permissions and limitations under the License. """ import copy +from typing import Optional -from django.conf import settings from django.utils.translation import gettext_lazy as _ from rest_framework.exceptions import APIException from rest_framework.status import HTTP_400_BAD_REQUEST, HTTP_409_CONFLICT @@ -23,16 +23,18 @@ class CoreAPIError(APIException): def __init__(self, code): self.code = code + self.data = {} super().__init__(str(self)) def __str__(self): return f"CoreAPIError {self.code.status_code}-{self.code.code_name}" - def format(self, message=None, replace=False, **kwargs): + def format(self, message: Optional[str] = None, replace: bool = False, data: Optional[dict] = None, **kwargs): """Using a customized message for this ErrorCode + :param data: exception body :param str message: if not given, default message will be used - :param bool replace: relace default message if true + :param bool replace: replace default message if true """ self.code = copy.copy(self.code) if message: @@ -45,6 +47,9 @@ def format(self, message=None, replace=False, **kwargs): if kwargs: self.code.message = self.code.message.format(**kwargs) + if data: + self.data = data + return self def f(self, message=None, **kwargs): @@ -59,7 +64,7 @@ def code_num(self): return self.code.code_num -class ErrorCode(object): +class ErrorCode: """Error code""" def __init__(self, code_name, message, code_num=-1, status_code=HTTP_400_BAD_REQUEST): @@ -69,7 +74,7 @@ def __init__(self, code_name, message, code_num=-1, status_code=HTTP_400_BAD_REQ self.status_code = status_code -class ErrorCodeCollection(object): +class ErrorCodeCollection: """A collection of ErrorCodes""" def __init__(self): @@ -98,24 +103,23 @@ def __getattr__(self, code_name): ErrorCode("RESOURCE_RESTORATION_FAILED", _("资源恢复失败")), # 登陆相关 ErrorCode("USER_DOES_NOT_EXIST", _("账号不存在"), 3210010), + ErrorCode("TOO_MANY_TRY", _("密码输入错误次数过多,已被锁定"), 3210011), ErrorCode("USERNAME_FORMAT_ERROR", _("账户名格式错误"), 3210012), - ErrorCode("DOMAIN_UNKNOWN", _("未知登陆域"), 3210017), + ErrorCode("PASSWORD_ERROR", _("账户或者密码错误,请重新输入"), 3210013), ErrorCode("USER_EXIST_MANY", _("存在多个同名账号,请联系管理员"), 3210014), - ErrorCode("USER_IS_DISABLED", _("账号已被管理员禁用,请联系管理员"), 3210016), ErrorCode("USER_IS_LOCKED", _("账号长时间未登录,已被冻结,请联系管理员"), 3210015), - ErrorCode("PASSWORD_ERROR", _("账户名和密码不匹配"), 3210013), - ErrorCode( - "PASSWORD_DUPLICATED", - _("新密码不能与最近{}次密码相同").format(settings.MAX_PASSWORD_HISTORY), - ), - ErrorCode("PASSWORD_EXPIRED", _("该账户密码已到期,请修改密码后登录"), 3210018), - ErrorCode("TOO_MANY_TRY", _("密码输入错误次数过多,已被锁定"), 3210011), + ErrorCode("USER_IS_DISABLED", _("账号已被管理员禁用,请联系管理员"), 3210016), + ErrorCode("DOMAIN_UNKNOWN", _("未知登陆域"), 3210017), + ErrorCode("PASSWORD_EXPIRED", _("该账户密码已到期"), 3210018), ErrorCode("CATEGORY_NOT_ENABLED", _("用户目录未启用"), 3210019), - ErrorCode("ERROR_FORMAT", _("传入参数错误"), 3210019), + ErrorCode("ERROR_FORMAT", _("传入参数错误"), 3210020), + ErrorCode("SHOULD_CHANGE_INITIAL_PASSWORD", _("平台分配的初始密码未修改"), 3210021), # 用户相关 + ErrorCode("PASSWORD_DUPLICATED", _("新密码不能与最近{max_password_history}次密码相同")), ErrorCode("EMAIL_NOT_PROVIDED", _("该用户没有提供邮箱,发送邮件失败")), ErrorCode("USER_ALREADY_EXISTED", _("该目录下此用户名已存在"), status_code=HTTP_409_CONFLICT), ErrorCode("SAVE_USER_INFO_FAILED", _("保存用户信息失败")), + ErrorCode("PASSWORD_DUPLICATED", _("新密码不能与最近{max_password_history}次密码相同")), # 上传文件相关 ErrorCode("FILE_IMPORT_TOO_LARGE", _("上传文件过大")), ErrorCode("FILE_IMPORT_FORMAT_ERROR", _("上传文件格式错误")), @@ -141,6 +145,7 @@ def __getattr__(self, code_name): # 配置相关 ErrorCode("CANNOT_FIND_SETTING_META", _("找不到对应的配置元信息")), ErrorCode("CANNOT_CREATE_SETTING", _("无法创建配置")), + ErrorCode("CANNOT_UPDATE_SETTING", _("无法更新配置")), # 组织架构相关 ErrorCode("DEPARTMENT_NAME_CONFLICT", _("同一个部门下子部门命名冲突")), # 用户字段相关 diff --git a/src/api/bkuser_core/common/exception_handler.py b/src/api/bkuser_core/common/exception_handler.py index dfb4ee400..dcfc78bde 100644 --- a/src/api/bkuser_core/common/exception_handler.py +++ b/src/api/bkuser_core/common/exception_handler.py @@ -56,6 +56,7 @@ def get_ee_exception_response(exc, context): # 主动抛出的已知异常 data["code"] = exc.code_num data["message"] = exc.message + data["data"] = exc.data or None elif isinstance(exc, Http404): data["message"] = "404, could not be found" elif isinstance(exc, PermissionDenied): diff --git a/src/api/bkuser_core/common/http.py b/src/api/bkuser_core/common/http.py index 8e9fe1da7..aaf143625 100644 --- a/src/api/bkuser_core/common/http.py +++ b/src/api/bkuser_core/common/http.py @@ -82,6 +82,9 @@ def should_use_raw_response(req: "HttpRequest", resp: "Response") -> bool: if not req.path.startswith("/api/"): return True + if req.path.startswith("/api/v3/"): + return True + # 是否强制使用原生格式 if exist_force_raw_header(req): return True diff --git a/src/api/bkuser_core/common/models.py b/src/api/bkuser_core/common/models.py index ae281609b..4c4da3ec5 100644 --- a/src/api/bkuser_core/common/models.py +++ b/src/api/bkuser_core/common/models.py @@ -30,3 +30,15 @@ def enable_or_disable(self, is_enable: bool, *args, **kwargs): disable_param = kwargs.pop("disable_param", {"enabled": False}) self.get_queryset().filter(*args, **kwargs).update(**disable_param) return + + +def is_obj_needed_update(obj, updated_values: dict) -> bool: + """判断对象是否需要被更新""" + for attr, updated_value in updated_values.items(): + if not hasattr(obj, attr): + raise ValueError(f"{attr} is not a valid attribution for {obj}.") + + if getattr(obj, attr) != updated_value: + return True + + return False diff --git a/src/api/bkuser_core/config/common/.env-tmpl b/src/api/bkuser_core/config/common/.env-tmpl index 6df98cdd6..20b9e9231 100644 --- a/src/api/bkuser_core/config/common/.env-tmpl +++ b/src/api/bkuser_core/config/common/.env-tmpl @@ -1,4 +1,5 @@ BK_PAAS_URL="http://paas.example.com" +BK_USER_SAAS_URL="http://bkuser-saas.example.com" BK_APP_CODE="bk-user" BK_APP_SECRET="some-default-token" diff --git a/src/api/bkuser_core/config/common/django_basic.py b/src/api/bkuser_core/config/common/django_basic.py index 8d6e8fbb8..6bfb40e54 100644 --- a/src/api/bkuser_core/config/common/django_basic.py +++ b/src/api/bkuser_core/config/common/django_basic.py @@ -146,6 +146,7 @@ "rest_framework.renderers.JSONRenderer", ], "DEFAULT_VERSIONING_CLASS": "rest_framework.versioning.URLPathVersioning", + "ORDERING_PARAM": "ordering", } SWAGGER_SETTINGS = { diff --git a/src/api/bkuser_core/config/common/logging.py b/src/api/bkuser_core/config/common/logging.py index 49e3c827a..5e5fea7cb 100644 --- a/src/api/bkuser_core/config/common/logging.py +++ b/src/api/bkuser_core/config/common/logging.py @@ -17,5 +17,4 @@ # ============================================================================== LOGGING_DIR = env("LOGGING_DIR", default=Path(PROJECT_ROOT) / "logs") -LOG_CLASS = "logging.handlers.RotatingFileHandler" LOG_LEVEL = env("LOG_LEVEL", default="INFO") diff --git a/src/api/bkuser_core/config/common/platform.py b/src/api/bkuser_core/config/common/platform.py index 4cba72eed..b649afcf7 100644 --- a/src/api/bkuser_core/config/common/platform.py +++ b/src/api/bkuser_core/config/common/platform.py @@ -48,7 +48,7 @@ # SaaS 应用 Code SAAS_CODE = "bk_user_manage" # SaaS 请求地址,用于拼接访问地址(默认支持二进制部署) -SAAS_URL = env("SAAS_URL", default=urllib.parse.urljoin(BK_PAAS_URL, f"/o/{SAAS_CODE}/")) +SAAS_URL = env("BK_USER_SAAS_URL", default=urllib.parse.urljoin(BK_PAAS_URL, f"/o/{SAAS_CODE}/")) # SaaS 偏好 client ip 头 CLIENT_IP_FROM_SAAS_HEADER = "HTTP_CLIENT_IP_FROM_SAAS" diff --git a/src/api/bkuser_core/config/common/storage.py b/src/api/bkuser_core/config/common/storage.py index fc583b44d..35a639151 100644 --- a/src/api/bkuser_core/config/common/storage.py +++ b/src/api/bkuser_core/config/common/storage.py @@ -15,7 +15,7 @@ # ============================================================================== # 数据库 # ============================================================================== -DB_PREFIX = "DB" +DB_PREFIX = env("DB_PREFIX", default="DB") DATABASES = get_db_config(env, DB_PREFIX) diff --git a/src/api/bkuser_core/config/common/system.py b/src/api/bkuser_core/config/common/system.py index 9955f0854..a28bb258f 100644 --- a/src/api/bkuser_core/config/common/system.py +++ b/src/api/bkuser_core/config/common/system.py @@ -19,7 +19,7 @@ # 最大密码长度(明文) PASSWORD_MAX_LENGTH = 32 # 重复密码最大历史数量 -MAX_PASSWORD_HISTORY = 3 +DEFAULT_MAX_PASSWORD_HISTORY = 3 # 用于加密密码历史 try: # there is a bug in django-environ, the default in bytes() is unused. @@ -48,8 +48,10 @@ # 最大的自定义字段数量(暂未启用) MAX_DYNAMIC_FIELDS = 20 -# 默认用户 Token 的过期时间 +# 默认用户 Token 的过期时间(用于发送邮件) DEFAULT_TOKEN_EXPIRE_SECONDS = 12 * 60 * 60 +# 页面临时生成用户 Token +PAGE_TOKEN_EXPIRE_SECONDS = 5 * 60 # 国际号码段默认值 DEFAULT_COUNTRY_CODE = "86" @@ -63,6 +65,11 @@ # 最大分页数量 MAX_PAGE_SIZE = env.int("MAX_PAGE_SIZE", default=2000) + +# 登录次数统计时间周期, 默认为一个月 +LOGIN_RECORD_COUNT_SECONDS = env.int("LOGIN_RECORD_COUNT_SECONDS", default=60 * 60 * 24 * 30) + +DRF_CROWN_DEFAULT_CONFIG = {"remain_request": True} # ============================================================================== # 开发调试 # ============================================================================== @@ -71,3 +78,9 @@ # 是否使用进度条(本地开发方便) USE_PROGRESS_BAR = False + +# ============================================================================== +# 数据同步 +# ============================================================================== +TASK_MAX_RETRIES = env.int("TASK_MAX_RETRIES", default=3) +RETRY_BACKOFF = env.int("RETRY_BACKOFF", default=30) diff --git a/src/api/bkuser_core/config/overlays/dev.tmpl b/src/api/bkuser_core/config/overlays/dev.tmpl index 4b1cc6b22..4a48b1aed 100644 --- a/src/api/bkuser_core/config/overlays/dev.tmpl +++ b/src/api/bkuser_core/config/overlays/dev.tmpl @@ -4,7 +4,7 @@ from bkuser_core.config.common.logging import * # noqa from bkuser_core.config.common.platform import * # noqa from bkuser_core.config.common.storage import * # noqa from bkuser_core.config.common.system import * # noqa -from bkuser_global.config import get_logging_config_dict +from bkuser_global.logging import get_logging, LoggingType DEBUG = True @@ -12,14 +12,19 @@ DEBUG = True # 日志设置 # =============================================================================== LOG_LEVEL = "DEBUG" -LOGGING = get_logging_config_dict( - log_level=LOG_LEVEL, - logging_dir=LOGGING_DIR, - log_class=LOG_CLASS, - file_name=APP_ID, - package_name="bkuser_core", +LOGGING = get_logging( + logging_type=LoggingType.STDOUT, log_level=LOG_LEVEL, package_name="bkuser_core", formatter="simple" ) -LOGGING["loggers"]["bkuser_core"]["handlers"] = ["console"] + +# use file logger +# LOGGING = get_logging( +# logging_type=LoggingType.FILE, +# log_level=LOG_LEVEL, +# package_name="bkuser_core", +# formatter="simple", +# logging_dir=LOGGING_DIR, +# file_name="api", +# ) # ============================================================================== @@ -60,8 +65,3 @@ if ENABLE_PROFILING: # silk middleware should be placed before any middleware using process_request MIDDLEWARE.insert(0, "silk.middleware.SilkyMiddleware") - -# ============================================================================== -# SaaS -# ============================================================================== -SAAS_URL = urllib.parse.urljoin(BK_PAAS_URL, f"/o/{SAAS_CODE}/") diff --git a/src/api/bkuser_core/config/overlays/prod.py b/src/api/bkuser_core/config/overlays/prod.py index 1ee6bde84..7746f4527 100644 --- a/src/api/bkuser_core/config/overlays/prod.py +++ b/src/api/bkuser_core/config/overlays/prod.py @@ -14,16 +14,6 @@ from bkuser_core.config.common.storage import * # noqa from bkuser_core.config.common.system import * # noqa -from bkuser_global.config import get_logging_config_dict +from bkuser_global.logging import LoggingType, get_logging -LOG_LEVEL = "INFO" - -LOGGING = get_logging_config_dict( - log_level=LOG_LEVEL, - logging_dir=LOGGING_DIR, - log_class=LOG_CLASS, - file_name=APP_ID, - package_name="bkuser_core", -) - -SAAS_URL = urllib.parse.urljoin(BK_PAAS_URL, f"/o/{SAAS_CODE}/") +LOGGING = get_logging(logging_type=LoggingType.STDOUT, log_level=LOG_LEVEL, package_name="bkuser_core") diff --git a/src/api/bkuser_core/config/overlays/unittest.py b/src/api/bkuser_core/config/overlays/unittest.py index bb76e226d..b6d236cd8 100644 --- a/src/api/bkuser_core/config/overlays/unittest.py +++ b/src/api/bkuser_core/config/overlays/unittest.py @@ -6,7 +6,7 @@ from bkuser_core.config.common.storage import * # noqa from bkuser_core.config.common.system import * # noqa -from bkuser_global.config import get_logging_config_dict +from bkuser_global.logging import LoggingType, get_logging DEBUG = True @@ -14,14 +14,7 @@ # 日志设置 # =============================================================================== LOG_LEVEL = "DEBUG" -LOGGING = get_logging_config_dict( - log_level=LOG_LEVEL, - logging_dir=LOGGING_DIR, - log_class=LOG_CLASS, - file_name=APP_ID, - package_name="bkuser_core", -) -LOGGING["loggers"]["bkuser_core"]["handlers"] = ["console"] +LOGGING = get_logging(logging_type=LoggingType.STDOUT, log_level=LOG_LEVEL, package_name="bkuser_core") # ============================================================================== # Test Ldap @@ -55,8 +48,3 @@ # profiling # ============================================================================== ENABLE_PROFILING = False - -# ============================================================================== -# SaaS -# ============================================================================== -SAAS_URL = urllib.parse.urljoin(BK_PAAS_URL, f"/o/{SAAS_CODE}/") diff --git a/src/api/bkuser_core/departments/serializers.py b/src/api/bkuser_core/departments/serializers.py index f26d6ab34..c6075aa34 100644 --- a/src/api/bkuser_core/departments/serializers.py +++ b/src/api/bkuser_core/departments/serializers.py @@ -10,7 +10,7 @@ """ from typing import Dict, List -from bkuser_core.common.serializers import ( +from bkuser_core.apis.v2.serializers import ( AdvancedListSerializer, AdvancedRetrieveSerialzier, CustomFieldsMixin, diff --git a/src/api/bkuser_core/departments/urls.py b/src/api/bkuser_core/departments/urls.py index 14c81ca61..a1d947b4e 100644 --- a/src/api/bkuser_core/departments/urls.py +++ b/src/api/bkuser_core/departments/urls.py @@ -8,7 +8,7 @@ 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 bkuser_core.common.constants import LOOKUP_FIELD_NAME +from bkuser_core.apis.v2.constants import LOOKUP_FIELD_NAME from django.conf.urls import url from . import views diff --git a/src/api/bkuser_core/departments/views.py b/src/api/bkuser_core/departments/views.py index e5fdb8af0..9c789749c 100644 --- a/src/api/bkuser_core/departments/views.py +++ b/src/api/bkuser_core/departments/views.py @@ -10,6 +10,13 @@ """ from typing import Type +from bkuser_core.apis.v2.serializers import AdvancedRetrieveSerialzier, EmptySerializer +from bkuser_core.apis.v2.viewset import ( + AdvancedBatchOperateViewSet, + AdvancedListAPIView, + AdvancedModelViewSet, + AdvancedSearchFilter, +) from bkuser_core.audit.constants import OperationType from bkuser_core.audit.utils import audit_general_log from bkuser_core.bkiam.exceptions import IAMPermissionDenied @@ -18,19 +25,12 @@ 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 -from bkuser_core.common.serializers import AdvancedRetrieveSerialzier, EmptySerializer -from bkuser_core.common.viewset import ( - AdvancedBatchOperateViewSet, - AdvancedListAPIView, - AdvancedModelViewSet, - AdvancedSearchFilter, -) from bkuser_core.departments import serializers as local_serializers from bkuser_core.departments.models import Department, DepartmentThroughModel from bkuser_core.departments.signals import post_department_create from bkuser_core.profiles.models import DynamicFieldInfo, Profile -from bkuser_core.profiles.serializers import ProfileMinimalSerializer, ProfileSerializer, RapidProfileSerializer from bkuser_core.profiles.utils import force_use_raw_username +from bkuser_core.profiles.v2.serializers import ProfileMinimalSerializer, ProfileSerializer, RapidProfileSerializer from django.conf import settings from django.utils.decorators import method_decorator from django.utils.translation import ugettext_lazy as _ diff --git a/src/api/bkuser_core/esb_sdk/apis/cmsi.py b/src/api/bkuser_core/esb_sdk/apis/cmsi.py index 32c4e3334..90b8b7faf 100644 --- a/src/api/bkuser_core/esb_sdk/apis/cmsi.py +++ b/src/api/bkuser_core/esb_sdk/apis/cmsi.py @@ -29,10 +29,10 @@ def __init__(self, client): path="/api/c/compapi{bk_api_ver}/cmsi/send_mp_weixin/", description=u"发送公众号微信消息", ) - self.send_qy_weixin = ComponentAPI( + self.send_rtx = ComponentAPI( client=self.client, method="POST", - path="/api/c/compapi{bk_api_ver}/cmsi/send_qy_weixin/", + path="/api/c/compapi{bk_api_ver}/cmsi/send_rtx/", description=u"发送企业微信", ) self.send_sms = ComponentAPI( diff --git a/src/api/bkuser_core/esb_sdk/client.py b/src/api/bkuser_core/esb_sdk/client.py index a0434c851..9e2ca3291 100644 --- a/src/api/bkuser_core/esb_sdk/client.py +++ b/src/api/bkuser_core/esb_sdk/client.py @@ -8,8 +8,6 @@ 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. """ -"""Component API Client -""" import json import logging import random @@ -22,13 +20,6 @@ from . import collections, conf from .utils import get_signature -# shutdown urllib3's warning -try: - requests.packages.urllib3.disable_warnings() -except ImportError: - pass - - logger = logging.getLogger("component") @@ -148,7 +139,7 @@ def request(self, method, url, params=None, data=None, **kwargs): if method == "POST": params = {} - url_path = urlparse.urlparse(url).path + url_path = urlparse(url).path # signature always in GET params params.update( { diff --git a/src/api/bkuser_core/profiles/managers.py b/src/api/bkuser_core/profiles/managers.py index 52e738018..fd5eb68a6 100644 --- a/src/api/bkuser_core/profiles/managers.py +++ b/src/api/bkuser_core/profiles/managers.py @@ -79,11 +79,10 @@ def get_extras_default_values(self) -> dict: class ProfileTokenManager(models.Manager): - def create(self, profile): + def create(self, profile, token_expire_seconds: int = settings.DEFAULT_TOKEN_EXPIRE_SECONDS): token = secrets.token_urlsafe(32) - # TODO: use another way generate token, - is not safe for swagger sdk, still not know why token = token.replace("-", "0") - expire_time = now() + datetime.timedelta(seconds=settings.DEFAULT_TOKEN_EXPIRE_SECONDS) + expire_time = now() + datetime.timedelta(seconds=token_expire_seconds) return super().create(token=token, profile=profile, expire_time=expire_time) diff --git a/src/api/bkuser_core/profiles/tasks.py b/src/api/bkuser_core/profiles/tasks.py index 2617f1feb..06bda00ab 100644 --- a/src/api/bkuser_core/profiles/tasks.py +++ b/src/api/bkuser_core/profiles/tasks.py @@ -9,16 +9,17 @@ specific language governing permissions and limitations under the License. """ import logging +import urllib.parse 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.utils import make_passwd_reset_url_by_token from bkuser_core.user_settings.loader import ConfigProvider from django.conf import settings -from . import exceptions -from .models import Profile - logger = logging.getLogger(__name__) @@ -49,8 +50,10 @@ def send_password_by_email(profile_id: int, raw_password: str = None, init: bool # 从平台重置密码 if token: email_config = config_loader["reset_mail_config"] - url = settings.SAAS_URL + "set_password?token=%s " % token - message = email_config["content"].format(url=url, reset_url=settings.SAAS_URL + "reset_password ") + message = email_config["content"].format( + url=make_passwd_reset_url_by_token(token), + reset_url=urllib.parse.urljoin(settings.SAAS_URL, "reset_password"), + ) # 在用户管理里管理操作重置密码 else: email_config = config_loader["reset_mail_config"] diff --git a/src/api/bkuser_core/profiles/urls.py b/src/api/bkuser_core/profiles/urls.py index a33ff9169..9291a0dff 100644 --- a/src/api/bkuser_core/profiles/urls.py +++ b/src/api/bkuser_core/profiles/urls.py @@ -8,151 +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. """ -from bkuser_core.common.constants import LOOKUP_FIELD_NAME -from django.conf.urls import url -from . import views +from bkuser_core.profiles.v2.urls import urlpatterns as v2_urlpatterns +from bkuser_core.profiles.v3.urls import urlpatterns as v3_urlpatterns -PVAR_PROFILE_ID = r"(?P<%s>[\w\-\@\.\$]+)" % LOOKUP_FIELD_NAME -PVAR_TOKEN = r"(?P[\w]+)" - -urlpatterns = [ - url( - r"^api/v2/profiles/$", - views.ProfileViewSet.as_view( - { - "get": "list", - "post": "create", - } - ), - name="profiles", - ), - url( - r"^api/v2/profiles/%s/$" % PVAR_PROFILE_ID, - views.ProfileViewSet.as_view( - { - "get": "retrieve", - "put": "update", - "patch": "partial_update", - "delete": "destroy", - } - ), - name="profiles.action", - ), - url( - r"^api/v2/profiles/%s/restoration/$" % PVAR_PROFILE_ID, - views.ProfileViewSet.as_view( - { - "post": "restoration", - } - ), - name="profiles.restoration", - ), - url( - r"^api/v2/profiles/%s/departments/$" % PVAR_PROFILE_ID, - views.ProfileViewSet.as_view( - { - "get": "get_departments", - } - ), - name="profiles.departments", - ), - url( - r"^api/v2/profiles/%s/leaders/$" % PVAR_PROFILE_ID, - views.ProfileViewSet.as_view( - { - "get": "get_leaders", - } - ), - name="profiles.leaders", - ), - url( - r"^api/v2/profiles/%s/token/$" % PVAR_PROFILE_ID, - views.ProfileViewSet.as_view( - { - "post": "generate_token", - } - ), - name="profiles.generate_token", - ), - url( - r"^api/v2/batch/profiles/$", - views.BatchProfileViewSet.as_view( - { - "get": "multiple_retrieve", - "patch": "multiple_update", - "delete": "multiple_delete", - } - ), - name="profiles.batch", - ), - url( - r"^api/v2/token/%s/$" % PVAR_TOKEN, - views.ProfileViewSet.as_view( - { - "get": "retrieve_by_token", - } - ), - name="profiles.retrieve_by_token", - ), - url( - r"^api/v2/profiles/%s/modify_password/$" % PVAR_PROFILE_ID, - views.ProfileViewSet.as_view( - { - "post": "modify_password", - } - ), - name="profiles.modify_password", - ), - ################## - # dynamic fields # - ################## - url( - r"^api/v2/dynamic_fields/$", - views.DynamicFieldsViewSet.as_view( - { - "get": "list", - "post": "create", - } - ), - name="dynamic_fields", - ), - url( - r"^api/v2/dynamic_fields/%s/$" % PVAR_PROFILE_ID, - views.DynamicFieldsViewSet.as_view( - { - "get": "retrieve", - "put": "update", - "patch": "partial_update", - "delete": "destroy", - } - ), - name="dynamic_fields.action", - ), - ######## - # Edge # - ######## - url( - r"^api/v2/edges/leader/$", - views.LeaderEdgeViewSet.as_view({"get": "list"}), - name="edge.leader", - ), -] - -urlpatterns += [ - url( - r"^api/v1/login/check/$", - views.ProfileLoginViewSet.as_view({"post": "login"}), - name="login.check", - ), - url( - r"^api/v1/login/profile/$", - views.ProfileLoginViewSet.as_view({"post": "upsert"}), - name="login.upsert", - ), - url( - r"^api/v1/login/profile/query/$", - views.ProfileLoginViewSet.as_view({"post": "batch_query"}), - name="login.batch_query", - ), -] +urlpatterns = v2_urlpatterns + v3_urlpatterns diff --git a/src/api/bkuser_core/profiles/utils.py b/src/api/bkuser_core/profiles/utils.py index af4dbb194..e21f7e32a 100644 --- a/src/api/bkuser_core/profiles/utils.py +++ b/src/api/bkuser_core/profiles/utils.py @@ -12,6 +12,7 @@ import random import re import string +import urllib.parse from typing import TYPE_CHECKING, Tuple from bkuser_core.categories.models import ProfileCategory @@ -179,10 +180,15 @@ def get_username(force_use_raw: bool, category_id: int, username: str, domain: s def check_former_passwords( instance: "Profile", new_password: str, - max_history: int = settings.MAX_PASSWORD_HISTORY, + max_history: int = settings.DEFAULT_MAX_PASSWORD_HISTORY, ) -> bool: """Check if new password in last passwords""" reset_records = ResetPassword.objects.filter(profile=instance).order_by("-create_time")[:max_history] former_passwords = [x.password for x in reset_records] return new_password in 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}") diff --git a/src/api/bkuser_core/tests/apis/iam/__init__.py b/src/api/bkuser_core/profiles/v2/__init__.py similarity index 100% rename from src/api/bkuser_core/tests/apis/iam/__init__.py rename to src/api/bkuser_core/profiles/v2/__init__.py diff --git a/src/api/bkuser_core/profiles/filters.py b/src/api/bkuser_core/profiles/v2/filters.py similarity index 97% rename from src/api/bkuser_core/profiles/filters.py rename to src/api/bkuser_core/profiles/v2/filters.py index 7f6c4d371..806b2174d 100644 --- a/src/api/bkuser_core/profiles/filters.py +++ b/src/api/bkuser_core/profiles/v2/filters.py @@ -11,8 +11,8 @@ import logging from itertools import chain +from bkuser_core.apis.v2.viewset import AdvancedSearchFilter from bkuser_core.categories.models import ProfileCategory -from bkuser_core.common.viewset import AdvancedSearchFilter from django.db.models import QuerySet logger = logging.getLogger(__name__) diff --git a/src/api/bkuser_core/profiles/serializers.py b/src/api/bkuser_core/profiles/v2/serializers.py similarity index 95% rename from src/api/bkuser_core/profiles/serializers.py rename to src/api/bkuser_core/profiles/v2/serializers.py index 342c080f0..6bcf16dcf 100644 --- a/src/api/bkuser_core/profiles/serializers.py +++ b/src/api/bkuser_core/profiles/v2/serializers.py @@ -8,19 +8,18 @@ an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. """ -from typing import Optional, Union +from typing import Union -from bkuser_core.common.serializers import AdvancedRetrieveSerialzier, CustomFieldsMixin, CustomFieldsModelSerializer +from bkuser_core.apis.v2.serializers import AdvancedRetrieveSerialzier, CustomFieldsMixin, CustomFieldsModelSerializer from bkuser_core.departments.serializers import 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 +from bkuser_core.profiles.validators import validate_domain, validate_username from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers from rest_framework.validators import ValidationError -from .constants import TIME_ZONE_CHOICES, LanguageEnum, RoleCodeEnum -from .models import DynamicFieldInfo, Profile -from .utils import force_use_raw_username, get_username -from .validators import validate_domain, validate_username - # =============================================================================== # Response # =============================================================================== @@ -31,7 +30,7 @@ ########### -def get_extras(extras_from_db: Union[dict, list], defaults: Optional[dict]) -> dict: +def get_extras(extras_from_db: Union[dict, list], defaults: dict) -> dict: if not defaults: defaults = DynamicFieldInfo.objects.get_extras_default_values() diff --git a/src/api/bkuser_core/profiles/v2/urls.py b/src/api/bkuser_core/profiles/v2/urls.py new file mode 100644 index 000000000..7de54c278 --- /dev/null +++ b/src/api/bkuser_core/profiles/v2/urls.py @@ -0,0 +1,158 @@ +# -*- 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 bkuser_core.apis.v2.constants import LOOKUP_FIELD_NAME +from django.conf.urls import url + +from . import views + +PVAR_PROFILE_ID = r"(?P<%s>[\w\-\@\.\$]+)" % LOOKUP_FIELD_NAME +PVAR_TOKEN = r"(?P[\w]+)" + +urlpatterns = [ + url( + r"^api/v2/profiles/$", + views.ProfileViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="profiles", + ), + url( + r"^api/v2/profiles/%s/$" % PVAR_PROFILE_ID, + views.ProfileViewSet.as_view( + { + "get": "retrieve", + "put": "update", + "patch": "partial_update", + "delete": "destroy", + } + ), + name="profiles.action", + ), + url( + r"^api/v2/profiles/%s/restoration/$" % PVAR_PROFILE_ID, + views.ProfileViewSet.as_view( + { + "post": "restoration", + } + ), + name="profiles.restoration", + ), + url( + r"^api/v2/profiles/%s/departments/$" % PVAR_PROFILE_ID, + views.ProfileViewSet.as_view( + { + "get": "get_departments", + } + ), + name="profiles.departments", + ), + url( + r"^api/v2/profiles/%s/leaders/$" % PVAR_PROFILE_ID, + views.ProfileViewSet.as_view( + { + "get": "get_leaders", + } + ), + name="profiles.leaders", + ), + url( + r"^api/v2/profiles/%s/token/$" % PVAR_PROFILE_ID, + views.ProfileViewSet.as_view( + { + "post": "generate_token", + } + ), + name="profiles.generate_token", + ), + url( + r"^api/v2/batch/profiles/$", + views.BatchProfileViewSet.as_view( + { + "get": "multiple_retrieve", + "patch": "multiple_update", + "delete": "multiple_delete", + } + ), + name="profiles.batch", + ), + url( + r"^api/v2/token/%s/$" % PVAR_TOKEN, + views.ProfileViewSet.as_view( + { + "get": "retrieve_by_token", + } + ), + name="profiles.retrieve_by_token", + ), + url( + r"^api/v2/profiles/%s/modify_password/$" % PVAR_PROFILE_ID, + views.ProfileViewSet.as_view( + { + "post": "modify_password", + } + ), + name="profiles.modify_password", + ), + ################## + # dynamic fields # + ################## + url( + r"^api/v2/dynamic_fields/$", + views.DynamicFieldsViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="dynamic_fields", + ), + url( + r"^api/v2/dynamic_fields/%s/$" % PVAR_PROFILE_ID, + views.DynamicFieldsViewSet.as_view( + { + "get": "retrieve", + "put": "update", + "patch": "partial_update", + "delete": "destroy", + } + ), + name="dynamic_fields.action", + ), + ######## + # Edge # + ######## + url( + r"^api/v2/edges/leader/$", + views.LeaderEdgeViewSet.as_view({"get": "list"}), + name="edge.leader", + ), +] + +urlpatterns += [ + url( + r"^api/v1/login/check/$", + views.ProfileLoginViewSet.as_view({"post": "login"}), + name="login.check", + ), + url( + r"^api/v1/login/profile/$", + views.ProfileLoginViewSet.as_view({"post": "upsert"}), + name="login.upsert", + ), + url( + r"^api/v1/login/profile/query/$", + views.ProfileLoginViewSet.as_view({"post": "batch_query"}), + name="login.batch_query", + ), +] diff --git a/src/api/bkuser_core/profiles/views.py b/src/api/bkuser_core/profiles/v2/views.py similarity index 88% rename from src/api/bkuser_core/profiles/views.py rename to src/api/bkuser_core/profiles/v2/views.py index cc922fb3b..1594c1c70 100644 --- a/src/api/bkuser_core/profiles/views.py +++ b/src/api/bkuser_core/profiles/v2/views.py @@ -14,25 +14,25 @@ from collections import defaultdict from operator import or_ +from bkuser_core.apis.v2.constants import LOOKUP_FIELD_NAME, LOOKUP_PARAM +from bkuser_core.apis.v2.serializers import ( + AdvancedListSerializer, + AdvancedRetrieveSerialzier, + BatchRetrieveSerializer, + EmptySerializer, +) +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_profile_log from bkuser_core.categories.constants import CategoryType from bkuser_core.categories.loader import get_plugin_by_category from bkuser_core.categories.models import ProfileCategory +from bkuser_core.categories.signals import post_dynamic_field_delete from bkuser_core.common.cache import clear_cache_if_succeed -from bkuser_core.common.constants import LOOKUP_FIELD_NAME, LOOKUP_PARAM from bkuser_core.common.error_codes import error_codes -from bkuser_core.common.serializers import ( - AdvancedListSerializer, - AdvancedRetrieveSerialzier, - BatchRetrieveSerializer, - EmptySerializer, -) -from bkuser_core.common.viewset import AdvancedBatchOperateViewSet, AdvancedListAPIView, AdvancedModelViewSet from bkuser_core.departments import serializers as department_serializer from bkuser_core.profiles.constants import ProfileStatus from bkuser_core.profiles.exceptions import CountryISOCodeNotMatch, ProfileEmailEmpty -from bkuser_core.profiles.filters import ProfileSearchFilter from bkuser_core.profiles.models import DynamicFieldInfo, LeaderThroughModel, Profile, ProfileTokenHolder from bkuser_core.profiles.password import PasswordValidator from bkuser_core.profiles.signals import post_field_create, post_profile_create, post_profile_update @@ -41,10 +41,13 @@ align_country_iso_code, check_former_passwords, force_use_raw_username, + make_passwd_reset_url_by_token, make_password_by_config, parse_username_domain, ) +from bkuser_core.profiles.v2.filters import ProfileSearchFilter from bkuser_core.profiles.validators import validate_username +from bkuser_core.user_settings.exceptions import SettingHasBeenDisabledError from bkuser_core.user_settings.loader import ConfigProvider from django.conf import settings from django.contrib.auth.hashers import make_password @@ -318,10 +321,14 @@ def _update(self, request, partial): # 密码修改加密 if validated_data.get("password"): pending_password = validated_data.get("password") - if check_former_passwords(instance, pending_password): - raise error_codes.PASSWORD_DUPLICATED - config_loader = ConfigProvider(category_id=instance.category_id) + try: + max_password_history = config_loader.get("max_password_history", settings.DEFAULT_MAX_PASSWORD_HISTORY) + if check_former_passwords(instance, pending_password, int(max_password_history)): + raise error_codes.PASSWORD_DUPLICATED.f(max_password_history=max_password_history) + except SettingHasBeenDisabledError: + logger.info("category<%s> has disabled checking password", instance.category_id) + PasswordValidator( min_length=int(config_loader["password_min_length"]), max_length=settings.PASSWORD_MAX_LENGTH, @@ -396,13 +403,18 @@ def modify_password(self, request, *args, **kwargs): old_password = serializer.validated_data["old_password"] new_password = serializer.validated_data["new_password"] - if check_former_passwords(instance, new_password): - raise error_codes.PASSWORD_DUPLICATED + + config_loader = ConfigProvider(category_id=instance.category_id) + try: + max_password_history = config_loader.get("max_password_history", settings.DEFAULT_MAX_PASSWORD_HISTORY) + if check_former_passwords(instance, new_password, int(max_password_history)): + raise error_codes.PASSWORD_DUPLICATED.f(max_password_history=max_password_history) + except SettingHasBeenDisabledError: + logger.info("category<%s> has disabled checking password", instance.category_id) if not instance.check_password(old_password): raise error_codes.PASSWORD_ERROR - config_loader = ConfigProvider(category_id=instance.category_id) PasswordValidator( min_length=int(config_loader["password_min_length"]), max_length=settings.PASSWORD_MAX_LENGTH, @@ -543,6 +555,7 @@ def login(self, request): raise error_codes.PASSWORD_ERROR time_aware_now = now() + config_loader = ConfigProvider(category_id=category.id) # Admin 用户只需直接判断 密码是否正确 (只有本地目录有密码配置) if not profile.is_superuser and category.type in [CategoryType.LOCAL.value]: @@ -557,7 +570,7 @@ def login(self, request): request=request, params={"is_success": False, "reason": LogInFailReason.DISABLED_USER.value}, ) - raise error_codes.USER_IS_DISABLED + raise error_codes.PASSWORD_ERROR elif profile.status == ProfileStatus.LOCKED.value: create_profile_log( profile=profile, @@ -565,10 +578,9 @@ def login(self, request): request=request, params={"is_success": False, "reason": LogInFailReason.LOCKED_USER.value}, ) - raise error_codes.USER_IS_LOCKED + raise error_codes.PASSWORD_ERROR # 获取密码配置 - config_loader = ConfigProvider(category_id=category.id) auto_unlock_seconds = int(config_loader["auto_unlock_seconds"]) max_trail_times = int(config_loader["max_trail_times"]) @@ -584,7 +596,10 @@ def login(self, request): request=request, params={"is_success": False, "reason": LogInFailReason.TOO_MANY_FAILURE.value}, ) - raise error_codes.TOO_MANY_TRY.f(f"请 {retry_after_wait}s 后再试") + + logger.info(f"用户<{profile}> 登录失败错误过多,已被锁定,请 {retry_after_wait}s 后再试") + # 当密码输入错误时,不暴露不同的信息,避免用户名爆破 + raise error_codes.PASSWORD_ERROR try: login_class = get_plugin_by_category(category).login_handler_cls @@ -595,42 +610,75 @@ def login(self, request): category.display_name, category.id, ) - raise error_codes.LOAD_LOGIN_HANDLER_FAILED + raise error_codes.PASSWORD_ERROR try: login_class().check(profile, password) + except Exception: create_profile_log( profile=profile, operation="LogIn", request=request, - params={"is_success": True}, + params={"is_success": False, "reason": LogInFailReason.BAD_PASSWORD.value}, ) - except Exception: + logger.exception("check profile<%s> failed", profile.username) + raise error_codes.PASSWORD_ERROR + + self._check_password_status(request, profile, config_loader, time_aware_now) + + create_profile_log(profile=profile, operation="LogIn", request=request, params={"is_success": True}) + return Response(data=local_serializers.ProfileSerializer(profile, context={"request": request}).data) + + def _check_password_status( + self, request, profile: Profile, config_loader: ConfigProvider, time_aware_now: datetime.datetime + ): + """当密码校验成功后,检查用户密码状态""" + # 密码状态校验:初始密码未修改 + # 暂时跳过判断 admin,考虑在 login 模块未升级替换时,admin 可以在 SaaS 配置中关掉该特性 + if ( + not profile.is_superuser + and config_loader.get("force_reset_first_login") + and profile.password_update_time is None + ): create_profile_log( profile=profile, operation="LogIn", request=request, - params={"is_success": False, "reason": LogInFailReason.BAD_PASSWORD.value}, + params={"is_success": False, "reason": LogInFailReason.SHOULD_CHANGE_INITIAL_PASSWORD.value}, ) - logger.exception("check profile<%s> failed", profile.username) - raise error_codes.PASSWORD_ERROR + + raise error_codes.SHOULD_CHANGE_INITIAL_PASSWORD.format( + data=self._generate_reset_passwd_url_with_token(profile) + ) + + # 密码状态校验:密码过期 + valid_period = datetime.timedelta(days=profile.password_valid_days) + if ( + profile.password_valid_days > 0 + and ((profile.password_update_time or profile.latest_password_update_time) + valid_period) < time_aware_now + ): + create_profile_log( + profile=profile, + operation="LogIn", + request=request, + params={"is_success": False, "reason": LogInFailReason.EXPIRED_PASSWORD.value}, + ) + + raise error_codes.PASSWORD_EXPIRED.format(data=self._generate_reset_passwd_url_with_token(profile)) + + @staticmethod + def _generate_reset_passwd_url_with_token(profile: Profile) -> dict: + data = {} + try: + token_holder = ProfileTokenHolder.objects.create( + profile=profile, token_expire_seconds=settings.PAGE_TOKEN_EXPIRE_SECONDS + ) + except Exception: # pylint: disable=broad-except + logger.exception("failed to create token for password reset") else: - # 密码状态校验:密码过期 - valid_period = datetime.timedelta(days=profile.password_valid_days) - if ( - profile.password_valid_days > 0 - and ((profile.password_update_time or profile.latest_password_update_time) + valid_period) - < time_aware_now - ): - create_profile_log( - profile=profile, - operation="LogIn", - request=request, - params={"is_success": False, "reason": LogInFailReason.EXPIRED_PASSWORD.value}, - ) - raise error_codes.PASSWORD_EXPIRED + data.update({"reset_password_url": make_passwd_reset_url_by_token(token_holder.token)}) - return Response(data=local_serializers.ProfileSerializer(profile, context={"request": request}).data) + return data @method_decorator(clear_cache_if_succeed) @swagger_auto_schema(request_body=local_serializers.LoginUpsertSerializer) @@ -790,10 +838,10 @@ def destroy(self, request, *args, **kwargs): # 内置字段不允许删除 if instance.builtin: raise error_codes.BUILTIN_FIELD_CANNOT_BE_DELETED - # 保证 order 密集 DynamicFieldInfo.objects.filter(order__gt=instance.order).update(order=F("order") - 1) + post_dynamic_field_delete.send(sender=self, instance=instance, operator=request.operator) return super().destroy(request, *args, **kwargs) diff --git a/src/api/bkuser_core/tests/apis/monitoring/__init__.py b/src/api/bkuser_core/profiles/v3/__init__.py similarity index 100% rename from src/api/bkuser_core/tests/apis/monitoring/__init__.py rename to src/api/bkuser_core/profiles/v3/__init__.py diff --git a/src/api/bkuser_core/profiles/v3/filters.py b/src/api/bkuser_core/profiles/v3/filters.py new file mode 100644 index 000000000..ca28e5caa --- /dev/null +++ b/src/api/bkuser_core/profiles/v3/filters.py @@ -0,0 +1,27 @@ +""" +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 django.db.models import ManyToManyField, ManyToManyRel, ManyToOneRel +from rest_framework import filters + + +class MultipleFieldFilter(filters.SearchFilter): + """多字段过滤器, 同时支持标准和非标准过滤""" + + def filter_by_params(self, params: dict, queryset, view): + """非标准 filter""" + plain_fields = [ + f.name + for f in queryset.model._meta.get_fields() + if not isinstance(f, (ManyToOneRel, ManyToManyRel, ManyToManyField)) + ] + plain_query_params = {key: value for key, value in params.items() if key in plain_fields} + m2m_query_params = {f"{key}__in": value for key, value in params.items() if key in view.supported_m2m_fields} + # in operator on many-to-many fields may cause duplicate results, so we use distinct + return queryset.filter(**m2m_query_params, **plain_query_params).distinct() diff --git a/src/api/bkuser_core/profiles/v3/serializers.py b/src/api/bkuser_core/profiles/v3/serializers.py new file mode 100644 index 000000000..c32ec7cee --- /dev/null +++ b/src/api/bkuser_core/profiles/v3/serializers.py @@ -0,0 +1,70 @@ +# -*- 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 bkuser_core.apis.v3.serializers import StringArrayField +from bkuser_core.departments.serializers import SimpleDepartmentSerializer +from bkuser_core.profiles.v2.serializers import LeaderSerializer +from rest_framework.fields import BooleanField, CharField, IntegerField, JSONField +from rest_framework.serializers import Serializer + + +class ProfileSerializer(Serializer): + """列出用户的 profile""" + + id = CharField(required=False, help_text="用户ID") + username = CharField(required=False, help_text="用户名") + qq = CharField(required=False, help_text="QQ") + email = CharField(required=False, help_text="邮箱") + telephone = CharField(required=False, help_text="电话") + wx_userid = CharField(required=False, help_text="微信用户id") + domain = CharField(required=False, help_text="域") + display_name = CharField(required=False, help_text="中文名") + status = CharField(required=False, help_text="账户状态") + staff_status = CharField(required=False, help_text="在职状态") + position = CharField(required=False, help_text="职位") + enabled = BooleanField(required=False, help_text="是否启用", default=True) + extras = JSONField(required=False, help_text="扩展字段") + + +# ------------ +# Request +# ------------ +class QueryProfileSerializer(ProfileSerializer): + ordering = CharField(required=False, help_text="排序字段", default="id") + cursor = CharField(required=False, help_text="游标") + # 暂不支持 fields 限制返回字段 + # fields = StringArrayField(required=False, help_text="返回字段") + departments = StringArrayField(required=False, help_text="部门id列表") + leaders = StringArrayField(required=False, help_text="上级id列表") + + def to_internal_value(self, data): + data = super().to_internal_value(data) + + if "leaders" in data: + data["leader"] = data.pop("leaders") + + return data + + +# ------------ +# Response +# ------------ +class ResultProfileSerializer(ProfileSerializer): + """返回用户 profile""" + + departments = SimpleDepartmentSerializer(many=True, required=False, help_text="部门列表") + leaders = LeaderSerializer(many=True, required=False, help_text="上级列表", source="leader") + + +class PaginatedProfileSerializer(Serializer): + count = IntegerField(required=False, help_text="总数") + next = CharField(required=False, help_text="下一页游标") + previous = CharField(required=False, help_text="上一页游标") + results = ResultProfileSerializer(many=True, help_text="结果") diff --git a/src/api/bkuser_core/profiles/v3/urls.py b/src/api/bkuser_core/profiles/v3/urls.py new file mode 100644 index 000000000..898443ec4 --- /dev/null +++ b/src/api/bkuser_core/profiles/v3/urls.py @@ -0,0 +1,25 @@ +# -*- 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 django.conf.urls import url + +from . import views + +urlpatterns = [ + url( + r"^api/v3/profiles/$", + views.ProfileViewSet.as_view( + { + "get": "list", + } + ), + name="profiles.list", + ), +] diff --git a/src/api/bkuser_core/profiles/v3/views.py b/src/api/bkuser_core/profiles/v3/views.py new file mode 100644 index 000000000..e412eea8d --- /dev/null +++ b/src/api/bkuser_core/profiles/v3/views.py @@ -0,0 +1,54 @@ +# -*- 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 logging + +from bkuser_core.apis.v3.serializers import AdvancedPagination +from bkuser_core.bkiam.exceptions import IAMPermissionDenied +from bkuser_core.bkiam.permissions import IAMPermission +from bkuser_core.common.error_codes import error_codes +from bkuser_core.profiles.models import Profile +from bkuser_core.profiles.v3.filters import MultipleFieldFilter +from bkuser_core.profiles.v3.serializers import PaginatedProfileSerializer, QueryProfileSerializer +from rest_framework import filters, viewsets +from rest_framework.generics import ListAPIView + +from bkuser_global.drf_crown.crown import inject_serializer + +logger = logging.getLogger(__name__) + + +class ProfileViewSet(viewsets.ModelViewSet, ListAPIView): + """获取用户数据""" + + queryset = Profile.objects.all() + permission_classes = [IAMPermission] + filter_backends = [filters.OrderingFilter] + pagination_class = AdvancedPagination + ordering = "id" + + supported_m2m_fields = ["leader", "departments"] + + @inject_serializer(query_in=QueryProfileSerializer, out=PaginatedProfileSerializer) + def list(self, request, validated_data: dict, *args, **kwargs): + """获取用户列表""" + self.check_permissions(request) + + try: + queryset = MultipleFieldFilter().filter_by_params( + validated_data, self.filter_queryset(self.get_queryset()), self + ) + except IAMPermissionDenied: + raise + except Exception: + logger.exception("failed to get profile list") + raise error_codes.QUERY_PARAMS_ERROR + + return self.get_paginated_response(self.paginate_queryset(queryset)) diff --git a/src/api/bkuser_core/profiles/validators.py b/src/api/bkuser_core/profiles/validators.py index 88f375217..d66fbebe2 100644 --- a/src/api/bkuser_core/profiles/validators.py +++ b/src/api/bkuser_core/profiles/validators.py @@ -8,11 +8,15 @@ 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 re +from typing import Any, ClassVar, Dict, Tuple, Type from bkuser_core.profiles.constants import DynamicFieldTypeEnum from django.utils.translation import gettext_lazy as _ from rest_framework.exceptions import ValidationError +from typing_extensions import Protocol USERNAME_REGEX = r"^(\d|[a-zA-Z])([a-zA-Z0-9._-]){0,31}" DOMAIN_REGEX = r"^(\d|[a-zA-Z])([a-zA-Z0-9-.]){0,15}" @@ -67,6 +71,151 @@ def validate_extras_value_unique(value: dict, category_id: int, profile_id: int ) +class ExtrasValidator(Protocol): + """自定义字段格式校验""" + + target_types: ClassVar[Tuple] + transform_types: ClassVar[Tuple] + + @classmethod + def validate(cls, value: Any, field_info): + raise NotImplementedError + + +class ExtrasNumberValidator: + target_types: ClassVar[Tuple] = (int, float) + transform_types: ClassVar[Tuple] = (str,) + + @classmethod + def validate(cls, value: Any, field_info): + if isinstance(value, cls.target_types): + return + + if isinstance(value, cls.transform_types): + try: + value = cls.transform(value) + return value + except Exception: + raise ValidationError(_("{}不符合格式要求,无法转换".format(value))) + + @classmethod + def transform(cls, value): + """格式转换""" + return float(value) + + +class ExtrasStringValidator: + target_types: ClassVar[Tuple] = (str,) + transform_types: ClassVar[Tuple] = () + + @classmethod + def validate(cls, value: Any, field_info): + if isinstance(value, cls.target_types): + return + + try: + value = cls.transform(value) + return value + except Exception: + raise ValidationError(_("{}不符合格式要求,无法转换".format(value))) + + @classmethod + def transform(cls, value): + """格式转换""" + return str(value) + + +class ExtrasOneEnumValidator: + target_types: ClassVar[Tuple] = (int,) + transform_types: ClassVar[Tuple] = (str,) + + @classmethod + def validate(cls, value: Any, field_info): + enums = [enum[0] for enum in field_info.options] + if isinstance(value, cls.target_types) and (value in enums): + return + + if isinstance(value, cls.transform_types): + try: + value = cls.transform(value) + except Exception: + raise ValidationError(_("{}不符合格式要求,无法转换".format(value))) + + if value in enums: + return value + raise ValidationError(_("{}不在枚举范围内".format(value))) + + @classmethod + def transform(cls, value): + """格式转换""" + return int(value) + + +class ExtrasMultiEnumValidator: + target_types: ClassVar[Tuple] = (list,) + transform_types: ClassVar[Tuple] = (str, set, tuple) + + @classmethod + def validate(cls, value: Any, field_info): + enums = [enum[0] for enum in field_info.options] + if isinstance(value, cls.target_types) and (set(value) <= set(enums)): + return + if isinstance(value, cls.transform_types): + try: + value = cls.transform(value) + except Exception: + raise ValidationError(_("{} 不符合格式要求,无法转换".format(value))) + + if set(value) <= set(enums): + return value + raise ValidationError(_("{} 不在枚举范围内".format(value))) + + @classmethod + def transform(cls, value): + return list(value) + + +class ExtrasTimerValidator: + target_types: ClassVar[Tuple] = (str,) + transform_types: ClassVar[Tuple] = () + + @classmethod + def validate(cls, value: Any, field_info): + if isinstance(value, cls.target_types): + try: + datetime.datetime.strptime(value, "%Y-%m-%d") + return + except Exception: + raise ValidationError(_("{} 不符合格式要求".format(value))) + raise ValidationError(_("{} 不符合格式要求".format(value))) + + +EXTRAS_VALIDATOR_MAP: Dict[DynamicFieldTypeEnum, Type[ExtrasValidator]] = { + DynamicFieldTypeEnum.NUMBER.value: ExtrasNumberValidator, + DynamicFieldTypeEnum.STRING.value: ExtrasStringValidator, + DynamicFieldTypeEnum.ONE_ENUM.value: ExtrasOneEnumValidator, + DynamicFieldTypeEnum.MULTI_ENUM.value: ExtrasMultiEnumValidator, + DynamicFieldTypeEnum.TIMER.value: ExtrasTimerValidator, +} + + +def validate_extras_value_type(value: dict): + """检测 extras 中 value 是否自定义字段规定的格式是否一致:不一致尝试进行转换""" + from bkuser_core.profiles.models import DynamicFieldInfo + + dynamic_fields = DynamicFieldInfo.objects.filter(name__in=value.keys()) + for field in dynamic_fields: + logging.info("going format dynamic field:{}, origin value:{}".format(field.name, value[field.name])) + + try: + EXTRAS_VALIDATOR_MAP[field.type].validate(value=value[field.name], field_info=field) + except Exception: + logging.info("fail to format dynamic field:{}".format(field.name)) + value[field.name] = "" + + return value + + BLACK_FIELD_NAMES = ["extras"] diff --git a/src/api/bkuser_core/tests/apis/profiles/__init__.py b/src/api/bkuser_core/tests/apis/v2/__init__.py similarity index 100% rename from src/api/bkuser_core/tests/apis/profiles/__init__.py rename to src/api/bkuser_core/tests/apis/v2/__init__.py diff --git a/src/api/bkuser_core/tests/apis/user_settings/__init__.py b/src/api/bkuser_core/tests/apis/v2/audits/__init__.py similarity index 100% rename from src/api/bkuser_core/tests/apis/user_settings/__init__.py rename to src/api/bkuser_core/tests/apis/v2/audits/__init__.py diff --git a/src/api/bkuser_core/tests/apis/audits/test_audits.py b/src/api/bkuser_core/tests/apis/v2/audits/test_audits.py similarity index 100% rename from src/api/bkuser_core/tests/apis/audits/test_audits.py rename to src/api/bkuser_core/tests/apis/v2/audits/test_audits.py diff --git a/src/api/bkuser_core/tests/apis/profiles/test_dynamic_fields.py b/src/api/bkuser_core/tests/apis/v2/categories/__init__.py similarity index 100% rename from src/api/bkuser_core/tests/apis/profiles/test_dynamic_fields.py rename to src/api/bkuser_core/tests/apis/v2/categories/__init__.py diff --git a/src/api/bkuser_core/tests/apis/categories/test_categories.py b/src/api/bkuser_core/tests/apis/v2/categories/test_categories.py similarity index 100% rename from src/api/bkuser_core/tests/apis/categories/test_categories.py rename to src/api/bkuser_core/tests/apis/v2/categories/test_categories.py diff --git a/src/api/bkuser_core/tests/categories/plugins/tof/mock.py b/src/api/bkuser_core/tests/apis/v2/departments/__init__.py similarity index 100% rename from src/api/bkuser_core/tests/categories/plugins/tof/mock.py rename to src/api/bkuser_core/tests/apis/v2/departments/__init__.py diff --git a/src/api/bkuser_core/tests/apis/departments/test_departments.py b/src/api/bkuser_core/tests/apis/v2/departments/test_departments.py similarity index 100% rename from src/api/bkuser_core/tests/apis/departments/test_departments.py rename to src/api/bkuser_core/tests/apis/v2/departments/test_departments.py diff --git a/src/api/bkuser_core/tests/categories/plugins/tof/test_fetch_data.py b/src/api/bkuser_core/tests/apis/v2/iam/__init__.py similarity index 100% rename from src/api/bkuser_core/tests/categories/plugins/tof/test_fetch_data.py rename to src/api/bkuser_core/tests/apis/v2/iam/__init__.py diff --git a/src/api/bkuser_core/tests/apis/iam/test_department.py b/src/api/bkuser_core/tests/apis/v2/iam/test_department.py similarity index 100% rename from src/api/bkuser_core/tests/apis/iam/test_department.py rename to src/api/bkuser_core/tests/apis/v2/iam/test_department.py diff --git a/src/api/bkuser_core/tests/apis/iam/test_field.py b/src/api/bkuser_core/tests/apis/v2/iam/test_field.py similarity index 100% rename from src/api/bkuser_core/tests/apis/iam/test_field.py rename to src/api/bkuser_core/tests/apis/v2/iam/test_field.py diff --git a/src/api/bkuser_core/tests/categories/plugins/tof/test_syncer.py b/src/api/bkuser_core/tests/apis/v2/monitoring/__init__.py similarity index 100% rename from src/api/bkuser_core/tests/categories/plugins/tof/test_syncer.py rename to src/api/bkuser_core/tests/apis/v2/monitoring/__init__.py diff --git a/src/api/bkuser_core/tests/apis/monitoring/test_healthz.py b/src/api/bkuser_core/tests/apis/v2/monitoring/test_healthz.py similarity index 100% rename from src/api/bkuser_core/tests/apis/monitoring/test_healthz.py rename to src/api/bkuser_core/tests/apis/v2/monitoring/test_healthz.py diff --git a/src/api/bkuser_core/tests/apis/v2/profiles/__init__.py b/src/api/bkuser_core/tests/apis/v2/profiles/__init__.py new file mode 100644 index 000000000..1060b7bf4 --- /dev/null +++ b/src/api/bkuser_core/tests/apis/v2/profiles/__init__.py @@ -0,0 +1,10 @@ +# -*- 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. +""" diff --git a/src/api/bkuser_core/tests/apis/v2/profiles/test_dynamic_fields.py b/src/api/bkuser_core/tests/apis/v2/profiles/test_dynamic_fields.py new file mode 100644 index 000000000..1060b7bf4 --- /dev/null +++ b/src/api/bkuser_core/tests/apis/v2/profiles/test_dynamic_fields.py @@ -0,0 +1,10 @@ +# -*- 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. +""" diff --git a/src/api/bkuser_core/tests/apis/profiles/test_login.py b/src/api/bkuser_core/tests/apis/v2/profiles/test_login.py similarity index 86% rename from src/api/bkuser_core/tests/apis/profiles/test_login.py rename to src/api/bkuser_core/tests/apis/v2/profiles/test_login.py index 3ff336314..d846683d0 100644 --- a/src/api/bkuser_core/tests/apis/profiles/test_login.py +++ b/src/api/bkuser_core/tests/apis/v2/profiles/test_login.py @@ -14,7 +14,7 @@ import pytest from bkuser_core.categories.constants import CategoryStatus from bkuser_core.profiles.constants import ProfileStatus, RoleCodeEnum -from bkuser_core.profiles.views import ProfileLoginViewSet +from bkuser_core.profiles.v2.views import ProfileLoginViewSet from bkuser_core.tests.apis.utils import get_api_factory from bkuser_core.tests.utils import make_simple_category, make_simple_profile from bkuser_core.user_settings.models import Setting @@ -61,7 +61,7 @@ def test_check(self, factory, check_view): """测试登录校验""" make_simple_profile( username="logintest", - force_create_params={"password": make_password("testpwd")}, + force_create_params={"password": make_password("testpwd"), "password_update_time": now()}, ) request = factory.post("/api/v1/login/check/", data={"username": "logintest", "password": "testpwd"}) response = check_view(request=request) @@ -82,7 +82,12 @@ def test_other_field_check(self, factory, check_view): """测试使用其他字段登录""" make_simple_profile( username="logintest", - force_create_params={"password": make_password("testpwd"), "email": "haha@haha", "telephone": "12345"}, + force_create_params={ + "password": make_password("testpwd"), + "email": "haha@haha", + "telephone": "12345", + "password_update_time": now(), + }, ) request = factory.post("/api/v1/login/check/", data={"username": "logintest", "password": "testpwd"}) response = check_view(request=request) @@ -106,11 +111,21 @@ def test_other_field_duplicate(self, factory, check_view): """测试使用其他字段登录重复问题""" make_simple_profile( username="logintest", - force_create_params={"password": make_password("testpwd"), "email": "haha@haha", "telephone": "12345"}, + force_create_params={ + "password": make_password("testpwd"), + "email": "haha@haha", + "telephone": "12345", + "password_update_time": now(), + }, ) make_simple_profile( username="logintest1", - force_create_params={"password": make_password("testpwd"), "email": "haha@haha", "telephone": "12345"}, + force_create_params={ + "password": make_password("testpwd"), + "email": "haha@haha", + "telephone": "12345", + "password_update_time": now(), + }, ) # 实际上是这些字段重复了,但是会模糊错误返回 request = factory.post("/api/v1/login/check/", data={"username": "12345", "password": "testpwd"}) @@ -127,7 +142,12 @@ def test_multiple_domain_check(self, factory, check_view): ca.make_default_settings() make_simple_profile( username="logintest", - force_create_params={"password": make_password("testpwd"), "domain": "testdomain", "category_id": ca.id}, + force_create_params={ + "password": make_password("testpwd"), + "domain": "testdomain", + "category_id": ca.id, + "password_update_time": now(), + }, ) request = factory.post("/api/v1/login/check/", data={"username": "logintest", "password": "testpwd"}) response = check_view(request=request) @@ -149,7 +169,12 @@ def test_check_error(self, factory, check_view): ca.make_default_settings() p = make_simple_profile( username="logintest", - force_create_params={"password": make_password("testpwd"), "domain": "testdomain", "category_id": ca.id}, + force_create_params={ + "password": make_password("testpwd"), + "domain": "testdomain", + "category_id": ca.id, + "password_update_time": now(), + }, ) # 未知登录域 @@ -187,7 +212,7 @@ def test_check_error(self, factory, check_view): data={"username": "logintest", "password": "testpwd", "domain": "testdomain"}, ) response = check_view(request=request) - assert response.data["code"] == "USER_IS_LOCKED" + assert response.data["code"] == "PASSWORD_ERROR" # 用户被禁用 p.status = ProfileStatus.DISABLED.value @@ -197,7 +222,7 @@ def test_check_error(self, factory, check_view): data={"username": "logintest", "password": "testpwd", "domain": "testdomain"}, ) response = check_view(request=request) - assert response.data["code"] == "USER_IS_DISABLED" + assert response.data["code"] == "PASSWORD_ERROR" # 超级用户不判断用户状态 p.role = RoleCodeEnum.SUPERUSER.value @@ -213,7 +238,7 @@ def test_check_error(self, factory, check_view): p.save() # 用户密码过期 - p.create_time = now() - datetime.timedelta(days=3 * 365) + p.password_update_time = now() - datetime.timedelta(days=3 * 365) p.password_valid_days = 1 p.status = ProfileStatus.NORMAL.value p.save() @@ -231,11 +256,21 @@ def test_check_error(self, factory, check_view): response = check_view(request=request) assert response.data["code"] == "PASSWORD_EXPIRED" + # 初始化密码需要修改 + p.password_update_time = None + p.save() + request = factory.post( + "/api/v1/login/check/", + data={"username": "logintest", "password": "testpwd", "domain": "testdomain"}, + ) + response = check_view(request=request) + assert response.data["code"] == "SHOULD_CHANGE_INITIAL_PASSWORD" + def test_check_auto_lock(self, factory, check_view): """测试多次错误自动锁定""" make_simple_profile( username="logintest", - force_create_params={"password": make_password("testpwd")}, + force_create_params={"password": make_password("testpwd"), "password_update_time": now()}, ) auto_unlock_seconds = Setting.objects.get(category__id=1, meta__key="auto_unlock_seconds") @@ -252,7 +287,7 @@ def test_check_auto_lock(self, factory, check_view): request = factory.post("/api/v1/login/check/", data={"username": "logintest", "password": "wrongpwd"}) response = check_view(request=request) - assert response.data["code"] == "TOO_MANY_TRY" + assert response.data["code"] == "PASSWORD_ERROR" # 确保解锁了 time.sleep(2) diff --git a/src/api/bkuser_core/tests/apis/profiles/test_profiles_action.py b/src/api/bkuser_core/tests/apis/v2/profiles/test_profiles_action.py similarity index 99% rename from src/api/bkuser_core/tests/apis/profiles/test_profiles_action.py rename to src/api/bkuser_core/tests/apis/v2/profiles/test_profiles_action.py index dd9b72ad5..86a768d95 100644 --- a/src/api/bkuser_core/tests/apis/profiles/test_profiles_action.py +++ b/src/api/bkuser_core/tests/apis/v2/profiles/test_profiles_action.py @@ -12,7 +12,7 @@ from bkuser_core.audit.utils import create_profile_log from bkuser_core.profiles.constants import ProfileStatus from bkuser_core.profiles.models import Profile -from bkuser_core.profiles.views import ProfileViewSet +from bkuser_core.profiles.v2.views import ProfileViewSet from bkuser_core.tests.apis.utils import get_api_factory from bkuser_core.tests.utils import get_one_object, make_simple_department, make_simple_profile diff --git a/src/api/bkuser_core/tests/apis/profiles/test_profiles_list.py b/src/api/bkuser_core/tests/apis/v2/profiles/test_profiles_list.py similarity index 99% rename from src/api/bkuser_core/tests/apis/profiles/test_profiles_list.py rename to src/api/bkuser_core/tests/apis/v2/profiles/test_profiles_list.py index 66bfd910f..d1f962c40 100644 --- a/src/api/bkuser_core/tests/apis/profiles/test_profiles_list.py +++ b/src/api/bkuser_core/tests/apis/v2/profiles/test_profiles_list.py @@ -11,7 +11,7 @@ from unittest.mock import patch import pytest -from bkuser_core.profiles.views import ProfileViewSet +from bkuser_core.profiles.v2.views import ProfileViewSet from bkuser_core.tests.apis.utils import get_api_factory from bkuser_core.tests.utils import make_simple_category, make_simple_dynamic_field, make_simple_profile from bkuser_core.user_settings.models import Setting diff --git a/src/api/bkuser_core/tests/apis/v2/user_settings/__init__.py b/src/api/bkuser_core/tests/apis/v2/user_settings/__init__.py new file mode 100644 index 000000000..1060b7bf4 --- /dev/null +++ b/src/api/bkuser_core/tests/apis/v2/user_settings/__init__.py @@ -0,0 +1,10 @@ +# -*- 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. +""" diff --git a/src/api/bkuser_core/tests/apis/user_settings/test_settings_list.py b/src/api/bkuser_core/tests/apis/v2/user_settings/test_settings_list.py similarity index 100% rename from src/api/bkuser_core/tests/apis/user_settings/test_settings_list.py rename to src/api/bkuser_core/tests/apis/v2/user_settings/test_settings_list.py diff --git a/src/api/bkuser_core/tests/apis/v3/__init__.py b/src/api/bkuser_core/tests/apis/v3/__init__.py new file mode 100644 index 000000000..1060b7bf4 --- /dev/null +++ b/src/api/bkuser_core/tests/apis/v3/__init__.py @@ -0,0 +1,10 @@ +# -*- 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. +""" diff --git a/src/api/bkuser_core/tests/apis/v3/profiles/__init__.py b/src/api/bkuser_core/tests/apis/v3/profiles/__init__.py new file mode 100644 index 000000000..1060b7bf4 --- /dev/null +++ b/src/api/bkuser_core/tests/apis/v3/profiles/__init__.py @@ -0,0 +1,10 @@ +# -*- 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. +""" diff --git a/src/api/bkuser_core/tests/apis/v3/profiles/test_list.py b/src/api/bkuser_core/tests/apis/v3/profiles/test_list.py new file mode 100644 index 000000000..d04fdb7c0 --- /dev/null +++ b/src/api/bkuser_core/tests/apis/v3/profiles/test_list.py @@ -0,0 +1,104 @@ +# -*- 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 pytest +from bkuser_core.departments.models import Department +from bkuser_core.profiles.v3.views import ProfileViewSet +from bkuser_core.tests.utils import make_simple_department, make_simple_profile + +pytestmark = pytest.mark.django_db + + +class TestListApis: + @pytest.fixture(scope="class") + def view(self): + return ProfileViewSet.as_view({"get": "list"}) + + # --------------- List --------------- + def test_profile_list(self, factory, view): + """测试正常用户列表返回""" + request = factory.get("/api/v3/profiles/") + response = view(request=request) + data = response.data["results"] + assert len(data) == 1 + assert data[0]["username"] == "admin" + + @pytest.mark.parametrize( + "samples,params,expected", + [ + ( + { + "user-a-1": {"category_id": 1, "telephone": 123456}, + "user-b-1": {"category_id": 2}, + "user-a-2": {"category_id": 1}, + }, + "category_id=1&telephone=123456", + "user-a-1", + ), + ( + { + "user-a-1": {"qq": "aaaa", "status": "NORMAL"}, + "user-b-1": {"qq": "aaaa", "status": "DELETED"}, + "user-a-2": {"qq": "bbbb"}, + }, + "qq=aaaa&status=NORMAL", + "user-a-1", + ), + ], + ) + def test_multiple_fields(self, factory, view, samples, params, expected): + """测试多字段过滤""" + for k, v in samples.items(): + make_simple_profile(k, force_create_params=v) + + url = f"/api/v2/profiles/?{params}" + request = factory.get(url) + response = view(request=request) + + assert ",".join([r["username"] for r in response.data["results"]]) == expected + + @pytest.mark.parametrize( + "samples,query_params,expected", + [ + ( + { + "user-a-1": {"telephone": 123456, "departments": [10001]}, + "user-b-1": {"departments": [10001, 10002]}, + "user-a-2": {"departments": [10003]}, + }, + "departments=10001,10002", + "user-a-1,user-b-1", + ), + ( + { + "user-a-1": {"telephone": 123456, "departments": [10001]}, + "user-b-1": {"departments": [10001, 10002]}, + "user-a-2": {"departments": [10003]}, + }, + "departments=10001,10002&telephone=123456", + "user-a-1", + ), + ], + ) + def test_m2m_field(self, factory, view, samples, query_params, expected): + """测试多对多字段过滤""" + for case in range(0, 5): + make_simple_department(f"department-{case}", force_create_params={"id": case + 10000}) + + for username, params in samples.items(): + departments = params.pop("departments", []) + p = make_simple_profile(username, force_create_params=params) + for d in Department.objects.filter(id__in=departments): + d.add_profile(p) + + url = f"/api/v2/profiles/?{query_params}" + request = factory.get(url) + response = view(request=request) + assert ",".join([r["username"] for r in response.data["results"]]) == expected diff --git a/src/api/bkuser_core/tests/audit/test_managers.py b/src/api/bkuser_core/tests/audit/test_managers.py new file mode 100644 index 000000000..ed3efe829 --- /dev/null +++ b/src/api/bkuser_core/tests/audit/test_managers.py @@ -0,0 +1,90 @@ +import datetime + +import pytest +from bkuser_core.audit.constants import LogInFailReason +from bkuser_core.audit.models import LogIn +from django.utils.timezone import now + +pytestmark = pytest.mark.django_db + + +class TestLoginManager: + @pytest.mark.parametrize( + "records,count", + [ + ( + ( + # latest success + (60 * 60 * 24 * 29, True), + (60 * 60 * 24 * 27, False), + (60 * 60 * 24 * 26, False), + (60 * 60 * 24 * 25, False), + ), + 3, + ), + ( + ( + # too far + (60 * 60 * 24 * 31, True), + # latest success + (60 * 60 * 24 * 29, True), + (60 * 60 * 24 * 27, False), + (60 * 60 * 24 * 26, False), + (60 * 60 * 24 * 25, False), + ), + 3, + ), + ( + ( + # too far + (60 * 60 * 24 * 32, False), + (60 * 60 * 24 * 31, True), + # latest success + (60 * 60 * 24 * 29, True), + (60 * 60 * 24 * 27, False), + (60 * 60 * 24 * 26, False), + ), + 2, + ), + ( + ( + # too far + (60 * 60 * 24 * 32, True), + (60 * 60 * 24 * 31, True), + # no success + (60 * 60 * 24 * 29, False), + (60 * 60 * 24 * 27, False), + (60 * 60 * 24 * 26, False), + (60 * 60 * 24 * 26, False), + ), + 4, + ), + ( + ( + # too far + (60 * 60 * 24 * 32, True), + (60 * 60 * 24 * 31, True), + (60 * 60 * 24 * 31, False), + # count start from below + (60 * 60 * 24 * 27, False), + (60 * 60 * 24 * 26, False), + (60 * 60 * 24 * 26, False), + ), + 3, + ), + ], + ) + def test_latest_failed_count(self, records, count): + """测试最近登录失败次数""" + + now_time = now() + for record in records: + _l = LogIn.objects.create( + profile_id=1, + is_success=record[1], + reason=LogInFailReason.BAD_PASSWORD.value, + ) + _l.create_time = now_time - datetime.timedelta(seconds=record[0]) + _l.save() + + assert LogIn.objects.latest_failed_count() == count diff --git a/src/api/bkuser_core/tests/categories/plugins/conftest.py b/src/api/bkuser_core/tests/categories/plugins/conftest.py index 57a466b99..bd3ace51c 100644 --- a/src/api/bkuser_core/tests/categories/plugins/conftest.py +++ b/src/api/bkuser_core/tests/categories/plugins/conftest.py @@ -11,7 +11,6 @@ import pytest from bkuser_core.categories.plugins.base import DBSyncManager, SyncContext from bkuser_core.categories.plugins.ldap.adaptor import ProfileFieldMapper -from bkuser_core.categories.plugins.ldap.syncer import SETTING_FIELD_MAP @pytest.fixture() @@ -44,7 +43,7 @@ def ldap_config(): @pytest.fixture() def profile_field_mapper(ldap_config): - return ProfileFieldMapper(config_loader=ldap_config, setting_field_map=SETTING_FIELD_MAP) + return ProfileFieldMapper(config_loader=ldap_config) @pytest.fixture diff --git a/src/api/bkuser_core/tests/categories/plugins/ldap/test_adaptor.py b/src/api/bkuser_core/tests/categories/plugins/ldap/test_adaptor.py index 0cae5c155..5ac268c3a 100644 --- a/src/api/bkuser_core/tests/categories/plugins/ldap/test_adaptor.py +++ b/src/api/bkuser_core/tests/categories/plugins/ldap/test_adaptor.py @@ -191,6 +191,7 @@ def test_parse_dn_tree(dn, restrict_types, expected): ["com", "center", "Users", "Schema Admins"], ["com", "center", "Builtin", "Administrators"], ], + extras={}, ), ), ( @@ -222,6 +223,7 @@ def test_parse_dn_tree(dn, restrict_types, expected): ["Users"], ["Builtin", "Guests"], ], + extras={}, ), ), ], diff --git a/src/api/bkuser_core/tests/categories/plugins/local/test_handler.py b/src/api/bkuser_core/tests/categories/plugins/local/test_handler.py index ed62f38b3..46e953fb9 100644 --- a/src/api/bkuser_core/tests/categories/plugins/local/test_handler.py +++ b/src/api/bkuser_core/tests/categories/plugins/local/test_handler.py @@ -9,8 +9,8 @@ specific language governing permissions and limitations under the License. """ import pytest -from bkuser_core.categories.handlers import make_local_default_settings from bkuser_core.categories.models import ProfileCategory +from bkuser_core.categories.plugins.local.handlers import make_local_default_settings from bkuser_core.user_settings.models import Setting pytestmark = pytest.mark.django_db diff --git a/src/api/bkuser_core/tests/categories/plugins/test_plugin.py b/src/api/bkuser_core/tests/categories/plugins/test_plugin.py new file mode 100644 index 000000000..b38a77bc3 --- /dev/null +++ b/src/api/bkuser_core/tests/categories/plugins/test_plugin.py @@ -0,0 +1,118 @@ +# -*- 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 time + +import pytest +from bkuser_core.categories.models import ProfileCategory +from bkuser_core.categories.plugins.base import Syncer +from bkuser_core.categories.plugins.plugin import DataSourcePlugin +from bkuser_core.user_settings.models import Setting, SettingMeta + +pytestmark = pytest.mark.django_db + + +class TestPlugin: + class FakeSyncerCls(Syncer): + """Fake Syncer Class""" + + @pytest.fixture + def plugin(self) -> DataSourcePlugin: + return DataSourcePlugin( + name="test", + syncer_cls=self.FakeSyncerCls, + login_handler_cls=None, + allow_client_write=True, + category_type="test", + ) + + @pytest.fixture + def test_category(self) -> ProfileCategory: + return ProfileCategory.objects.create(type="test", domain="test.com") + + @pytest.mark.parametrize( + "settings,expected", + [ + ( + { + "some-key": {"default": "some-default", "example": "some-example"}, + "other-key": {"namespace": "foo", "region": "bar", "default": "qqq"}, + }, + { + "metas": [ + {"key": "other-key", "default": "qqq", "example": "", "region": "bar", "namespace": "foo"}, + { + "key": "some-key", + "default": "some-default", + "example": "some-example", + "region": "default", + "namespace": "general", + }, + ], + "instances": [ + {"meta__key": "other-key", "value": "qqq"}, + {"meta__key": "some-key", "value": "some-default"}, + ], + }, + ), + ( + { + "some-key": {"default": None, "example": "some-example"}, + }, + { + "metas": [ + { + "key": "some-key", + "default": None, + "example": "some-example", + "region": "default", + "namespace": "general", + }, + ], + "instances": [], + }, + ), + ], + ) + def test_load_settings(self, plugin, test_category, settings, expected): + """test load settings""" + + for key, meta_info in settings.items(): + plugin.init_settings(key, meta_info) + + assert ( + list( + SettingMeta.objects.filter(category_type="test").values( + "key", "default", "example", "region", "namespace" + ) + ) + == expected["metas"] + ) + assert ( + list(Setting.objects.filter(category=test_category).values("meta__key", "value")) == expected["instances"] + ) + + def test_load_no_update(self, plugin, test_category): + """test load settings without updating""" + meta = SettingMeta.objects.create(category_type="test", key="foo", region="a", namespace="b", default="wasd") + update_time = meta.update_time + + time.sleep(0.1) + plugin.init_settings("foo", {"region": "a", "namespace": "b", "default": "wasd"}) + new_update_time = SettingMeta.objects.get(category_type="test", key="foo", namespace="b").update_time + + assert update_time == new_update_time + + # region 可以被修改 + time.sleep(0.1) + plugin.init_settings("foo", {"region": "www", "namespace": "b"}) + meta = SettingMeta.objects.get(category_type="test", key="foo", namespace="b") + assert update_time != meta.update_time + assert meta.region == "www" diff --git a/src/api/bkuser_core/tests/categories/plugins/test_tasks.py b/src/api/bkuser_core/tests/categories/plugins/test_tasks.py new file mode 100644 index 000000000..f050f0c92 --- /dev/null +++ b/src/api/bkuser_core/tests/categories/plugins/test_tasks.py @@ -0,0 +1,47 @@ +# -*- 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 pytest +from bkuser_core.categories.constants import SyncTaskStatus, SyncTaskType +from bkuser_core.categories.models import SyncTask +from bkuser_core.categories.tasks import sync_data_task + +pytestmark = pytest.mark.django_db + + +class TestSyncDataTask: + @pytest.fixture + def sync_task(self, test_ldap_category): + return SyncTask.objects.register_task(category=test_ldap_category, operator="admin", type_=SyncTaskType.AUTO) + + @pytest.mark.parametrize( + "retrying_status", + [ + [True], + [False], + [True, False], + [True, True, False], + [True, True, True], + ], + ) + def test_sync_data_task(self, test_ldap_category, sync_task, retrying_status): + """测试同步数据任务""" + + for t in retrying_status: + with pytest.raises(ValueError): + with sync_data_task(test_ldap_category, sync_task.id, t): + raise ValueError("Anything wrong") + + sync_task = SyncTask.objects.get(pk=sync_task.id) + assert ( + sync_task.status == SyncTaskStatus.RETRYING.value if retrying_status[-1] else SyncTaskStatus.FAILED.value + ) + + assert sync_task.retried_count == len([x for x in retrying_status if x]) diff --git a/src/api/bkuser_core/tests/common/test_cache.py b/src/api/bkuser_core/tests/common/test_cache.py index f2948b6fd..024f6134e 100644 --- a/src/api/bkuser_core/tests/common/test_cache.py +++ b/src/api/bkuser_core/tests/common/test_cache.py @@ -10,7 +10,7 @@ """ import pytest from bkuser_core.common.http import _force_response_data -from bkuser_core.profiles.views import ProfileViewSet +from bkuser_core.profiles.v2.views import ProfileViewSet from bkuser_core.tests.apis.utils import get_api_factory from django.conf import settings diff --git a/src/api/bkuser_core/tests/common/test_http.py b/src/api/bkuser_core/tests/common/test_http.py index 7a8f01cf9..4c71a5886 100644 --- a/src/api/bkuser_core/tests/common/test_http.py +++ b/src/api/bkuser_core/tests/common/test_http.py @@ -10,7 +10,7 @@ """ import pytest from bkuser_core.common.http import _force_response_data, force_response_ee_format, force_response_raw_format -from bkuser_core.profiles.views import ProfileViewSet +from bkuser_core.profiles.v2.views import ProfileViewSet from django.conf import settings from rest_framework.test import APIRequestFactory diff --git a/src/api/bkuser_core/tests/common/test_serializers.py b/src/api/bkuser_core/tests/common/test_serializers.py index 9b7373911..d0142c283 100644 --- a/src/api/bkuser_core/tests/common/test_serializers.py +++ b/src/api/bkuser_core/tests/common/test_serializers.py @@ -9,9 +9,9 @@ specific language governing permissions and limitations under the License. """ -from bkuser_core.common.serializers import is_custom_fields_enabled +from bkuser_core.apis.v2.serializers import is_custom_fields_enabled from bkuser_core.departments.serializers import DepartmentAddProfilesSerializer, DepartmentSerializer -from bkuser_core.profiles.serializers import ProfileSerializer +from bkuser_core.profiles.v2.serializers import ProfileSerializer class TestSerializers: diff --git a/src/api/bkuser_core/tests/profiles/test_tasks.py b/src/api/bkuser_core/tests/profiles/test_tasks.py index e0522aafb..94b8b7988 100644 --- a/src/api/bkuser_core/tests/profiles/test_tasks.py +++ b/src/api/bkuser_core/tests/profiles/test_tasks.py @@ -8,6 +8,7 @@ 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 urllib.parse from unittest.mock import patch import pytest @@ -34,7 +35,8 @@ class TestSendMailTask: "abcd", False, "aaaa", - f"{settings.SAAS_URL}set_password?token=aaaa :{settings.SAAS_URL}reset_password ", + f'{urllib.parse.urljoin(settings.SAAS_URL, "set_password?token=aaaa")}:' + f'{urllib.parse.urljoin(settings.SAAS_URL, "reset_password")}', ), ], ) diff --git a/src/api/bkuser_core/urls.py b/src/api/bkuser_core/urls.py index 3b289e2ec..9280bec71 100644 --- a/src/api/bkuser_core/urls.py +++ b/src/api/bkuser_core/urls.py @@ -10,14 +10,10 @@ """ import logging -from bkuser_core.common.serializers import patch_datetime_field from django.conf import settings from django.conf.urls import include, url from django.utils.module_loading import import_module -patch_datetime_field() - - logger = logging.getLogger(__name__) diff --git a/src/api/bkuser_core/user_settings/exceptions.py b/src/api/bkuser_core/user_settings/exceptions.py new file mode 100644 index 000000000..c87172c09 --- /dev/null +++ b/src/api/bkuser_core/user_settings/exceptions.py @@ -0,0 +1,21 @@ +# -*- 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. +""" + + +class SettingHasBeenDisabledError(Exception): + """配置已经被禁用""" + + def __init__(self, key: str, *args): + self.key = key + super().__init__(*args) + + def __str__(self): + return f"setting {self.key} has been disabled." diff --git a/src/api/bkuser_core/user_settings/loader.py b/src/api/bkuser_core/user_settings/loader.py index 3d60b486c..bc08d4205 100644 --- a/src/api/bkuser_core/user_settings/loader.py +++ b/src/api/bkuser_core/user_settings/loader.py @@ -10,6 +10,8 @@ """ from dataclasses import dataclass, field +from bkuser_core.user_settings.exceptions import SettingHasBeenDisabledError + from .models import Setting @@ -24,11 +26,14 @@ def __post_init__(self): self._refresh_config() def _refresh_config(self): - settings = Setting.objects.prefetch_related("meta").filter(category_id=self.category_id, enabled=True) + settings = Setting.objects.prefetch_related("meta").filter(category_id=self.category_id) self._raws = {x.meta.key: x for x in settings} self._config = {x.meta.key: x.value for x in settings} def get(self, k, d=None): + if k in self._raws and not self._raws.get(k).enabled: + raise SettingHasBeenDisabledError(k) + return self._config.get(k, d) def __getitem__(self, key): 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 7415f40a7..66eeb043d 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 @@ -43,10 +43,10 @@ def forwards_func(apps, schema_editor): dict( key="init_mail_config", default={ - "title": "蓝鲸智云企业版 - 您的帐户已经成功创建!", + "title": "蓝鲸智云企业版 - 您的账户已经成功创建!", "sender": "蓝鲸智云企业版", - "content": "您好!您的蓝鲸智云企业版帐户已经成功创建,以下是您的帐户信息:登录帐户:{username}, " - "初始登录密码:{password} 为了保障帐户安全,我们建议您尽快登录蓝鲸智云企业版修改密码:{url} " + "content": "您好!您的蓝鲸智云企业版账户已经成功创建,以下是您的账户信息:登录账户:{username}, " + "初始登录密码:{password} 为了保障账户安全,我们建议您尽快登录蓝鲸智云企业版修改密码:{url} " "此邮件为系统自动发送,请勿回复。蓝鲸智云官网: http://bk.tencent.com", }, ), diff --git a/src/api/bkuser_core/user_settings/migrations/0006_auto_20200429_1540.py b/src/api/bkuser_core/user_settings/migrations/0006_auto_20200429_1540.py index e0eb4cb07..6f72e7dd7 100644 --- a/src/api/bkuser_core/user_settings/migrations/0006_auto_20200429_1540.py +++ b/src/api/bkuser_core/user_settings/migrations/0006_auto_20200429_1540.py @@ -18,22 +18,6 @@ def forwards_func(apps, schema_editor): """添加默认用户目录""" - SettingMeta = apps.get_model("user_settings", "SettingMeta") - - tof_connection_settings = [ - dict(key="root_company_id", default=0), - dict(key="max_depth", default=8), - dict(key="leader_keyword", default="TeamLeader"), - dict(key="normal_status_keyword", default="在职"), - dict(key="common_email_suffix", default="@tencent.com"), - dict(key="exempt_sync_department_ids", default=[]), - dict(key="pull_interval", default=60 * 60 * 24), - ] - - for x in tof_connection_settings: - SettingMeta.objects.create( - namespace=SettingsEnableNamespaces.GENERAL.value, category_type=CategoryType.TOF.value, required=True, **x - ) class Migration(migrations.Migration): diff --git a/src/api/bkuser_core/user_settings/migrations/0007_auto_20210703_2020.py b/src/api/bkuser_core/user_settings/migrations/0007_auto_20210703_2020.py index df7ee73b8..09b5cfec3 100644 --- a/src/api/bkuser_core/user_settings/migrations/0007_auto_20210703_2020.py +++ b/src/api/bkuser_core/user_settings/migrations/0007_auto_20210703_2020.py @@ -22,7 +22,8 @@ def forwards_func(apps, schema_editor): new_setting_meta_map = {} for category_type in need_connect_types: meta = SettingMeta.objects.create( - namespace=SettingsEnableNamespaces.CONNECTION.value, + namespace=SettingsEnableNamespaces.FIELDS.value, + region="group", category_type=category_type, required=False, **dict(key="user_member_of", example="memberOf", default="memberOf") @@ -46,7 +47,8 @@ def backwards_func(apps, schema_editor): SettingMeta = apps.get_model("user_settings", "SettingMeta") need_connect_types = [CategoryType.MAD.value, CategoryType.LDAP.value] meta = SettingMeta.objects.filter( - namespace=SettingsEnableNamespaces.CONNECTION.value, + namespace=SettingsEnableNamespaces.FIELDS.value, + region="group", category_type__in=need_connect_types, key="user_member_of", ) diff --git a/src/api/bkuser_core/user_settings/migrations/0008_auto_20210706_1702.py b/src/api/bkuser_core/user_settings/migrations/0008_auto_20210706_1702.py index 588356f2a..182572cfd 100644 --- a/src/api/bkuser_core/user_settings/migrations/0008_auto_20210706_1702.py +++ b/src/api/bkuser_core/user_settings/migrations/0008_auto_20210706_1702.py @@ -44,7 +44,6 @@ class Migration(migrations.Migration): ("local", "本地目录"), ("mad", "Microsoft Active Directory"), ("ldap", "OpenLDAP"), - ("tof", "TOF"), ("custom", "自定义目录"), ], max_length=32, diff --git a/src/api/bkuser_core/user_settings/migrations/0009_alter_settingmeta_category_type.py b/src/api/bkuser_core/user_settings/migrations/0009_alter_settingmeta_category_type.py index 4c6ac42bd..a44c3952a 100644 --- a/src/api/bkuser_core/user_settings/migrations/0009_alter_settingmeta_category_type.py +++ b/src/api/bkuser_core/user_settings/migrations/0009_alter_settingmeta_category_type.py @@ -6,13 +6,23 @@ class Migration(migrations.Migration): dependencies = [ - ('user_settings', '0008_auto_20210706_1702'), + ("user_settings", "0008_auto_20210706_1702"), ] operations = [ migrations.AlterField( - model_name='settingmeta', - name='category_type', - field=models.CharField(choices=[('local', '本地目录'), ('mad', 'Microsoft Active Directory'), ('ldap', 'OpenLDAP'), ('tof', 'TOF'), ('custom', '自定义目录'), ('pluggable', '可插拔目录')], max_length=32, verbose_name='类型'), + model_name="settingmeta", + name="category_type", + field=models.CharField( + choices=[ + ("local", "本地目录"), + ("mad", "Microsoft Active Directory"), + ("ldap", "OpenLDAP"), + ("custom", "自定义目录"), + ("pluggable", "可插拔目录"), + ], + max_length=32, + verbose_name="类型", + ), ), ] diff --git a/src/api/bkuser_core/user_settings/migrations/0010_auto_20211201_1601.py b/src/api/bkuser_core/user_settings/migrations/0010_auto_20211201_1601.py new file mode 100644 index 000000000..8a53a482c --- /dev/null +++ b/src/api/bkuser_core/user_settings/migrations/0010_auto_20211201_1601.py @@ -0,0 +1,52 @@ +# Generated by Django 3.2.5 on 2021-12-01 08:01 + +from django.db import migrations +from django.conf import settings + +from bkuser_core.categories.constants import CategoryType +from bkuser_core.user_settings.constants import SettingsEnableNamespaces + + +def forwards_func(apps, schema_editor): + """增加配置项""" + SettingMeta = apps.get_model("user_settings", "SettingMeta") + meta = SettingMeta.objects.create( + namespace=SettingsEnableNamespaces.PASSWORD.value, + category_type=CategoryType.LOCAL.value, + required=False, + **dict( + key="max_password_history", + example=settings.DEFAULT_MAX_PASSWORD_HISTORY, + default=settings.DEFAULT_MAX_PASSWORD_HISTORY, + ) + ) + ProfileCategory = apps.get_model("categories", "ProfileCategory") + Setting = apps.get_model("user_settings", "Setting") + + for category in ProfileCategory.objects.filter(type=CategoryType.LOCAL.value): + Setting.objects.get_or_create( + category=category, + meta=meta, + value=meta.default, + ) + + +def backwards_func(apps, schema_editor): + SettingMeta = apps.get_model("user_settings", "SettingMeta") + meta = SettingMeta.objects.get( + namespace=SettingsEnableNamespaces.PASSWORD.value, + category_type=CategoryType.LOCAL.value, + key="user_member_of", + ) + 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", "0009_alter_settingmeta_category_type"), + ] + + operations = [migrations.RunPython(forwards_func, backwards_func)] diff --git a/src/api/bkuser_core/user_settings/migrations/0011_alter_extend_fields_connection_settings.py b/src/api/bkuser_core/user_settings/migrations/0011_alter_extend_fields_connection_settings.py new file mode 100644 index 000000000..d0106c0fc --- /dev/null +++ b/src/api/bkuser_core/user_settings/migrations/0011_alter_extend_fields_connection_settings.py @@ -0,0 +1,56 @@ +# -*- 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") + + for category_type in [CategoryType.LDAP.value, CategoryType.MAD.value]: + meta, _ = SettingMeta.objects.get_or_create( + key="dynamic_fields_mapping", + namespace=SettingsEnableNamespaces.FIELDS.value, + category_type=category_type, + region="extend", + defaults={"default": {}, "required": False}, + ) + + # 保证已存在的目录拥有默认配置 + for c in ProfileCategory.objects.filter(type=category_type): + Setting.objects.get_or_create(meta=meta, category_id=c.id, value=meta.default) + + meta = SettingMeta.objects.filter(key__in=["bk_fields", "mad_fields"]) + Setting.objects.filter(meta__in=meta).delete() + meta.delete() + + +def backwards_func(apps, schema_editor): + SettingMeta = apps.get_model("user_settings", "SettingMeta") + Setting = apps.get_model("user_settings", "Setting") + + Setting.objects.filter(meta__key="dynamic_fields_mapping").delete() + SettingMeta.objects.filter(key="dynamic_fields_mapping").delete() + + +class Migration(migrations.Migration): + + dependencies = [ + ("user_settings", "0010_auto_20211201_1601"), + ] + + operations = [migrations.RunPython(forwards_func, backwards_func)] diff --git a/src/api/bkuser_core/user_settings/models.py b/src/api/bkuser_core/user_settings/models.py index d633c3f7a..e5258e26e 100644 --- a/src/api/bkuser_core/user_settings/models.py +++ b/src/api/bkuser_core/user_settings/models.py @@ -58,7 +58,7 @@ class SettingMeta(TimestampedModel): key = models.CharField("配置键", max_length=64) enabled = models.BooleanField(default=True) example = jsonfield.JSONField("示例", default="") - default = jsonfield.JSONField("默认值", default="") + default = jsonfield.JSONField("默认值", default=None) choices = jsonfield.JSONField("可选值", default=[]) required = models.BooleanField("是否必要", default=False) namespace = models.CharField( diff --git a/src/api/bkuser_core/user_settings/serializers.py b/src/api/bkuser_core/user_settings/serializers.py index a7ff4fa68..1aa0755ec 100644 --- a/src/api/bkuser_core/user_settings/serializers.py +++ b/src/api/bkuser_core/user_settings/serializers.py @@ -8,7 +8,7 @@ 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 bkuser_core.common.serializers import CustomFieldsModelSerializer +from bkuser_core.apis.v2.serializers import CustomFieldsModelSerializer from rest_framework import serializers from .models import Setting, SettingMeta @@ -45,6 +45,7 @@ class SettingCreateSerializer(serializers.Serializer): class SettingUpdateSerializer(serializers.Serializer): value = serializers.JSONField() + enabled = serializers.BooleanField(default=True) class SettingListSerializer(serializers.Serializer): diff --git a/src/api/bkuser_core/user_settings/urls.py b/src/api/bkuser_core/user_settings/urls.py index 0f42b7eb0..9ac7bf315 100644 --- a/src/api/bkuser_core/user_settings/urls.py +++ b/src/api/bkuser_core/user_settings/urls.py @@ -8,7 +8,7 @@ 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 bkuser_core.common.constants import LOOKUP_FIELD_NAME +from bkuser_core.apis.v2.constants import LOOKUP_FIELD_NAME from django.conf.urls import url from . import views diff --git a/src/api/bkuser_core/user_settings/views.py b/src/api/bkuser_core/user_settings/views.py index f3cd4b111..27cc9ad91 100644 --- a/src/api/bkuser_core/user_settings/views.py +++ b/src/api/bkuser_core/user_settings/views.py @@ -10,10 +10,11 @@ """ import logging +from bkuser_core.apis.v2.viewset import AdvancedListAPIView, AdvancedModelViewSet 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 -from bkuser_core.common.viewset import AdvancedListAPIView, AdvancedModelViewSet +from bkuser_core.common.models import is_obj_needed_update from bkuser_core.user_settings import serializers from bkuser_core.user_settings.models import Setting, SettingMeta from bkuser_core.user_settings.serializers import SettingUpdateSerializer @@ -23,13 +24,15 @@ from rest_framework import status from rest_framework.response import Response +from bkuser_global.drf_crown.crown import inject_serializer + logger = logging.getLogger(__name__) class SettingViewSet(AdvancedModelViewSet): """配置项""" - queryset = Setting.objects.filter(enabled=True) + queryset = Setting.objects.all() serializer_class = serializers.SettingSerializer lookup_field: str = "id" @@ -96,27 +99,38 @@ def create(self, request, *args, **kwargs): ) return Response(serializers.SettingSerializer(setting).data, status=status.HTTP_201_CREATED) - @swagger_auto_schema( - request_body=SettingUpdateSerializer(), - responses={"200": serializers.SettingSerializer()}, - ) - def update(self, request, *args, **kwargs): - result = super().update(request, *args, **kwargs) - post_setting_update.send( - sender=self, instance=self.get_object(), operator=request.operator, extra_values={"request": request} - ) - return result - - @swagger_auto_schema( - request_body=SettingUpdateSerializer(), - responses={"200": serializers.SettingSerializer()}, - ) - def partial_update(self, request, *args, **kwargs): - result = super().partial_update(request, *args, **kwargs) - post_setting_update.send( - sender=self, instance=self.get_object(), operator=request.operator, extra_values={"request": request} - ) - return result + def _update(self, request, validated_data): + instance = self.get_object() + try: + need_update = is_obj_needed_update(instance, validated_data) + except ValueError: + raise error_codes.CANNOT_UPDATE_SETTING + + if need_update: + try: + for k, v in validated_data.items(): + setattr(instance, k, v) + instance.save() + except Exception: + logger.exception("failed to update setting") + raise error_codes.CANNOT_UPDATE_SETTING + else: + # 仅当更新成功时才发送信号 + post_setting_update.send( + sender=self, + instance=self.get_object(), + operator=request.operator, + extra_values={"request": request}, + ) + return instance + + @inject_serializer(body_in=SettingUpdateSerializer(), out=serializers.SettingSerializer) + def update(self, request, validated_data, *args, **kwargs): + return self._update(request, validated_data) + + @inject_serializer(body_in=SettingUpdateSerializer(), out=serializers.SettingSerializer) + def partial_update(self, request, validated_data, *args, **kwargs): + return self._update(request, validated_data) class SettingMetaViewSet(AdvancedModelViewSet, AdvancedListAPIView): diff --git a/src/api/manage.py b/src/api/manage.py index 72f5597d5..bb32dac23 100644 --- a/src/api/manage.py +++ b/src/api/manage.py @@ -1,4 +1,13 @@ #!/usr/bin/env python +""" +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 os import sys diff --git a/src/api/poetry.lock b/src/api/poetry.lock index e587beb54..ace1d47ce 100644 --- a/src/api/poetry.lock +++ b/src/api/poetry.lock @@ -1497,6 +1497,19 @@ type = "legacy" url = "https://mirrors.tencent.com/pypi/simple" reference = "tencent-mirrors" +[[package]] +name = "pyyaml" +version = "6.0" +description = "YAML parser and emitter for Python" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.source] +type = "legacy" +url = "https://mirrors.tencent.com/pypi/simple" +reference = "tencent-mirrors" + [[package]] name = "redis" version = "3.2.0" @@ -1668,6 +1681,19 @@ type = "legacy" url = "https://mirrors.tencent.com/pypi/simple" reference = "tencent-mirrors" +[[package]] +name = "types-cachetools" +version = "4.2.9" +description = "Typing stubs for cachetools" +category = "dev" +optional = false +python-versions = "*" + +[package.source] +type = "legacy" +url = "https://mirrors.tencent.com/pypi/simple" +reference = "tencent-mirrors" + [[package]] name = "types-dataclasses" version = "0.1.5" @@ -1865,7 +1891,7 @@ reference = "tencent-mirrors" [metadata] lock-version = "1.1" python-versions = "3.6.14" -content-hash = "9e24b2f0dff4561390ae71ec18233f6c1cbffad065b257a609ed909b5ee139b8" +content-hash = "c6c1d6a20be3b1306e46207c2ae114c977198d721b4267678b44b9ba987d2bf6" [metadata.files] aenum = [ @@ -1933,6 +1959,11 @@ cffi = [ {file = "cffi-1.14.6-cp27-cp27m-win_amd64.whl", hash = "sha256:7bcac9a2b4fdbed2c16fa5681356d7121ecabf041f18d97ed5b8e0dd38a80224"}, {file = "cffi-1.14.6-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:ed38b924ce794e505647f7c331b22a693bee1538fdf46b0222c4717b42f744e7"}, {file = "cffi-1.14.6-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:e22dcb48709fc51a7b58a927391b23ab37eb3737a98ac4338e2448bef8559b33"}, + {file = "cffi-1.14.6-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:aedb15f0a5a5949ecb129a82b72b19df97bbbca024081ed2ef88bd5c0a610534"}, + {file = "cffi-1.14.6-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:48916e459c54c4a70e52745639f1db524542140433599e13911b2f329834276a"}, + {file = "cffi-1.14.6-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:f627688813d0a4140153ff532537fbe4afea5a3dffce1f9deb7f91f848a832b5"}, + {file = "cffi-1.14.6-cp35-cp35m-win32.whl", hash = "sha256:f0010c6f9d1a4011e429109fda55a225921e3206e7f62a0c22a35344bfd13cca"}, + {file = "cffi-1.14.6-cp35-cp35m-win_amd64.whl", hash = "sha256:57e555a9feb4a8460415f1aac331a2dc833b1115284f7ded7278b54afc5bd218"}, {file = "cffi-1.14.6-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e8c6a99be100371dbb046880e7a282152aa5d6127ae01783e37662ef73850d8f"}, {file = "cffi-1.14.6-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:19ca0dbdeda3b2615421d54bef8985f72af6e0c47082a8d26122adac81a95872"}, {file = "cffi-1.14.6-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:d950695ae4381ecd856bcaf2b1e866720e4ab9a1498cba61c602e56630ca7195"}, @@ -2051,8 +2082,10 @@ cryptography = [ {file = "cryptography-3.4.7-cp36-abi3-win_amd64.whl", hash = "sha256:de4e5f7f68220d92b7637fc99847475b59154b7a1b3868fb7385337af54ac9ca"}, {file = "cryptography-3.4.7-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:26965837447f9c82f1855e0bc8bc4fb910240b6e0d16a664bb722df3b5b06873"}, {file = "cryptography-3.4.7-pp36-pypy36_pp73-manylinux2014_x86_64.whl", hash = "sha256:eb8cc2afe8b05acbd84a43905832ec78e7b3873fb124ca190f574dca7389a87d"}, + {file = "cryptography-3.4.7-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:b01fd6f2737816cb1e08ed4807ae194404790eac7ad030b34f2ce72b332f5586"}, {file = "cryptography-3.4.7-pp37-pypy37_pp73-manylinux2010_x86_64.whl", hash = "sha256:7ec5d3b029f5fa2b179325908b9cd93db28ab7b85bb6c1db56b10e0b54235177"}, {file = "cryptography-3.4.7-pp37-pypy37_pp73-manylinux2014_x86_64.whl", hash = "sha256:ee77aa129f481be46f8d92a1a7db57269a2f23052d5f2433b4621bb457081cc9"}, + {file = "cryptography-3.4.7-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:bf40af59ca2465b24e54f671b2de2c59257ddc4f7e5706dbd6930e26823668d3"}, {file = "cryptography-3.4.7.tar.gz", hash = "sha256:3d10de8116d25649631977cb37da6cbdd2d6fa0e0281d014a5b7d337255ca713"}, ] curlify = [ @@ -2254,12 +2287,28 @@ ldap3 = [ {file = "ldap3-2.6.1.tar.gz", hash = "sha256:27cb673e7afcb539f6adcae5a3ecac4e74eb37ca0a2d50dc98f29a3829eee529"}, ] markupsafe = [ + {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4dc8f9fb58f7364b63fd9f85013b780ef83c11857ae79f2feda41e270468dd9b"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:20dca64a3ef2d6e4d5d615a3fd418ad3bde77a47ec8a23d984a12b5b4c74491a"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cdfba22ea2f0029c9261a4bd07e830a8da012291fbe44dc794e488b6c9bb353a"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-win32.whl", hash = "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:deb993cacb280823246a026e3b2d81c493c53de6acfd5e6bfe31ab3402bb37dd"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:63f3268ba69ace99cab4e3e3b5840b03340efed0948ab8f78d2fd87ee5442a4f"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:8d206346619592c6200148b01a2142798c989edcb9c896f9ac9722a99d4e77e6"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"}, @@ -2268,14 +2317,27 @@ markupsafe = [ {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d6c7ebd4e944c85e2c3421e612a7057a2f48d478d79e61800d81468a8d842207"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f0567c4dc99f264f49fe27da5f735f414c4e7e7dd850cfd8e69f0862d7c74ea9"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:89c687013cb1cd489a0f0ac24febe8c7a666e6e221b783e53ac50ebf68e45d86"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9"}, {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:aca6377c0cb8a8253e493c6b451565ac77e98c2951c45f913e0b52facdcff83f"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:04635854b943835a6ea959e948d19dcd311762c5c0c6e1f0e16ee57022669194"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6300b8454aa6930a24b9618fbb54b5a68135092bc666f7b06901f897fa5c2fee"}, {file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"}, {file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"}, {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"}, @@ -2285,6 +2347,12 @@ markupsafe = [ {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"}, {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"}, {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4296f2b1ce8c86a6aea78613c34bb1a672ea0e3de9c6ba08a960efe0b0a09047"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f02365d4e99430a12647f09b6cc8bab61a6564363f313126f775eb4f6ef798e"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5b6d930f030f8ed98e3e6c98ffa0652bdb82601e7a016ec2ab5d7ff23baa78d1"}, {file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"}, {file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"}, {file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"}, @@ -2452,6 +2520,41 @@ pytz = [ {file = "pytz-2021.1-py2.py3-none-any.whl", hash = "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798"}, {file = "pytz-2021.1.tar.gz", hash = "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da"}, ] +pyyaml = [ + {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, + {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, + {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, + {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, + {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"}, + {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, + {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, + {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"}, + {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, + {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, + {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"}, + {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, + {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, + {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, + {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"}, + {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, + {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, + {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, +] redis = [ {file = "redis-3.2.0-py2.py3-none-any.whl", hash = "sha256:9b19425a38fd074eb5795ff2b0d9a55b46a44f91f5347995f27e3ad257a7d775"}, {file = "redis-3.2.0.tar.gz", hash = "sha256:724932360d48e5407e8f82e405ab3650a36ed02c7e460d1e6fddf0f038422b54"}, @@ -2488,6 +2591,10 @@ requests = [ {file = "ruamel.yaml-0.17.10.tar.gz", hash = "sha256:106bc8d6dc6a0ff7c9196a47570432036f41d556b779c6b4e618085f57e39e67"}, ] "ruamel.yaml.clib" = [ + {file = "ruamel.yaml.clib-0.2.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6e7be2c5bcb297f5b82fee9c665eb2eb7001d1050deaba8471842979293a80b0"}, + {file = "ruamel.yaml.clib-0.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:221eca6f35076c6ae472a531afa1c223b9c29377e62936f61bc8e6e8bdc5f9e7"}, + {file = "ruamel.yaml.clib-0.2.6-cp310-cp310-win32.whl", hash = "sha256:1070ba9dd7f9370d0513d649420c3b362ac2d687fe78c6e888f5b12bf8bc7bee"}, + {file = "ruamel.yaml.clib-0.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:77df077d32921ad46f34816a9a16e6356d8100374579bc35e15bab5d4e9377de"}, {file = "ruamel.yaml.clib-0.2.6-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:cfdb9389d888c5b74af297e51ce357b800dd844898af9d4a547ffc143fa56751"}, {file = "ruamel.yaml.clib-0.2.6-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:7b2927e92feb51d830f531de4ccb11b320255ee95e791022555971c466af4527"}, {file = "ruamel.yaml.clib-0.2.6-cp35-cp35m-win32.whl", hash = "sha256:ada3f400d9923a190ea8b59c8f60680c4ef8a4b0dfae134d2f2ff68429adfab5"}, @@ -2562,6 +2669,10 @@ typed-ast = [ {file = "typed_ast-1.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:9c6d1a54552b5330bc657b7ef0eae25d00ba7ffe85d9ea8ae6540d2197a3788c"}, {file = "typed_ast-1.4.3.tar.gz", hash = "sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65"}, ] +types-cachetools = [ + {file = "types-cachetools-4.2.9.tar.gz", hash = "sha256:4ac4b7866da8d3f750413a3e91809c2bf6124aa920ef6dc32c5df2d84a53585c"}, + {file = "types_cachetools-4.2.9-py3-none-any.whl", hash = "sha256:da18b1acbc4a03ed3ce8f6ad4d70ad8ca31f542c205cb45ceb524d2ddd544c28"}, +] types-dataclasses = [ {file = "types-dataclasses-0.1.5.tar.gz", hash = "sha256:7b5f4099fb21c209f2df3a83c2b64308c29955769d610a457244dc0eebe1cafc"}, {file = "types_dataclasses-0.1.5-py2.py3-none-any.whl", hash = "sha256:c19491cfb981bff9cafd9c113c291a7a54adccc6298ded8ca3de0d7abe211984"}, diff --git a/src/api/pyproject.toml b/src/api/pyproject.toml index 6ac00ccff..e5f5578db 100644 --- a/src/api/pyproject.toml +++ b/src/api/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "bk-user-api" -version = "2.3.1" +version = "2.3.2" description = "bk-user Api" authors = ["IMBlues "] @@ -37,6 +37,7 @@ cryptography = "^3.0.0" pydantic = "^1.8.2" django-environ = "^0.4.5" django-prometheus = "^2.1.0" +pyyaml = "^6.0" [tool.poetry.dev-dependencies] ipython = "^7.15.0" @@ -60,6 +61,7 @@ types-pyyaml = "^5.4.3" types-pymysql = "^1.0.0" types-redis = "^3.5.4" types-toml = "^0.1.3" +types-cachetools = "^4.2.9" [[tool.poetry.source]] name = "tencent-mirrors" diff --git a/src/bkuser_global/config.py b/src/bkuser_global/config.py index 55f6adf90..da9ff19a5 100644 --- a/src/bkuser_global/config.py +++ b/src/bkuser_global/config.py @@ -8,8 +8,6 @@ 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 os - import environ @@ -43,109 +41,3 @@ def get_db_config(env: environ.Env, db_prefix: str) -> dict: "TEST": {"CHARSET": "utf8mb4", "COLLATION": "utf8mb4_general_ci"}, } } - - -def get_logging_config_dict(log_level: str, logging_dir: str, log_class: str, file_name: str, package_name: str): - if not os.path.exists(logging_dir): - os.makedirs(logging_dir) - - return { - "version": 1, - "disable_existing_loggers": False, - "formatters": { - "verbose": { - "format": "%(levelname)s [%(asctime)s] %(name)s(ln:%(lineno)d): %(message)s", # noqa - "datefmt": "%Y-%m-%d %H:%M:%S", - }, - "simple": {"format": "%(levelname)s %(message)s"}, - "iam": {"format": "[IAM] %(asctime)s %(message)s"}, - }, - "handlers": { - "null": {"level": "DEBUG", "class": "logging.NullHandler"}, - "root": { - "class": log_class, - "formatter": "verbose", - "filename": os.path.join(logging_dir, "%s.log" % file_name), - "maxBytes": 1024 * 1024 * 10, - "backupCount": 5, - }, - "console": { - "level": log_level, - "class": "logging.StreamHandler", - "formatter": "verbose", - }, - "component": { - "class": log_class, - "formatter": "verbose", - "filename": os.path.join(logging_dir, "component.log"), - "maxBytes": 1024 * 1024 * 10, - "backupCount": 5, - }, - "iam": { - "class": log_class, - "formatter": "iam", - "filename": os.path.join(logging_dir, "iam.log"), - "maxBytes": 1024 * 1024 * 10, - "backupCount": 5, - }, - }, - "loggers": { - "django": { - "handlers": ["null"], - "level": "INFO", - "propagate": True, - }, - "django.request": { - "handlers": [ - "root", - ], - "level": "ERROR", - "propagate": True, - }, - "django.db.backends": { - "handlers": [ - "root", - ], - "level": "INFO", - "propagate": True, - }, - "django.security": { - "handlers": [ - "root", - ], - "level": "INFO", - "propagate": True, - }, - package_name: { - "handlers": [ - "console", - ], - "level": log_level, - "propagate": True, - }, - "root": { - "handlers": [ - "console", - ], - "level": log_level, - "propagate": True, - }, - "requests": { - "handlers": [ - "console", - ], - "level": os.getenv("DJANGO_LOG_LEVEL", "INFO"), - }, - # 组件调用日志 - "component": { - "handlers": ["console"], - "level": "WARN", - "propagate": True, - }, - "iam": { - "handlers": ["console"], - "level": "DEBUG", - "propagate": True, - }, - }, - } diff --git a/src/bkuser_global/logging.py b/src/bkuser_global/logging.py new file mode 100644 index 000000000..4b9665777 --- /dev/null +++ b/src/bkuser_global/logging.py @@ -0,0 +1,163 @@ +# -*- 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 os + + +class LoggingType: + """日志输出类型""" + + # 所有日志都送到标注输出,对于容器部署时更亲和 + STDOUT = 0 + # 日志按类型分散在各个不同文件,二进制部署时更亲和 + FILE = 1 + + +def get_logging(logging_type: int, **kwargs) -> dict: + """获取 Logging 配置""" + + if logging_type == LoggingType.STDOUT: + return get_stdout_logging(**kwargs) + elif logging_type == LoggingType.FILE: + return get_file_logging(**kwargs) + else: + return get_file_logging(**kwargs) + + +formatters = { + "verbose": { + "format": "%(levelname)s [%(asctime)s] %(lineno)d %(funcName)s %(process)d %(thread)d \n%(message)s \n", + "datefmt": "%Y-%m-%d %H:%M:%S", + }, + "json": { + "()": "pythonjsonlogger.jsonlogger.JsonFormatter", + "fmt": ( + "%(levelname)s %(asctime)s %(pathname)s %(lineno)d " + "%(funcName)s %(process)d %(thread)d %(request_id)s %(message)s" + ), + }, + "simple": {"format": "%(levelname)s %(message)s"}, + "iam": {"format": "[IAM] %(asctime)s %(message)s"}, +} + + +def get_loggers(package_name: str, log_level: str) -> dict: + return { + "django": { + "handlers": ["null"], + "level": "INFO", + "propagate": True, + }, + "django.request": { + "handlers": ["root"], + "level": "ERROR", + "propagate": True, + }, + "django.db.backends": { + "handlers": ["root"], + "level": "INFO", + "propagate": True, + }, + "django.security": { + "handlers": ["root"], + "level": "INFO", + "propagate": True, + }, + package_name: { + "handlers": ["root"], + "level": log_level, + "propagate": True, + }, + "": { + "handlers": ["root"], + "level": log_level, + }, + "requests": { + "handlers": ["root"], + "level": log_level, + }, + # 组件调用日志 + "component": { + "handlers": ["root"], + "level": "WARN", + "propagate": True, + }, + "iam": { + "handlers": ["root"], + "level": log_level, + "propagate": True, + }, + } + + +def get_stdout_logging(log_level: str, package_name: str, formatter: str = "json"): + """获取标准输出日志配置""" + log_class = "logging.StreamHandler" + + return { + "version": 1, + "disable_existing_loggers": False, + "formatters": formatters, + "handlers": { + "null": {"level": "DEBUG", "class": "logging.NullHandler"}, + "root": { + "class": log_class, + "formatter": formatter, + }, + "component": { + "class": log_class, + "formatter": formatter, + }, + "iam": { + "class": log_class, + "formatter": "iam", + }, + }, + "loggers": get_loggers(package_name, log_level), + } + + +def get_file_logging(log_level: str, logging_dir: str, file_name: str, package_name: str, formatter: str = "json"): + """获取文件日志配置""" + log_class = "logging.handlers.RotatingFileHandler" + + if not os.path.exists(logging_dir): + os.makedirs(logging_dir) + + return { + "version": 1, + "disable_existing_loggers": False, + "formatters": formatters, + "handlers": { + "null": {"level": "DEBUG", "class": "logging.NullHandler"}, + "root": { + "class": log_class, + "formatter": formatter, + "filename": os.path.join(logging_dir, f"{file_name}.log"), + "maxBytes": 1024 * 1024 * 10, + "backupCount": 5, + }, + "component": { + "class": log_class, + "formatter": formatter, + "filename": os.path.join(logging_dir, "component.log"), + "maxBytes": 1024 * 1024 * 10, + "backupCount": 5, + }, + "iam": { + "class": log_class, + "formatter": "iam", + "filename": os.path.join(logging_dir, "iam.log"), + "maxBytes": 1024 * 1024 * 10, + "backupCount": 5, + }, + }, + "loggers": get_loggers(package_name, log_level), + } diff --git a/src/login/.gitignore b/src/login/.gitignore new file mode 100755 index 000000000..f6dc3eb09 --- /dev/null +++ b/src/login/.gitignore @@ -0,0 +1,140 @@ +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +pytestdebug.log + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ +doc/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ +pythonenv* +.envrc + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# profiling data +.prof diff --git a/src/login/Dockerfile b/src/login/Dockerfile new file mode 100644 index 000000000..368d1a5f9 --- /dev/null +++ b/src/login/Dockerfile @@ -0,0 +1,33 @@ +FROM python:3.6.14-slim-buster +USER root + +RUN rm /etc/apt/sources.list && \ + echo "deb https://mirrors.cloud.tencent.com/debian buster main contrib non-free" >> /etc/apt/sources.list && \ + echo "deb https://mirrors.cloud.tencent.com/debian buster-updates main contrib non-free" >> /etc/apt/sources.list && \ + echo "deb-src https://mirrors.cloud.tencent.com/debian buster main contrib non-free" >> /etc/apt/sources.list && \ + echo "deb-src https://mirrors.cloud.tencent.com/debian buster-updates main contrib non-free" >> /etc/apt/sources.list + +RUN mkdir ~/.pip && printf '[global]\nindex-url = https://mirrors.tencent.com/pypi/simple/' > ~/.pip/pip.conf + +RUN apt-get update && apt-get install -y gcc gettext + +ENV LC_ALL=C.UTF-8 \ + LANG=C.UTF-8 + +RUN pip install --upgrade pip +RUN pip install poetry==1.1.7 + +WORKDIR /app +COPY src/login/pyproject.toml /app +COPY src/login/poetry.lock /app +RUN poetry config experimental.new-installer false && poetry config virtualenvs.create false && poetry install --no-dev + +COPY src/login/wsgi.py /app +COPY src/login/bklogin /app/bklogin +COPY src/login/locale /app/locale +COPY src/login/static /app/static +COPY src/login/manage.py /app +COPY src/bkuser_global /app/bkuser_global +COPY src/login/bin/start.sh /app + +CMD ["bash", "/app/start.sh"] diff --git a/src/login/Makefile b/src/login/Makefile new file mode 100755 index 000000000..6099acbb5 --- /dev/null +++ b/src/login/Makefile @@ -0,0 +1,15 @@ +i18n_all: i18n_tpl i18n_js i18n_mo + +# make messages of python file and django template file to django.po +i18n_tpl: + django-admin.py makemessages -d django -l en -e html,part -e py + django-admin.py makemessages -d django -l zh_Hans -e html,part -e py + +# make messages of javascript file and django template file to djangojs.po +i18n_js: + django-admin.py makemessages -d djangojs -l en + django-admin.py makemessages -d djangojs -l zh_Hans + +# compile django.po and djangojs.po to django.mo and djangojs.mo +i18n_mo: + django-admin.py compilemessages diff --git a/src/login/RELEASE.md b/src/login/RELEASE.md new file mode 100644 index 000000000..feb525432 --- /dev/null +++ b/src/login/RELEASE.md @@ -0,0 +1,8 @@ +# Changelog + +## [Version: 1.0.0] - 2021-07-30 + +- 支持容器化部署 +- 日志输出到标准输出中,方便采集 +- 删除 oauth 登录功能 + diff --git a/src/login/__init__.py b/src/login/__init__.py new file mode 100755 index 000000000..1c6763228 --- /dev/null +++ b/src/login/__init__.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS +Community Edition) available. +Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. +Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. +You may obtain a copy of the License at http://opensource.org/licenses/MIT +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. +""" diff --git a/src/login/bin/start.sh b/src/login/bin/start.sh new file mode 100755 index 000000000..7c7bfb13b --- /dev/null +++ b/src/login/bin/start.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +python manage.py collectstatic +python manage.py compilemessages + +command="gunicorn wsgi -w 16 --timeout 150 -b 0.0.0.0: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=-" + +## Run! +exec bash -c "$command" diff --git a/src/login/bklogin/__init__.py b/src/login/bklogin/__init__.py new file mode 100644 index 000000000..1c6763228 --- /dev/null +++ b/src/login/bklogin/__init__.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS +Community Edition) available. +Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. +Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. +You may obtain a copy of the License at http://opensource.org/licenses/MIT +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. +""" diff --git a/src/login/bklogin/api/__init__.py b/src/login/bklogin/api/__init__.py new file mode 100755 index 000000000..1c6763228 --- /dev/null +++ b/src/login/bklogin/api/__init__.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS +Community Edition) available. +Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. +Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. +You may obtain a copy of the License at http://opensource.org/licenses/MIT +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. +""" diff --git a/src/login/bklogin/api/constants.py b/src/login/bklogin/api/constants.py new file mode 100755 index 000000000..dc181bb16 --- /dev/null +++ b/src/login/bklogin/api/constants.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS +Community Edition) available. +Copyright (C) 2017-2018 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 bklogin.common.constants import enum + +ApiErrorCodeEnum = enum( + SUCCESS="00", + PARAM_NOT_VALID="1200", + USER_NOT_EXISTS="1201", + # 做兼容 + USER_NOT_EXISTS2="1300", + USER_INFO_UPDATE_FAIL="1202", +) + +ApiErrorCodeEnumV2 = enum( + SUCCESS=0, + PARAM_NOT_VALID=1302100, + USER_NOT_EXISTS=1302101, + USER_INFO_UPDATE_FAIL=1302102, + USER_NOT_EXISTS2=1302103, +) + +ApiErrorCodeEnumV3 = enum( + SUCCESS=0, + PARAM_NOT_VALID=1302100, + USER_NOT_EXISTS=1302101, + USER_INFO_UPDATE_FAIL=1302102, + USER_NOT_EXISTS2=1302103, + RESOUCE_OWNER_MISMATCH=1302200, +) diff --git a/src/login/bklogin/api/utils.py b/src/login/bklogin/api/utils.py new file mode 100755 index 000000000..23e509b9f --- /dev/null +++ b/src/login/bklogin/api/utils.py @@ -0,0 +1,106 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS +Community Edition) available. +Copyright (C) 2017-2018 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 bklogin.api.constants import ApiErrorCodeEnum, ApiErrorCodeEnumV2, ApiErrorCodeEnumV3 +from django.conf import settings +from django.http import JsonResponse + + +def is_request_from_esb(request): + """ + 请求是否来自ESB + """ + x_app_token = request.META.get("HTTP_X_APP_TOKEN") + x_app_code = request.META.get("HTTP_X_APP_CODE") + if x_app_code == "esb" and x_app_token == settings.ESB_TOKEN: + return True + return False + + +######## +# v1 # +######## + + +class APIV1BaseJsonResponse(JsonResponse): + def __init__(self, result, code, message, data=None): + data = data if data is not None else {} + json_data = {"result": result, "code": code, "message": message, "data": data} + super(APIV1BaseJsonResponse, self).__init__(json_data) + + +class APIV1FailJsonResponse(APIV1BaseJsonResponse): + def __init__(self, message, **kwargs): + code = kwargs.get("code") or ApiErrorCodeEnum.PARAM_NOT_VALID + data = kwargs.get("data") + super(APIV1FailJsonResponse, self).__init__(False, code, message, data=data) + + +class APIV1OKJsonResponse(APIV1BaseJsonResponse): + def __init__(self, message, **kwargs): + data = kwargs.get("data") + super(APIV1OKJsonResponse, self).__init__(True, ApiErrorCodeEnum.SUCCESS, message, data=data) + + +######## +# v2 # +######## + + +class APIV2BaseJsonResponse(JsonResponse): + def __init__(self, result, code, message, data=None): + data = data if data is not None else {} + json_data = {"result": result, "bk_error_code": code, "bk_error_msg": message, "data": data} + super(APIV2BaseJsonResponse, self).__init__(json_data) + + +class APIV2FailJsonResponse(APIV2BaseJsonResponse): + def __init__(self, message, **kwargs): + code = kwargs.get("code") or ApiErrorCodeEnumV2.PARAM_NOT_VALID + data = kwargs.get("data") + super(APIV2FailJsonResponse, self).__init__(False, code, message, data=data) + + +class APIV2OKJsonResponse(APIV2BaseJsonResponse): + def __init__(self, message, **kwargs): + data = kwargs.get("data") + super(APIV2OKJsonResponse, self).__init__(True, ApiErrorCodeEnumV2.SUCCESS, message, data=data) + + +######## +# v3 # +######## +# result/code/message/data +# code is int + + +class APIV3BaseJsonResponse(JsonResponse): + def __init__(self, result, code, message, data=None): + data = data if data is not None else {} + json_data = {"result": result, "code": code, "message": message, "data": data} + super(APIV3BaseJsonResponse, self).__init__(json_data) + + +class APIV3FailJsonResponse(APIV3BaseJsonResponse): + def __init__(self, message, **kwargs): + code = kwargs.get("code") or ApiErrorCodeEnumV3.PARAM_NOT_VALID + data = kwargs.get("data") + super(APIV3FailJsonResponse, self).__init__(False, code, message, data=data) + + +class APIV3OKJsonResponse(APIV3BaseJsonResponse): + def __init__(self, message, **kwargs): + data = kwargs.get("data") + super(APIV3OKJsonResponse, self).__init__(True, ApiErrorCodeEnumV3.SUCCESS, message, data=data) diff --git a/src/login/bklogin/api/views.py b/src/login/bklogin/api/views.py new file mode 100755 index 000000000..11adb3dce --- /dev/null +++ b/src/login/bklogin/api/views.py @@ -0,0 +1,138 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS +Community Edition) available. +Copyright (C) 2017-2018 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 bklogin.api.constants import ApiErrorCodeEnum, ApiErrorCodeEnumV2, ApiErrorCodeEnumV3 +from bklogin.api.utils import ( + APIV1FailJsonResponse, + APIV1OKJsonResponse, + APIV2FailJsonResponse, + APIV2OKJsonResponse, + APIV3FailJsonResponse, + APIV3OKJsonResponse, + is_request_from_esb, +) +from bklogin.bkauth.utils import validate_bk_token +from bklogin.common import usermgr +from bklogin.common.mixins.exempt import LoginExemptMixin +from django.utils.translation import ugettext as _ +from django.views.generic import View + +######## +# v1 # +######## + + +class CheckLoginView(LoginExemptMixin, View): + def get(self, request): + # 验证Token参数 + is_valid, username, message = validate_bk_token(request.GET) + if not is_valid: + return APIV1FailJsonResponse(message, code=ApiErrorCodeEnum.PARAM_NOT_VALID) + return APIV1OKJsonResponse(_("用户验证成功"), data={"username": username}) + + +class UserView(LoginExemptMixin, View): + def get(self, request): + """ + 获取用户信息API + """ + # 验证Token参数 + is_valid, username, message = validate_bk_token(request.GET) + if not is_valid: + # 如果是ESB的请求,可以直接从参数中获取用户id + is_from_esb = is_request_from_esb(request) + username = request.GET.get("username") + if not is_from_esb or not username: + return APIV1FailJsonResponse(message, code=ApiErrorCodeEnum.PARAM_NOT_VALID) + + # 获取用户数据 + ok, message, data = usermgr.get_user(username) + if not ok: + return APIV1FailJsonResponse(message, code=ApiErrorCodeEnum.USER_NOT_EXISTS2) + + return APIV1OKJsonResponse(_("用户信息获取成功"), data=data) + + +######## +# v2 # +######## + + +class CheckLoginViewV2(LoginExemptMixin, View): + def get(self, request): + # 验证Token参数 + is_valid, username, message = validate_bk_token(request.GET) + if not is_valid: + return APIV2FailJsonResponse(message, code=ApiErrorCodeEnumV2.PARAM_NOT_VALID) + return APIV2OKJsonResponse(_("用户验证成功"), data={"bk_username": username}) + + +class UserViewV2(LoginExemptMixin, View): + def get(self, request): + """ + 获取用户信息API + """ + # 验证Token参数 + is_valid, username, message = validate_bk_token(request.GET) + if not is_valid: + # 如果是ESB的请求,可以直接从参数中获取用户id + is_from_esb = is_request_from_esb(request) + username = request.GET.get("bk_username") + if not is_from_esb or not username: + return APIV2FailJsonResponse(message, code=ApiErrorCodeEnumV2.PARAM_NOT_VALID) + + # 获取用户数据 + ok, message, data = usermgr.get_user(username, "v2") + if not ok: + return APIV2FailJsonResponse(message, code=ApiErrorCodeEnumV2.USER_NOT_EXISTS2) + + return APIV2OKJsonResponse(_("用户信息获取成功"), data=data) + + +######## +# v3 # +######## + + +class CheckLoginViewV3(LoginExemptMixin, View): + def get(self, request): + # 验证Token参数 + is_valid, username, message = validate_bk_token(request.GET) + if not is_valid: + return APIV3FailJsonResponse(message, code=ApiErrorCodeEnumV3.PARAM_NOT_VALID) + return APIV3OKJsonResponse(_("用户验证成功"), data={"bk_username": username}) + + +class UserViewV3(LoginExemptMixin, View): + def get(self, request): + """ + 获取用户信息API + v3, 直接返回usermgr返回的内容不做字段转换 + """ + # 验证Token参数 + is_valid, username, message = validate_bk_token(request.GET) + if not is_valid: + # 如果是ESB的请求,可以直接从参数中获取用户id + is_from_esb = is_request_from_esb(request) + username = request.GET.get("bk_username") + if not is_from_esb or not username: + return APIV3FailJsonResponse(message, code=ApiErrorCodeEnumV3.PARAM_NOT_VALID) + + # 获取用户数据 + ok, message, data = usermgr.get_user(username, "v3") + if not ok: + return APIV3FailJsonResponse(message, code=ApiErrorCodeEnumV3.USER_NOT_EXISTS2) + + return APIV3OKJsonResponse(_("用户信息获取成功"), data=data) diff --git a/src/login/bklogin/backends/__init__.py b/src/login/bklogin/backends/__init__.py new file mode 100755 index 000000000..1c6763228 --- /dev/null +++ b/src/login/bklogin/backends/__init__.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS +Community Edition) available. +Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. +Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. +You may obtain a copy of the License at http://opensource.org/licenses/MIT +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. +""" diff --git a/src/login/bklogin/backends/bk.py b/src/login/bklogin/backends/bk.py new file mode 100755 index 000000000..95a80275d --- /dev/null +++ b/src/login/bklogin/backends/bk.py @@ -0,0 +1,92 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS +Community Edition) available. +Copyright (C) 2017-2018 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 bklogin.common.exceptions import AuthenticationError, PasswordNeedReset +from bklogin.common.usermgr import get_categories_str +from bklogin.components import usermgr_api +from blue_krill.data_types.enum import StructuredEnum +from django.contrib.auth import get_user_model +from django.contrib.auth.backends import ModelBackend +from django.core.exceptions import ObjectDoesNotExist + + +def _split_username(username): + """ + admin => ("admin", "") + admin@123.com => ("admin", "123.com") + admin@123.com@456.com => ("admin@123.com", "145.com") + """ + if "@" not in username: + return username, "" + parts = username.split("@") + length = len(parts) + if length == 2: + return parts[0], parts[1] + return "@".join(parts[: length - 1]), parts[length - 1] + + +class BkUserCheckCode(int, StructuredEnum): + """Bk user check code, defined by api module""" + + # TODO: move into global code + USER_DOES_NOT_EXIST = 3210010 + TOO_MANY_TRY = 3210011 + USERNAME_FORMAT_ERROR = 3210012 + PASSWORD_ERROR = 3210013 + USER_EXIST_MANY = 3210014 + USER_IS_LOCKED = 3210015 + USER_IS_DISABLED = 3210016 + DOMAIN_UNKNOWN = 3210017 + PASSWORD_EXPIRED = 3210018 + CATEGORY_NOT_ENABLED = 3210019 + ERROR_FORMAT = 3210020 + SHOULD_CHANGE_INITIAL_PASSWORD = 3210021 + + +class BkUserBackend(ModelBackend): + """ + 蓝鲸用户管理提供的认证 + """ + + def authenticate(self, request, username=None, password=None, **kwargs): + # NOTE: username here maybe: username/phone/email + if not username or not password: + return None + + domain_list = get_categories_str().split(";") + + s_username, s_domain = _split_username(username) + if s_domain in domain_list: + username, domain = s_username, s_domain + else: + domain = "" + + # 调用用户管理接口进行验证 + ok, code, message, extra_values = usermgr_api.authenticate( + username, password, language=kwargs.get("language"), domain=domain + ) + + # 认证不通过 + 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")) + raise AuthenticationError(message=message, redirect_to=extra_values.get("redirect_to")) + + # set the username to real username + username = extra_values.get("username", username) + user_model = get_user_model() + try: + user = user_model.objects.get(username=username) + except ObjectDoesNotExist: + user = user_model.objects.create_user(username=username) + + user.fill_with_userinfo(extra_values) + return user diff --git a/src/login/bklogin/bk_i18n/__init__.py b/src/login/bklogin/bk_i18n/__init__.py new file mode 100755 index 000000000..7ee703375 --- /dev/null +++ b/src/login/bklogin/bk_i18n/__init__.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS +Community Edition) available. +Copyright (C) 2017-2018 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 + +default_app_config = "bklogin.bk_i18n.apps.BkI18nAppConfig" diff --git a/src/login/bklogin/bk_i18n/apps.py b/src/login/bklogin/bk_i18n/apps.py new file mode 100755 index 000000000..7195c98e7 --- /dev/null +++ b/src/login/bklogin/bk_i18n/apps.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS +Community Edition) available. +Copyright (C) 2017-2018 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 django.apps import AppConfig + + +class BkI18nAppConfig(AppConfig): + name = "bklogin.bk_i18n" + + def ready(self): + import bklogin.bk_i18n.signal_receivers # noqa diff --git a/src/login/bklogin/bk_i18n/constants.py b/src/login/bklogin/bk_i18n/constants.py new file mode 100755 index 000000000..4b70a8b42 --- /dev/null +++ b/src/login/bklogin/bk_i18n/constants.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS +Community Edition) available. +Copyright (C) 2017-2018 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 bklogin.common.constants import enum + +LanguageEnum = enum(ZH_CN="zh-cn", EN="en") + +DJANGO_LANG_TO_BK_LANG = {"zh-hans": LanguageEnum.ZH_CN, "en": LanguageEnum.EN} + +BK_LANG_TO_DJANGO_LANG = {v: k for k, v in DJANGO_LANG_TO_BK_LANG.items()} + +# note: Add synchronization when add login api +LOGIN_API_URL_SUFFIX_LIST = [ + "is_login", + "get_user", + "get_all_user", + "get_batch_user", +] diff --git a/src/login/bklogin/bk_i18n/middlewares.py b/src/login/bklogin/bk_i18n/middlewares.py new file mode 100755 index 000000000..88c83864f --- /dev/null +++ b/src/login/bklogin/bk_i18n/middlewares.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS +Community Edition) available. +Copyright (C) 2017-2018 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 + +import pytz +from bklogin.bk_i18n.constants import LOGIN_API_URL_SUFFIX_LIST +from django.conf import settings +from django.utils import timezone, translation +from django.utils.deprecation import MiddlewareMixin +from django.utils.translation import trans_real as trans + + +class TimezoneMiddleware(MiddlewareMixin): + def process_request(self, request): + tzname = request.session.get(settings.TIMEZONE_SESSION_KEY) + if tzname: + timezone.activate(pytz.timezone(tzname)) + else: + timezone.deactivate() + + +class LanguageMiddleware(MiddlewareMixin): + def process_request(self, request): + language = request.session.get(translation.LANGUAGE_SESSION_KEY) + if language: + translation.activate(language) + request.LANGUAGE_CODE = translation.get_language() + + +class ApiLanguageMiddleware(MiddlewareMixin): + def process_request(self, request): + # check api url + full_path = request.get_full_path() + is_api_url = False + for i in LOGIN_API_URL_SUFFIX_LIST: + if full_path.startswith("/accounts/" + i + "/") or full_path.startswith("/login/accounts/" + i + "/"): + is_api_url = True + break + # only api url do + if is_api_url: + try: + language = request.META.get("HTTP_BLUEKING_LANGUAGE", "en") + language = trans.get_supported_language_variant(language) + except Exception: + language = "en" + if language: + translation.activate(language) + request.LANGUAGE_CODE = translation.get_language() diff --git a/src/login/bklogin/bk_i18n/migrations/__init__.py b/src/login/bklogin/bk_i18n/migrations/__init__.py new file mode 100755 index 000000000..27e830ad6 --- /dev/null +++ b/src/login/bklogin/bk_i18n/migrations/__init__.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS +Community Edition) available. +Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. +Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. +You may obtain a copy of the License at http://opensource.org/licenses/MIT +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. +""" + diff --git a/src/login/bklogin/bk_i18n/signal_receivers.py b/src/login/bklogin/bk_i18n/signal_receivers.py new file mode 100755 index 000000000..55015ca23 --- /dev/null +++ b/src/login/bklogin/bk_i18n/signal_receivers.py @@ -0,0 +1,89 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS +Community Edition) available. +Copyright (C) 2017-2018 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 bklogin.bk_i18n.constants import BK_LANG_TO_DJANGO_LANG, DJANGO_LANG_TO_BK_LANG +from bklogin.components.usermgr_api import upsert_user +from django.conf import settings +from django.contrib.auth.signals import user_logged_in +from django.dispatch import receiver +from django.utils import translation +from django.utils.translation.trans_real import ( + check_for_language, + get_languages, + get_supported_language_variant, + language_code_re, + parse_accept_lang_header, +) + + +def _get_language_from_request(request, user): + """从请求中获取需要同步到用户个人信息的语言""" + supported_lang_codes = get_languages() + # session 有language,说明在登录页面有进行修改或设置,则需要同步到用户个人信息中 + lang_code = request.session.get(translation.LANGUAGE_SESSION_KEY) + if lang_code in supported_lang_codes and lang_code is not None and check_for_language(lang_code): + return lang_code + + # 个人信息中已有language + if user.language: + return None + + # session 情况不满足同步到用户个人信息,且目前个人信息中无language设置 + # 查询header头 + accept = request.META.get("HTTP_ACCEPT_LANGUAGE", "") + for accept_lang, unused in parse_accept_lang_header(accept): + if accept_lang == "*": + break + + if not language_code_re.search(accept_lang): + continue + + try: + return get_supported_language_variant(accept_lang) + except LookupError: + continue + + # 使用settings默认设置 + try: + return get_supported_language_variant(settings.LANGUAGE_CODE) + except LookupError: + return settings.LANGUAGE_CODE + + +@receiver(user_logged_in, dispatch_uid="update_user_i18n_info") +def update_user_i18n_info(sender, request, user, *args, **kwargs): + """登录后自动刷新用户语言等国际化所需信息""" + time_zone = user.time_zone + if not time_zone: + # 默认使用settings中配置 + time_zone = settings.TIME_ZONE + # sync time_zone to usermgr + upsert_user(username=user.username, time_zone=time_zone) + + # 设置language + lang_code = _get_language_from_request(request, user) + bk_lang_code = user.language + if lang_code: + # 蓝鲸约定的语言代号与Django的有不同,需要进行转换 + bk_lang_code = DJANGO_LANG_TO_BK_LANG[lang_code] + # sync language to usermgr + upsert_user(username=user.username, language=bk_lang_code) + request.user.language = bk_lang_code + + lang_code = BK_LANG_TO_DJANGO_LANG[bk_lang_code] + # set session for render html when logged in not redirect + request.session[translation.LANGUAGE_SESSION_KEY] = lang_code + translation.activate(lang_code) + request.LANGUAGE_CODE = translation.get_language() + request.session[settings.TIMEZONE_SESSION_KEY] = time_zone diff --git a/src/login/bklogin/bkaccount/__init__.py b/src/login/bklogin/bkaccount/__init__.py new file mode 100755 index 000000000..1c6763228 --- /dev/null +++ b/src/login/bklogin/bkaccount/__init__.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS +Community Edition) available. +Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. +Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. +You may obtain a copy of the License at http://opensource.org/licenses/MIT +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. +""" diff --git a/src/login/bklogin/bkaccount/admin.py b/src/login/bklogin/bkaccount/admin.py new file mode 100755 index 000000000..a2ec10d95 --- /dev/null +++ b/src/login/bklogin/bkaccount/admin.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS +Community Edition) available. +Copyright (C) 2017-2018 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 bklogin.bkaccount.models import LoginLog +from django.contrib import admin + + +class LoginLogAdmin(admin.ModelAdmin): + """ + The forms to add and change login log instances. + + The fields to be used in displaying the LoginLog model. + """ + + list_display = ["username", "login_time", "login_browser", "login_ip", "login_host", "app_id"] + search_fields = ["username"] + list_filter = ["app_id"] + + +admin.site.register(LoginLog, LoginLogAdmin) diff --git a/src/login/bklogin/bkaccount/manager.py b/src/login/bklogin/bkaccount/manager.py new file mode 100755 index 000000000..6c5cb2565 --- /dev/null +++ b/src/login/bklogin/bkaccount/manager.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS +Community Edition) available. +Copyright (C) 2017-2018 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 django.db import models +from django.utils import timezone +from django.utils.translation import ugettext as _ + + +class LoginLogManager(models.Manager): + """ + User login log manager + """ + + def record_login(self, _username, _login_browser, _login_ip, host, app_id): + try: + self.model( + username=_username, + login_browser=_login_browser, + login_ip=_login_ip, + login_host=host, + login_time=timezone.now(), + app_id=app_id, + ).save() + return (True, _("记录成功")) + except Exception: + return (False, _("用户登录记录失败")) diff --git a/src/login/bklogin/bkaccount/migrations/0001_initial.py b/src/login/bklogin/bkaccount/migrations/0001_initial.py new file mode 100755 index 000000000..f98872c22 --- /dev/null +++ b/src/login/bklogin/bkaccount/migrations/0001_initial.py @@ -0,0 +1,86 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS +Community Edition) available. +Copyright (C) 2017-2018 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 django.db import models, migrations +import django.utils.timezone +from django.conf import settings + + +class Migration(migrations.Migration): + + dependencies = [ + ("auth", "0006_require_contenttypes_0002"), + ] + + operations = [ + migrations.CreateModel( + name="BkToken", + fields=[ + ("id", models.AutoField(verbose_name="ID", serialize=False, auto_created=True, primary_key=True)), + ( + "token", + models.CharField( + unique=True, max_length=255, verbose_name="\u767b\u5f55\u7968\u636e", db_index=True + ), + ), + ( + "is_logout", + models.BooleanField( + default=False, + verbose_name="\u7968\u636e\u662f\u5426\u5df2\u7ecf\u6267\u884c\u8fc7\u9000\u51fa\u767b\u5f55\u64cd\u4f5c", + ), + ), + ], + options={ + "db_table": "login_bktoken", + "verbose_name": "\u767b\u5f55\u7968\u636e", + "verbose_name_plural": "\u767b\u5f55\u7968\u636e", + }, + ), + migrations.CreateModel( + name="LoginLog", + fields=[ + ("id", models.AutoField(verbose_name="ID", serialize=False, auto_created=True, primary_key=True)), + ("login_time", models.DateTimeField(verbose_name="\u767b\u5f55\u65f6\u95f4")), + ( + "login_browser", + models.CharField( + max_length=200, null=True, verbose_name="\u767b\u5f55\u6d4f\u89c8\u5668", blank=True + ), + ), + ( + "login_ip", + models.CharField(max_length=50, null=True, verbose_name="\u7528\u6237\u767b\u5f55ip", blank=True), + ), + ( + "login_host", + models.CharField(max_length=100, null=True, verbose_name="\u767b\u5f55HOST", blank=True), + ), + ("app_id", models.CharField(max_length=30, null=True, verbose_name=b"APP_ID", blank=True)), + ( + "user", + models.ForeignKey( + verbose_name="\u7528\u6237", + to=settings.AUTH_USER_MODEL, + on_delete=django.db.models.deletion.CASCADE, + ), + ), + ], + options={ + "db_table": "login_bklog", + "verbose_name": "\u7528\u6237\u767b\u5f55\u65e5\u5fd7", + "verbose_name_plural": "\u7528\u6237\u767b\u5f55\u65e5\u5fd7", + }, + ), + ] diff --git a/src/login/bklogin/bkaccount/migrations/0002_initial_user_data.py b/src/login/bklogin/bkaccount/migrations/0002_initial_user_data.py new file mode 100755 index 000000000..e53413bee --- /dev/null +++ b/src/login/bklogin/bkaccount/migrations/0002_initial_user_data.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS +Community Edition) available. +Copyright (C) 2017-2018 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 django.db import migrations +from django.conf import settings +from django.contrib.auth import get_user_model + + +def initial_user_data(apps, schema_editor): + pass + + +class Migration(migrations.Migration): + + dependencies = [ + ('bkaccount', '0001_initial'), + ] + + operations = [ + migrations.RunPython(initial_user_data), + ] diff --git a/src/login/bklogin/bkaccount/migrations/0003_bktoken_inactive_expire_time.py b/src/login/bklogin/bkaccount/migrations/0003_bktoken_inactive_expire_time.py new file mode 100755 index 000000000..948f76a9e --- /dev/null +++ b/src/login/bklogin/bkaccount/migrations/0003_bktoken_inactive_expire_time.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS +Community Edition) available. +Copyright (C) 2017-2018 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 django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('bkaccount', '0002_initial_user_data'), + ] + + operations = [ + migrations.AddField( + model_name='bktoken', + name='inactive_expire_time', + field=models.IntegerField(default=0, verbose_name='\u65e0\u64cd\u4f5c\u5931\u6548\u65f6\u95f4\u6233'), + ), + ] diff --git a/src/login/bklogin/bkaccount/migrations/0004_auto_20170621_0929.py b/src/login/bklogin/bkaccount/migrations/0004_auto_20170621_0929.py new file mode 100755 index 000000000..345773c7b --- /dev/null +++ b/src/login/bklogin/bkaccount/migrations/0004_auto_20170621_0929.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS +Community Edition) available. +Copyright (C) 2017-2018 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 django.db import migrations, models +import django.utils.timezone +from django.conf import settings + + +class Migration(migrations.Migration): + + dependencies = [ + ('bkaccount', '0003_bktoken_inactive_expire_time'), + ] + + operations = [ + ] diff --git a/src/login/bklogin/bkaccount/migrations/0005_initial_role.py b/src/login/bklogin/bkaccount/migrations/0005_initial_role.py new file mode 100755 index 000000000..cc517b2e5 --- /dev/null +++ b/src/login/bklogin/bkaccount/migrations/0005_initial_role.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS +Community Edition) available. +Copyright (C) 2017-2018 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 django.db import migrations + + +def load_data(apps, schema_editor): + """ + 初始化 用户角色 + """ + pass + + +class Migration(migrations.Migration): + + dependencies = [ + ('bkaccount', '0004_auto_20170621_0929'), + ] + + operations = [ + migrations.RunPython(load_data) + ] diff --git a/src/login/bklogin/bkaccount/migrations/0006_initial_bkuser_role.py b/src/login/bklogin/bkaccount/migrations/0006_initial_bkuser_role.py new file mode 100755 index 000000000..3eb3ac8b6 --- /dev/null +++ b/src/login/bklogin/bkaccount/migrations/0006_initial_bkuser_role.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS +Community Edition) available. +Copyright (C) 2017-2018 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 django.db import migrations + + +def load_data(apps, schema_editor): + """ + 初始化已存在的用户的角色 + """ + pass + + +class Migration(migrations.Migration): + + dependencies = [ + ('bkaccount', '0005_initial_role'), + ] + + operations = [ + migrations.RunPython(load_data) + ] diff --git a/src/login/bklogin/bkaccount/migrations/0007_userinfo.py b/src/login/bklogin/bkaccount/migrations/0007_userinfo.py new file mode 100755 index 000000000..60b07dcf9 --- /dev/null +++ b/src/login/bklogin/bkaccount/migrations/0007_userinfo.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS +Community Edition) available. +Copyright (C) 2017-2018 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 django.db import migrations, models +from django.conf import settings + + +class Migration(migrations.Migration): + + dependencies = [ + ('bkaccount', '0006_initial_bkuser_role'), + ] + + operations = [ + ] diff --git a/src/login/bklogin/bkaccount/migrations/0008_auto_20171116_2026.py b/src/login/bklogin/bkaccount/migrations/0008_auto_20171116_2026.py new file mode 100755 index 000000000..f37100899 --- /dev/null +++ b/src/login/bklogin/bkaccount/migrations/0008_auto_20171116_2026.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS +Community Edition) available. +Copyright (C) 2017-2018 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 django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('bkaccount', '0007_userinfo'), + ] + + operations = [ + ] diff --git a/src/login/bklogin/bkaccount/migrations/0009_add_role_data.py b/src/login/bklogin/bkaccount/migrations/0009_add_role_data.py new file mode 100755 index 000000000..57d169187 --- /dev/null +++ b/src/login/bklogin/bkaccount/migrations/0009_add_role_data.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS +Community Edition) available. +Copyright (C) 2017-2018 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 django.db import migrations + + +def load_data(apps, schema_editor): + """ + 新增 用户角色 + """ + pass + + +class Migration(migrations.Migration): + + dependencies = [ + ('bkaccount', '0008_auto_20171116_2026'), + ] + + operations = [ + ] diff --git a/src/login/bklogin/bkaccount/migrations/0010_auto_20190704_1106.py b/src/login/bklogin/bkaccount/migrations/0010_auto_20190704_1106.py new file mode 100755 index 000000000..869d9de84 --- /dev/null +++ b/src/login/bklogin/bkaccount/migrations/0010_auto_20190704_1106.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS +Community Edition) available. +Copyright (C) 2017-2018 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 django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("bkaccount", "0009_add_role_data"), + ] + + operations = [ + migrations.RemoveField( + model_name="LoginLog", + name="user", + ), + migrations.AddField( + model_name="LoginLog", + name="username", + field=models.CharField(max_length=128, null=True, verbose_name="\u7528\u6237\u540d", blank=True), + ), + ] diff --git a/src/login/bklogin/bkaccount/migrations/__init__.py b/src/login/bklogin/bkaccount/migrations/__init__.py new file mode 100755 index 000000000..27e830ad6 --- /dev/null +++ b/src/login/bklogin/bkaccount/migrations/__init__.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS +Community Edition) available. +Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. +Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. +You may obtain a copy of the License at http://opensource.org/licenses/MIT +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. +""" + diff --git a/src/login/bklogin/bkaccount/models.py b/src/login/bklogin/bkaccount/models.py new file mode 100755 index 000000000..0fb32d3f9 --- /dev/null +++ b/src/login/bklogin/bkaccount/models.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS +Community Edition) available. +Copyright (C) 2017-2018 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 bklogin.bkaccount.manager import LoginLogManager +from django.db import models + + +class LoginLog(models.Model): + """ + User login log + """ + + username = models.CharField("用户名", max_length=128, blank=True, null=True) + login_time = models.DateTimeField("登录时间") + login_browser = models.CharField("登录浏览器", max_length=200, blank=True, null=True) + login_ip = models.CharField("用户登录ip", max_length=50, blank=True, null=True) + login_host = models.CharField("登录HOST", max_length=100, blank=True, null=True) + app_id = models.CharField("APP_ID", max_length=30, blank=True, null=True) + + objects = LoginLogManager() + + def __str__(self): + return f"{self.user.chname}({self.user.username})" + + class Meta: + db_table = "login_bklog" + verbose_name = "用户登录日志" + verbose_name_plural = "用户登录日志" + + +class BkToken(models.Model): + """ + 登录票据 + """ + + token = models.CharField("登录票据", max_length=255, unique=True, db_index=True) + # 是否已经退出登录 + is_logout = models.BooleanField("票据是否已经执行过退出登录操作", default=False) + # 无操作过期时间戳 + inactive_expire_time = models.IntegerField("无操作失效时间戳", default=0) + + def __str__(self): + return self.token + + class Meta: + db_table = "login_bktoken" + verbose_name = "登录票据" + verbose_name_plural = "登录票据" diff --git a/src/login/bklogin/bkauth/__init__.py b/src/login/bklogin/bkauth/__init__.py new file mode 100755 index 000000000..1c6763228 --- /dev/null +++ b/src/login/bklogin/bkauth/__init__.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS +Community Edition) available. +Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. +Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. +You may obtain a copy of the License at http://opensource.org/licenses/MIT +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. +""" diff --git a/src/login/bklogin/bkauth/actions.py b/src/login/bklogin/bkauth/actions.py new file mode 100755 index 000000000..bf165e571 --- /dev/null +++ b/src/login/bklogin/bkauth/actions.py @@ -0,0 +1,120 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS +Community Edition) available. +Copyright (C) 2017-2018 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 urllib.error +import urllib.parse +import urllib.request +from builtins import str + +from bklogin.bkauth.constants import REDIRECT_FIELD_NAME +from bklogin.bkauth.utils import get_bk_token, is_safe_url, record_login_log, set_bk_token_invalid +from django.conf import settings +from django.contrib.auth import login as auth_login +from django.contrib.auth.forms import AuthenticationForm +from django.http import HttpResponseRedirect +from django.template.response import TemplateResponse + +""" +actions for login success/fail +""" + + +BK_LOGIN_URL = str(settings.LOGIN_URL) +BK_COOKIE_NAME = settings.BK_COOKIE_NAME + + +def login_failed_response(request, redirect_to, app_id): + """ + 登录失败跳转,目前重定向到登录,后续可返还支持自定义的错误页面 + """ + redirect_url = BK_LOGIN_URL + query = {} + if redirect_to: + query[REDIRECT_FIELD_NAME] = redirect_to + if app_id: + query["app_id"] = app_id + + if query: + redirect_url = "%s?%s" % (BK_LOGIN_URL, urllib.parse.urlencode(query)) + response = HttpResponseRedirect(redirect_url) + response = set_bk_token_invalid(request, response) + return response + + +def login_success_response(request, user_or_form, redirect_to, app_id): + """ + 用户验证成功后,登录处理 + """ + # 判读是form还是user + if isinstance(user_or_form, AuthenticationForm): + user = user_or_form.get_user() + username = user.username + # username = user_or_form.cleaned_data.get('username', '') + else: + user = user_or_form + username = user.username + + # 检查回调URL是否安全,防钓鱼 + if not is_safe_url(url=redirect_to, host=request.get_host()): + # 调整到根目录 + redirect_to = "/console/" + + # if from logout + if redirect_to == "/logout/": + redirect_to = "/console/" + + # 设置用户登录 + auth_login(request, user) + # 记录登录日志 + record_login_log(request, username, app_id) + + secure = False + # uncomment this if you need a secure cookie; + # the http domain will not access the bk_token in secure cookie + # secure = (settings.HTTP_SCHEMA == "https") + bk_token, expire_time = get_bk_token(username) + response = HttpResponseRedirect(redirect_to) + response.set_cookie( + BK_COOKIE_NAME, bk_token, expires=expire_time, domain=settings.BK_COOKIE_DOMAIN, httponly=True, secure=secure + ) + + # set cookie for app or platform + response.set_cookie( + settings.LANGUAGE_COOKIE_NAME, + request.user.language, + # max_age=settings.LANGUAGE_COOKIE_AGE, + expires=expire_time, + path=settings.LANGUAGE_COOKIE_PATH, + domain=settings.LANGUAGE_COOKIE_DOMAIN, + ) + return response + + +def login_redirect_response(request, redirect_url, is_from_logout): + """ + 登录重定向 + """ + response = HttpResponseRedirect(redirect_url) + # 来自注销,则需清除蓝鲸bk_token + if is_from_logout: + response = set_bk_token_invalid(request, response) + return response + + +def login_license_fail_response(request, template_name="account/login.html"): + """ + 证书认证,登录失败页面 + """ + response = TemplateResponse(request, template_name, {"custom_login": True}) + response = set_bk_token_invalid(request, response) + return response diff --git a/src/login/bklogin/bkauth/constants.py b/src/login/bklogin/bkauth/constants.py new file mode 100755 index 000000000..d591817c7 --- /dev/null +++ b/src/login/bklogin/bkauth/constants.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS +Community Edition) available. +Copyright (C) 2017-2018 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. +""" + + +REDIRECT_FIELD_NAME = "c_url" diff --git a/src/login/bklogin/bkauth/decorators.py b/src/login/bklogin/bkauth/decorators.py new file mode 100755 index 000000000..9a1c2a53a --- /dev/null +++ b/src/login/bklogin/bkauth/decorators.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS +Community Edition) available. +Copyright (C) 2017-2018 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 functools import wraps + + +def login_exempt(view_func): + """ + 登录豁免,被此装饰器修饰的action可以不校验登录 + """ + + def wrapped_view(*args, **kwargs): + return view_func(*args, **kwargs) + + wrapped_view.login_exempt = True + return wraps(view_func)(wrapped_view) diff --git a/src/login/bklogin/bkauth/forms.py b/src/login/bklogin/bkauth/forms.py new file mode 100755 index 000000000..21e7ad784 --- /dev/null +++ b/src/login/bklogin/bkauth/forms.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS +Community Edition) available. +Copyright (C) 2017-2018 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 django import forms +from django.contrib.auth import authenticate +from django.contrib.auth.forms import AuthenticationForm + + +class BkAuthenticationForm(AuthenticationForm): + def clean(self): + username = self.cleaned_data.get("username") + password = self.cleaned_data.get("password") + + if username and password: + self.user_cache = authenticate( + username=username, + password=password, + language=getattr(self.request, "LANGUAGE_CODE", ""), + ) + if self.user_cache is None: + raise forms.ValidationError( + self.error_messages["invalid_login"], + code="invalid_login", + params={"username": self.username_field.verbose_name}, + ) + else: + self.confirm_login_allowed(self.user_cache) + + return self.cleaned_data diff --git a/src/login/bklogin/bkauth/manager.py b/src/login/bklogin/bkauth/manager.py new file mode 100755 index 000000000..335b047af --- /dev/null +++ b/src/login/bklogin/bkauth/manager.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS +Community Edition) available. +Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. +Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. +You may obtain a copy of the License at http://opensource.org/licenses/MIT +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. +""" + + +from django.contrib.auth.models import BaseUserManager +from django.utils import timezone + + +class BkUserManager(BaseUserManager): + """BK user manager""" + + def create_user(self, username, password=None): + """ + Create and saves a User with the given username and password + """ + if not username: + raise ValueError("'The given username must be set") + + now = timezone.now() + user = self.model(username=username, last_login=now) + user.set_password(password) + user.save(using=self._db) + + return user diff --git a/src/login/bklogin/bkauth/middlewares.py b/src/login/bklogin/bkauth/middlewares.py new file mode 100755 index 000000000..76a7f0efe --- /dev/null +++ b/src/login/bklogin/bkauth/middlewares.py @@ -0,0 +1,110 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS +Community Edition) available. +Copyright (C) 2017-2018 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 urllib.parse import urlparse + +from bklogin.bk_i18n.constants import BK_LANG_TO_DJANGO_LANG +from bklogin.bkauth.constants import REDIRECT_FIELD_NAME +from bklogin.bkauth.utils import validate_bk_token +from bklogin.common.log import logger +from django.conf import settings +from django.contrib.auth import authenticate, get_user_model +from django.contrib.auth.models import AnonymousUser +from django.contrib.auth.views import redirect_to_login +from django.http import HttpResponse +from django.shortcuts import resolve_url +from django.utils import translation +from django.utils.deprecation import MiddlewareMixin + +BK_LOGIN_URL = str(settings.LOGIN_URL) + + +def redirect_login(request): + """ + 重定向到登录页面. + + 登录态验证不通过时调用 + """ + if request.is_ajax(): + return HttpResponse(status=401) + + path = request.build_absolute_uri() + resolved_login_url = resolve_url(BK_LOGIN_URL) + # If the login url is the same scheme and net location then just + # use the path as the "next" url. + login_scheme, login_netloc = urlparse(resolved_login_url)[:2] + current_scheme, current_netloc = urlparse(path)[:2] + if (not login_scheme or login_scheme == current_scheme) and (not login_netloc or login_netloc == current_netloc): + path = settings.SITE_URL[:-1] + request.get_full_path() + return redirect_to_login(path, resolved_login_url, REDIRECT_FIELD_NAME) + + +class LoginMiddleware(MiddlewareMixin): + def process_request(self, request): + """设置user""" + # 静态资源不做登录态设置 + full_path = request.get_full_path() + if full_path.startswith(settings.STATIC_URL) or full_path == "/robots.txt": + return None + + # 静态资源不做登录态设置 + if full_path in [settings.SITE_URL + "i18n/setlang/", "/i18n/setlang/"]: + return None + + user = None + bk_token = request.COOKIES.get("bk_token") + + path_prefix = settings.FORCE_SCRIPT_NAME or "" + if bk_token and full_path.startswith("%s/oauth/authorize/" % path_prefix): + is_valid, username, message = validate_bk_token(request.COOKIES) + if is_valid: + try: + UserModel = get_user_model() + user = UserModel.objects.get(username=username) + user.bk_token = bk_token + except Exception: + logger.exception("get user via username=%s fail", username) + user = None + else: + user = authenticate(request=request) + if user: + # 设置timezone session + request.session[settings.TIMEZONE_SESSION_KEY] = user.time_zone + # 设置language session + request.session[translation.LANGUAGE_SESSION_KEY] = BK_LANG_TO_DJANGO_LANG[user.language] + + request.user = user or AnonymousUser() + + def process_view(self, request, view, args, kwargs): + # 静态资源不做登录态验证 + full_path = request.get_full_path() + if full_path.startswith(settings.STATIC_URL) or full_path == "/robots.txt": + return None + + # 静态资源不做登录态验证 + if full_path in [ + settings.SITE_URL + "i18n/setlang/", + "/i18n/setlang/", + settings.SITE_URL + "jsi18n/i18n/", + "/jsi18n/i18n/", + ]: + return None + + if getattr(view, "login_exempt", False): + return None + + if request.user.is_authenticated: + return None + + return redirect_login(request) diff --git a/src/login/bklogin/bkauth/migrations/0001_initial.py b/src/login/bklogin/bkauth/migrations/0001_initial.py new file mode 100644 index 000000000..3aeadbe11 --- /dev/null +++ b/src/login/bklogin/bkauth/migrations/0001_initial.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.29 on 2021-07-14 10:51 +from __future__ import unicode_literals + +import django.contrib.auth.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='User', + fields=[ + ('password', models.CharField(max_length=128, verbose_name='password')), + ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), + ('username', models.CharField(max_length=255, primary_key=True, serialize=False)), + ], + bases=(models.Model, django.contrib.auth.models.AnonymousUser), + ), + ] diff --git a/src/login/bklogin/bkauth/migrations/__init__.py b/src/login/bklogin/bkauth/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/login/bklogin/bkauth/models.py b/src/login/bklogin/bkauth/models.py new file mode 100755 index 000000000..39cbedfa1 --- /dev/null +++ b/src/login/bklogin/bkauth/models.py @@ -0,0 +1,122 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS +Community Edition) available. +Copyright (C) 2017-2018 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 builtins import object + +from bklogin.bkauth.manager import BkUserManager +from bklogin.bkauth.utils import is_bk_token_valid +from bklogin.components.usermgr_api import upsert_user +from django.contrib.auth import models +from django.db import models as db_models +from django.utils import timezone + + +class User(models.AbstractBaseUser, models.AnonymousUser): + """Blueking User Model, It's abstract and will not create table in database""" + + username = db_models.CharField(primary_key=True, max_length=255) + USERNAME_FIELD = "username" + + objects = BkUserManager() + + def __init__(self, *args, **kwargs): + self.init_fields() + + # NOTE: 兼容老版本文档中: + # user = UserModel(username, display_name="mockadmin", email="mockadmin@mock.com",) + if len(args) == 1 and isinstance(args[0], str): + args = (None, timezone.now(), args[0]) + + super(User, self).__init__(*args) + for k, v in list(kwargs.items()): + setattr(self, k, v) + + def init_fields(self): + self.time_zone = None + self.language = None + self.display_name = None + self.telephone = None + self.email = None + self.wx_id = None + self.position = None + self.role = None + self.extras = None + self.status = None + self.logo_url = None + + self.time_zone = None + self.language = None + + self.bk_token = None + self.is_superuser = False + + self.password = "" + + def fill_with_userinfo(self, userinfo): + self.username = userinfo.get("username") + self.display_name = userinfo.get("display_name") + self.telephone = userinfo.get("telephone") + self.email = userinfo.get("email") + self.wx_id = userinfo.get("wx_id") + self.position = userinfo.get("position") + self.role = userinfo.get("role") + self.extras = userinfo.get("extras") + self.status = userinfo.get("status") + self.logo_url = userinfo.get("logo_url") + self.time_zone = userinfo.get("time_zone") + self.language = userinfo.get("language") + + role = 1 if userinfo.get("role") == 1 else 0 + self.is_superuser = role == 1 + + def sync_to_usermgr(self): + """ + fields supported: + username string required, 用户名,长度:1~255 + + display_name string optional, 显示名,长度:1~255 + telephone string optional, 手机号,必须是手机号格式,11位数字 + email string optional, 邮箱,必须是邮箱格式 + position string optional, 职位 + role int optional, 角色,默认0:0 普通用户, 1 超级管理员, 2 开发者, 3 职能化用户, 4 审计员 + language string optional, 默认 zh-cn,可选 zh-cn、en + time_zone string optional, 默认 Asia/Shanghai + + """ + if not self.username: + return False, "username should be setted" + data = {} + for key in ["display_name", "telephone", "email", "position", "role", "language", "time_zone"]: + if getattr(self, key) is not None: + data[key] = getattr(self, key) + + if not data: + return False, "all the fields are None" + + ok, message, _data = upsert_user(self.username, **data) + return ok, message + + @property + def is_authenticated(self): + if not self.bk_token: + return False + ok, _ = is_bk_token_valid(self.bk_token) + return ok + + @property + def is_anonymous(self): + return not self.is_authenticated + + class Meta(object): + app_label = "bkauth" diff --git a/src/login/bklogin/bkauth/utils.py b/src/login/bklogin/bkauth/utils.py new file mode 100755 index 000000000..f97e8099d --- /dev/null +++ b/src/login/bklogin/bkauth/utils.py @@ -0,0 +1,202 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS +Community Edition) available. +Copyright (C) 2017-2018 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 + +import datetime +import time +import unicodedata +from urllib.parse import urlparse + +from bklogin.bkaccount.models import BkToken, LoginLog +from bklogin.common.encrypt import salt +from bklogin.common.log import logger +from bklogin.common.utils.basic import escape_html_return_msg +from blue_krill.encrypt.handler import EncryptHandler +from django.conf import settings +from django.utils import timezone +from django.utils.translation import ugettext as _ + +BK_COOKIE_AGE = settings.BK_COOKIE_AGE +BK_INACTIVE_COOKIE_AGE = settings.BK_INACTIVE_COOKIE_AGE + + +def get_bk_token(username): + """ + 生成用户的登录态 + """ + bk_token = "" + expire_time = int(time.time()) + # 重试5次 + retry_count = 0 + while not bk_token and retry_count < 5: + now_time = int(time.time()) + expire_time = now_time + BK_COOKIE_AGE + inactive_expire_time = now_time + BK_INACTIVE_COOKIE_AGE + plain_token = "%s|%s|%s" % (expire_time, username, salt()) + bk_token = EncryptHandler().encrypt(plain_token) + try: + # BkToken.objects.create(token=bk_token) + BkToken.objects.create(token=bk_token, inactive_expire_time=inactive_expire_time) + except Exception: + # logger.exception(u"登录票据保存失败") + logger.exception("Login ticket failed to be saved during ticket generation") + # 循环结束前将bk_token置空后重新生成 + bk_token = "" if retry_count < 4 else bk_token + retry_count += 1 + return bk_token, datetime.datetime.fromtimestamp(expire_time, timezone.get_current_timezone()) + + +def is_bk_token_valid(bk_token): # NOQA + """ + 验证用户登录态 + """ + if not bk_token: + error_msg = _("缺少参数bk_token") + return False, error_msg + + try: + plain_bk_token = EncryptHandler().decrypt(bk_token) + except Exception: + plain_bk_token = "" + # logger.exception(u"参数[%s]解析失败" % bk_token) + logger.exception("Parameter[%s] parse failed" % bk_token) + + # 参数bk_token非法 + error_msg = _("参数bk_token非法") + if not plain_bk_token: + return False, error_msg + + try: + token_info = plain_bk_token.split("|") + if not token_info or len(token_info) < 3: + return False, error_msg + except Exception: + logger.exception("split token fail: %s" % bk_token) + return False, error_msg + + try: + # is_logout = BkToken.objects.get(token=bk_token).is_logout + bktoken_obj = BkToken.objects.get(token=bk_token) + is_logout = bktoken_obj.is_logout + inactive_expire_time = bktoken_obj.inactive_expire_time + except Exception: + error_msg = _("不存在bk_token[%s]的记录") % bk_token + return False, error_msg + + expire_time = int(token_info[0]) + now_time = int(time.time()) + # token已注销 + if is_logout: + error_msg = _("登录态已注销") + return False, error_msg + # token有效期已过 + if now_time > expire_time + settings.BK_TOKEN_OFFSET_ERROR_TIME: + error_msg = _("登录态已过期") + return False, error_msg + # token有效期大于当前时间的有效期 + if expire_time - now_time > BK_COOKIE_AGE + settings.BK_TOKEN_OFFSET_ERROR_TIME: + error_msg = _("登录态有效期不合法") + return False, error_msg + + # token 无操作有效期已过, + if now_time > inactive_expire_time + settings.BK_TOKEN_OFFSET_ERROR_TIME: + error_msg = _("长时间无操作,登录态已过期") + return False, error_msg + + # 更新 无操作有效期 + try: + BkToken.objects.filter(token=bk_token).update(inactive_expire_time=now_time + BK_INACTIVE_COOKIE_AGE) + except Exception: + logger.exception("update inactive_expire_time fail") + + username = token_info[1] + return True, username + + +@escape_html_return_msg +def validate_bk_token(data): + """ + 检查bk_token的合法性,并返回用户实例 + """ + bk_token = data.get(settings.BK_COOKIE_NAME) + # 验证Token参数 + is_valid, username = is_bk_token_valid(bk_token) + if not is_valid: + return False, None, username + + # TODO: ? use usermgr get user check if user exists? + return True, username, "" + + +def set_bk_token_invalid(request, response=None): + """ + 将登录票据设置为不合法 + """ + bk_token = request.COOKIES.get(settings.BK_COOKIE_NAME, None) + if bk_token: + BkToken.objects.filter(token=bk_token).update(is_logout=True) + if response is not None: + # delete cookie + response.delete_cookie(settings.BK_COOKIE_NAME, domain=settings.BK_COOKIE_DOMAIN) + return response + return None + + +def is_safe_url(url, host=None): + """ + 判断url是否与当前host的根域一致 + + 以下情况返回False: + 1)根域不一致 + 2)url的scheme不为:https(s) + 3)url为空 + """ + if url is not None: + url = url.strip() + if not url: + return False + # Chrome treats \ completely as / + url = url.replace("\\", "/") + # Chrome considers any URL with more than two slashes to be absolute, but + # urlparse is not so flexible. Treat any url with three slashes as unsafe. + if url.startswith("///"): + return False + url_info = urlparse(url) + # Forbid URLs like http:///example.com - with a scheme, but without a hostname. + # In that URL, example.com is not the hostname but, a path component. However, + # Chrome will still consider example.com to be the hostname, so we must not + # allow this syntax. + if not url_info.netloc and url_info.scheme: + return False + # Forbid URLs that start with control characters. Some browsers (like + # Chrome) ignore quite a few control characters at the start of a + # URL and might consider the URL as scheme relative. + if unicodedata.category(url[0])[0] == "C": + return False + url_domain = url_info.netloc.split(":")[0].split(".")[-2] if url_info.netloc else "" + host_domain = host.split(":")[0].split(".")[-2] if host else "" + return (not url_info.netloc or url_domain == host_domain) and ( + not url_info.scheme or url_info.scheme in ["http", "https"] + ) + + +def record_login_log(request, username, app_id): + """ + 记录用户登录日志 + """ + host = request.get_host() + login_browser = request.META.get("HTTP_USER_AGENT", "unknown") + # 获取用户ip + login_ip = request.META.get("HTTP_X_FORWARDED_FOR", "REMOTE_ADDR") + + LoginLog.objects.record_login(username, login_browser, login_ip, host, app_id) diff --git a/src/login/bklogin/bkauth/views.py b/src/login/bklogin/bkauth/views.py new file mode 100755 index 000000000..2599a0d31 --- /dev/null +++ b/src/login/bklogin/bkauth/views.py @@ -0,0 +1,185 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS +Community Edition) available. +Copyright (C) 2017-2018 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 functools import wraps + +from bklogin.bkauth.actions import login_license_fail_response, login_success_response +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.license import check_license +from bklogin.common.mixins.exempt import LoginExemptMixin +from bklogin.common.usermgr import get_categories_str +from django.conf import settings +from django.contrib.auth import logout as auth_logout +from django.contrib.sites.shortcuts import get_current_site +from django.http import HttpResponseForbidden, HttpResponseRedirect +from django.shortcuts import render +from django.template.response import TemplateResponse +from django.utils.module_loading import import_string +from django.utils.translation import ugettext as _ +from django.views.generic import View + + +def only_plain_xframe_options_exempt(view_func): + """ + only allow /plain/ to be opened by a iframe + add some code: from django.views.decorators.clickjacking import xframe_options_exempt + """ + + def wrapped_view(*args, **kwargs): + resp = view_func(*args, **kwargs) + + if not isinstance(resp, HttpResponseRedirect): + origin_url = resp._request.META.get("HTTP_REFERER") + login_host = resp._request.get_host() + + if resp._request.path_info == "/plain/" and is_safe_url(url=origin_url, host=login_host): + resp.xframe_options_exempt = True + + return resp + + return wraps(view_func)(wrapped_view) + + +class LoginView(LoginExemptMixin, View): + """ + 登录 & 登录弹窗 + """ + + is_plain = False + + @only_plain_xframe_options_exempt + def get(self, request): + # TODO1: from django.views.decorators.clickjacking import xframe_options_exempt + # TODO2: should check if the request from the legal domain + return self._login(request) + + @only_plain_xframe_options_exempt + def post(self, request): + return self._login(request) + + def _login(self, request): + # 判断调用方式 + if settings.LOGIN_TYPE != "custom_login": + return _bk_login(request) + + if settings.EDITION == "ee": + # 校验企业正式是否有效,无效则不可登录 + is_license_ok, message, vaild_start_time, vaild_end_time = check_license() + if not is_license_ok: + return login_license_fail_response(request) + + # 调用自定义login view + custom_login_view = import_string(settings.CUSTOM_LOGIN_VIEW) + return custom_login_view(request) + + +def _bk_login(request): + """ + 登录页面和登录动作 + """ + authentication_form = BkAuthenticationForm + # NOTE: account/login.html 为支持自适应大小的模板 + template_name = "account/login.html" + forget_reset_password_url = f"{settings.BK_USERMGR_SAAS_URL}/reset_password" + token_set_password_url = "" + + redirect_to = request.POST.get(REDIRECT_FIELD_NAME, request.GET.get(REDIRECT_FIELD_NAME, "")) + # support oauth2 redirect ?next= + if not redirect_to and "next" in request.GET: + redirect_to = request.GET.get("next") + + app_id = request.POST.get("app_id", request.GET.get("app_id", "")) + + if settings.EDITION == "ee": + # 校验企业证书是否有效,无效则不可登录 + is_license_ok, message, vaild_start_time, vaild_end_time = check_license() + else: + is_license_ok = True + template_name = "account/login_ce.html" + + error_message = "" + login_redirect_to = "" + + # POST + if request.method == "POST" and is_license_ok: + form = authentication_form(request, data=request.POST) + try: + if form.is_valid(): + return login_success_response(request, form, redirect_to, app_id) + except AuthenticationError as e: + login_redirect_to = e.redirect_to + error_message = e.message + except PasswordNeedReset as e: + token_set_password_url = e.reset_password_url + error_message = e.message + else: + error_message = _("账户或者密码错误,请重新输入") + # GET + else: + form = authentication_form(request) + + # NOTE: get categories from usermgr + categories = get_categories_str() + + current_site = get_current_site(request) + context = { + "form": form, + "error_message": error_message, + REDIRECT_FIELD_NAME: redirect_to, + "site": current_site, + "site_name": current_site.name, + "app_id": app_id, + "is_license_ok": is_license_ok, + "token_set_password_url": token_set_password_url, + "forget_password_url": forget_reset_password_url, + "login_redirect_to": login_redirect_to, + "categories": categories, + "is_plain": request.path_info == "/plain/", + } + + response = TemplateResponse(request, template_name, context) + response = set_bk_token_invalid(request, response) + return response + + +class LogoutView(LoginExemptMixin, View): + """ + 登出并重定向到登录页面 + """ + + def get(self, request): + auth_logout(request) + next_page = None + + if REDIRECT_FIELD_NAME in request.POST or REDIRECT_FIELD_NAME in request.GET: + next_page = request.POST.get(REDIRECT_FIELD_NAME, request.GET.get(REDIRECT_FIELD_NAME)) + # Security check -- don't allow redirection to a different host. + if not is_safe_url(url=next_page, host=request.get_host()): + next_page = request.path + + if next_page: + # Redirect to this page until the session has been cleared. + response = HttpResponseRedirect(next_page) + else: + # Redirect to login url. + response = HttpResponseRedirect("%s?%s" % (settings.LOGIN_URL, "is_from_logout=1")) + + # 将登录票据设置为不合法 + response = set_bk_token_invalid(request, response) + return response + + +def csrf_failure(request, reason=""): + return HttpResponseForbidden(render(request, "csrf_failure.html"), content_type="text/html") diff --git a/src/login/bklogin/common/__init__.py b/src/login/bklogin/common/__init__.py new file mode 100755 index 000000000..1c6763228 --- /dev/null +++ b/src/login/bklogin/common/__init__.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS +Community Edition) available. +Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. +Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. +You may obtain a copy of the License at http://opensource.org/licenses/MIT +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. +""" diff --git a/src/login/bklogin/common/constants.py b/src/login/bklogin/common/constants.py new file mode 100755 index 000000000..05456923f --- /dev/null +++ b/src/login/bklogin/common/constants.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS +Community Edition) available. +Copyright (C) 2017-2018 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. +""" + + +def enum(**enums): + return type("Enum", (), enums) + + +DATETIME_FORMAT_STRING = "%Y-%m-%d %H:%M:%S" + +LICENSE_VAILD_CACHE_KEY = "BK_LICENSE_VALID" + + +# 用户管理与登录自身提供的用户信息字段KeyMap +USERMGR_BKLOGIN_FIELD_MAP = { + "display_name": "chname", + "telephone": "phone", + "wx_id": "wx_userid", + "email": "email", + "role": "role", + "language": "language", + "time_zone": "time_zone", + "qq": "qq", +} + +BKLOGIN_USERMGR_FIELD_MAP = {v: k for k, v in list(USERMGR_BKLOGIN_FIELD_MAP.items())} diff --git a/src/login/bklogin/common/context_processors.py b/src/login/bklogin/common/context_processors.py new file mode 100755 index 000000000..a536f6a98 --- /dev/null +++ b/src/login/bklogin/common/context_processors.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS +Community Edition) available. +Copyright (C) 2017-2018 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 urllib.parse +from builtins import str + +from django.conf import settings +from django.utils import timezone + +""" +context_processor for common(setting) +** 除setting外的其他context_processor内容,均采用组件的方式(string) +""" + + +def site_settings(request): + real_static_url = urllib.parse.urljoin(str(settings.SITE_URL), str("." + settings.STATIC_URL)) + cur_domain = request.get_host() + return { + "LOGIN_URL": settings.LOGIN_URL, + "LOGOUT_URL": settings.LOGOUT_URL, + "STATIC_URL": real_static_url, + "SITE_URL": settings.SITE_URL, + "STATIC_VERSION": settings.STATIC_VERSION, + "CUR_DOMIAN": cur_domain, + "APP_PATH": request.get_full_path(), + "NOW": timezone.now(), + "EDITION": settings.EDITION, + # 本地 js 后缀名 + "JS_SUFFIX": settings.JS_SUFFIX, + # 本地 css 后缀名 + "CSS_SUFFIX": settings.CSS_SUFFIX, + } diff --git a/src/login/bklogin/common/encrypt.py b/src/login/bklogin/common/encrypt.py new file mode 100644 index 000000000..6e670f1d0 --- /dev/null +++ b/src/login/bklogin/common/encrypt.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS +Community Edition) available. +Copyright (C) 2017-2018 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 random + + +def salt(length=8): + """ + 生成长度为length 的随机字符串 + """ + aplhabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + return "".join([random.choice(aplhabet) for _ in range(length)]) diff --git a/src/login/bklogin/common/exceptions.py b/src/login/bklogin/common/exceptions.py new file mode 100755 index 000000000..499a402e6 --- /dev/null +++ b/src/login/bklogin/common/exceptions.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS +Community Edition) available. +Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. +Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. +You may obtain a copy of the License at http://opensource.org/licenses/MIT +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. +""" + + +from typing import Optional + +from bklogin.common.constants import enum +from django.utils.translation import ugettext_lazy as _ + +LoginErrorCodes = enum( + E1302000_DEFAULT_CODE=1302000, + E1302001_BASE_SETTINGS_ERROR=1302001, + E1302002_BASE_DATABASE_ERROR=1302002, + E1302003_BASE_HTTP_DEPENDENCE_ERROR=1302003, + E1302004_BASE_BKSUITE_DATABASE_ERROR=1302004, + E1302005_BASE_LICENSE_ERROR=1302005, + # E1302006_ENTERPRISE_LOGIN_ERROR=1302006, +) + + +class AuthenticationError(Exception): + message = "login error" + redirect_to = "" + + def __init__(self, message=None, redirect_to=None): + if message is not None: + self.message = message + if redirect_to is not None: + self.redirect_to = redirect_to + + +class PasswordNeedReset(Exception): + """Auth failure due to needing reset of password""" + + def __init__(self, reset_password_url: str, message: Optional[str] = None): + self.reset_password_url = reset_password_url + self.message = message or _("登录校验失败,请重置密码") diff --git a/src/login/bklogin/common/http.py b/src/login/bklogin/common/http.py new file mode 100755 index 000000000..e603cb5a6 --- /dev/null +++ b/src/login/bklogin/common/http.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS +Community Edition) available. +Copyright (C) 2017-2018 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 + +import requests +from bklogin.common.log import logger + +""" +请求登录的http基础方法 + +Rules: +1. POST/DELETE/PUT: json in - json out, 如果resp.json报错, 则是登录接口问题 +2. GET带参数 HEAD不带参数 +3. 以统一的header头发送请求 +""" + + +def _gen_header(): + headers = { + "Content-Type": "application/json", + } + return headers + + +def _http_request(method, url, headers=None, data=None, timeout=None, verify=False, cert=None): + try: + if method == "GET": + resp = requests.get(url=url, headers=headers, params=data, timeout=timeout, verify=verify, cert=cert) + elif method == "HEAD": + resp = requests.head(url=url, headers=headers, verify=verify, cert=cert) + elif method == "POST": + resp = requests.post(url=url, headers=headers, json=data, timeout=timeout, verify=verify, cert=cert) + elif method == "DELETE": + resp = requests.delete(url=url, headers=headers, json=data, timeout=timeout, verify=verify, cert=cert) + elif method == "PUT": + resp = requests.put(url=url, headers=headers, json=data, timeout=timeout, verify=verify, cert=cert) + else: + return False, None + except requests.exceptions.RequestException: + logger.exception("http request error! method: %s, url: %s" % (method, url)) + return False, None + else: + if resp.status_code != 200: + content = resp.content[:100] if resp.content else "" + error_msg = "http request fail! method: %s, url: %s, " "response_status_code: %s, response_content: %s" + logger.error(error_msg % (method, url, resp.status_code, content)) + return False, None + + return True, resp.json() + + +def http_get(url, data, verify=False, cert=None, timeout=None): + headers = _gen_header() + return _http_request(method="GET", url=url, headers=headers, data=data, verify=verify, cert=cert, timeout=timeout) + + +def http_post(url, data, verify=False, cert=None, timeout=None): + headers = _gen_header() + return _http_request(method="POST", url=url, headers=headers, data=data, verify=verify, cert=cert, timeout=timeout) + + +def http_delete(url, data, verify=False, cert=None, timeout=None): + headers = _gen_header() + return _http_request( + method="DELETE", url=url, headers=headers, data=data, verify=verify, cert=cert, timeout=timeout + ) diff --git a/src/login/bklogin/common/license.py b/src/login/bklogin/common/license.py new file mode 100755 index 000000000..fcbff9862 --- /dev/null +++ b/src/login/bklogin/common/license.py @@ -0,0 +1,125 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS +Community Edition) available. +Copyright (C) 2017-2018 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 + +import os +from builtins import str + +from bklogin.common.constants import DATETIME_FORMAT_STRING, LICENSE_VAILD_CACHE_KEY +from bklogin.common.http import http_post +from bklogin.common.log import logger +from bklogin.common.utils.time import parse_local_datetime +from django.conf import settings +from django.core.cache import cache +from django.utils import timezone, translation +from django.utils.translation import ugettext as _ + +""" +企业证书校验等相关通用函数 +""" + + +def _validate_cert_key_file(cert_file, key_file): + """ + 校验本地cert和key文件 + """ + # 检查cert/key文件是否存在 + if not os.path.isfile(cert_file): + logger.error("The local certificate is unavailable: certificate file (platform.cert) does not exist") + return False, _("证书文件(platform.cert)不存在: %s") % cert_file, None + if not os.path.isfile(key_file): + logger.error("The local certificate is unavailable: key file (platform.key) does not exist") + return False, _("密钥文件(platform.key)不存在: %s") % key_file, None + # 读取证书文件内容 + cert_raw_string = None + with open(cert_file) as f: + cert_raw_string = f.read() + if not cert_raw_string: + msg = "The local certificate is unavailable: certificate file (platform.cert) is empty or has been damaged" + logger.error(msg) + return False, _("证书文件(platform.cert)为空或已被损坏"), None + return True, "", cert_raw_string + + +def _validate_remote_license(cert_server_url, cert_file, key_file, cert_raw_string): + """ + 请求证书服务器校验证书 + """ + param = { + "certificate": cert_raw_string, + "platform": "open_paas", + "requesttime": timezone.now().strftime(DATETIME_FORMAT_STRING), + } + + ok, data = http_post(cert_server_url, param, verify=False, cert=(cert_file, key_file)) + # do retry + retry_count = 0 + while (not ok) and retry_count < 3: + logger.info("validate remote license http post failed! retry %s" % (retry_count + 1)) + ok, data = http_post(cert_server_url, param, verify=False, cert=(cert_file, key_file), timeout=30) + retry_count += 1 + + if not ok: + return False, "request license_server error", _("license_server请求校验证书异常"), None, None + + if data["result"]: + return False, data["message"], data["message_cn"], None, None + return True, "", "", data["validstarttime"], data["validendtime"] + + +def check_license(): + """ + 检查企业正式是否有效 + :return: True/False, message, + """ + # 本地测试环境需要 + # if settings.ENVIRONMENT in ['development']: + # valid_start_time = parse_local_datetime('2017-05-05 12:00:00') + # valid_end_time = parse_local_datetime('2017-09-01 00:00:00') + # return True, u"证书校验成功", valid_start_time, valid_end_time + + client_cert_file_path = str(settings.CLIENT_CERT_FILE_PATH) + client_key_file_path = str(settings.CLIENT_KEY_FILE_PATH) + certificate_server_url = str(settings.CERTIFICATE_SERVER_URL) + + # 本地证书文件检查 + is_valid, message, cert_raw_string = _validate_cert_key_file(client_cert_file_path, client_key_file_path) + if not is_valid: + return False, message, None, None + + # 远程检查证书 + # 先从缓存中获取 + remote_license_result = cache.get(LICENSE_VAILD_CACHE_KEY) + if not remote_license_result: + remote_license_result = _validate_remote_license( + certificate_server_url, client_cert_file_path, client_key_file_path, cert_raw_string + ) + # 设置缓存 + cache.set(LICENSE_VAILD_CACHE_KEY, remote_license_result) + + is_valid, message, message_cn, valid_start_time, valid_end_time = remote_license_result + + if not is_valid: + logger.error(message) + # TODO to write a function for selecting data of current lageuage + error_message = message_cn if translation.get_language() in ["zh-hans"] else message + return False, error_message, None, None + + try: + # 时间转换 + valid_start_time = parse_local_datetime(valid_start_time, zone=timezone.utc) + valid_end_time = parse_local_datetime(valid_end_time, zone=timezone.utc) + except Exception as error: + logger.exception("An error occurred while checking enterprise certificate conversion time:%s" % error) + return False, _("证书不可用,请求未返回有效期或返回格式有误"), None, None + return True, _("证书校验成功"), valid_start_time, valid_end_time diff --git a/src/login/bklogin/common/log.py b/src/login/bklogin/common/log.py new file mode 100755 index 000000000..0cce1b94b --- /dev/null +++ b/src/login/bklogin/common/log.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS +Community Edition) available. +Copyright (C) 2017-2018 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 + +import logging + +logger = logging.getLogger("root") + +""" +Usage: + + from common.log import logger + + logger.info("test") + logger.error("wrong1") + logger.exception("wrong2") + + # with traceback + try: + 1 / 0 + except Exception: + logger.exception("wrong3") +""" diff --git a/src/login/bklogin/common/mixins/__init__.py b/src/login/bklogin/common/mixins/__init__.py new file mode 100755 index 000000000..1c6763228 --- /dev/null +++ b/src/login/bklogin/common/mixins/__init__.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS +Community Edition) available. +Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. +Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. +You may obtain a copy of the License at http://opensource.org/licenses/MIT +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. +""" diff --git a/src/login/bklogin/common/mixins/exempt.py b/src/login/bklogin/common/mixins/exempt.py new file mode 100755 index 000000000..a4616dac0 --- /dev/null +++ b/src/login/bklogin/common/mixins/exempt.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS +Community Edition) available. +Copyright (C) 2017-2018 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 builtins import object + +from bklogin.bkauth.decorators import login_exempt +from django.utils.decorators import method_decorator +from django.views.decorators.csrf import csrf_exempt + + +class CsrfExemptMixin(object): + """ + Mixin allows you to request without `csrftoken`. + """ + + @method_decorator(csrf_exempt) + def dispatch(self, *args, **kwargs): + return super(CsrfExemptMixin, self).dispatch(*args, **kwargs) + + +class LoginExemptMixin(object): + """ + Mixin allows you to request without `login`. + """ + + @method_decorator(login_exempt) + def dispatch(self, *args, **kwargs): + return super(LoginExemptMixin, self).dispatch(*args, **kwargs) + + +class CsrfAndLoginExemptMixin(object): + """ + Mixin allows you to request without `login` and `csrftoken`. + """ + + @method_decorator(csrf_exempt) + @method_decorator(login_exempt) + def dispatch(self, *args, **kwargs): + return super(CsrfAndLoginExemptMixin, self).dispatch(*args, **kwargs) diff --git a/src/login/bklogin/common/usermgr.py b/src/login/bklogin/common/usermgr.py new file mode 100755 index 000000000..4d550008f --- /dev/null +++ b/src/login/bklogin/common/usermgr.py @@ -0,0 +1,137 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS +Community Edition) available. +Copyright (C) 2017-2018 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 builtins import str + +from bklogin.common.constants import BKLOGIN_USERMGR_FIELD_MAP +from bklogin.common.log import logger +from bklogin.components import usermgr_api +from cachetools import TTLCache, cached + + +def _user_info(usermgr_userinfo): + """ + 用户信息转换 + """ + return { + "username": usermgr_userinfo.get("username"), + "chname": (usermgr_userinfo.get(BKLOGIN_USERMGR_FIELD_MAP["chname"]) or usermgr_userinfo.get("chname") or ""), + "qq": usermgr_userinfo.get(BKLOGIN_USERMGR_FIELD_MAP["qq"]) or "", + "phone": (usermgr_userinfo.get(BKLOGIN_USERMGR_FIELD_MAP["phone"]) or usermgr_userinfo.get("phone") or ""), + "email": usermgr_userinfo.get(BKLOGIN_USERMGR_FIELD_MAP["email"]) or "", + "role": str(usermgr_userinfo.get(BKLOGIN_USERMGR_FIELD_MAP["role"])) or "", + "wx_userid": ( + usermgr_userinfo.get(BKLOGIN_USERMGR_FIELD_MAP["wx_userid"]) or usermgr_userinfo.get("wx_userid") or "" + ), + "language": usermgr_userinfo.get(BKLOGIN_USERMGR_FIELD_MAP["language"]) or "", + "time_zone": usermgr_userinfo.get(BKLOGIN_USERMGR_FIELD_MAP["time_zone"]) or "", + } + + +def _user_info_v2(usermgr_userinfo): + """ + 用户信息转换 + """ + return { + "bk_username": usermgr_userinfo.get("username"), + "chname": (usermgr_userinfo.get(BKLOGIN_USERMGR_FIELD_MAP["chname"]) or usermgr_userinfo.get("chname") or ""), + "qq": usermgr_userinfo.get(BKLOGIN_USERMGR_FIELD_MAP["qq"]) or "", + "phone": (usermgr_userinfo.get(BKLOGIN_USERMGR_FIELD_MAP["phone"]) or usermgr_userinfo.get("phone") or ""), + "email": usermgr_userinfo.get(BKLOGIN_USERMGR_FIELD_MAP["email"]) or "", + "bk_role": usermgr_userinfo.get(BKLOGIN_USERMGR_FIELD_MAP["role"]) or 0, + "wx_userid": ( + usermgr_userinfo.get(BKLOGIN_USERMGR_FIELD_MAP["wx_userid"]) or usermgr_userinfo.get("wx_userid") or "" + ), + "language": usermgr_userinfo.get(BKLOGIN_USERMGR_FIELD_MAP["language"]) or "", + "time_zone": usermgr_userinfo.get(BKLOGIN_USERMGR_FIELD_MAP["time_zone"]) or "", + } + + +def _batch_query_users(username_list, version=None): + """ + 转换数据 + """ + if version == "v1": + user_info_func = _user_info + elif version == "v2": + user_info_func = _user_info_v2 + else: + # v3 or None, will not change the return fields + user_info_func = None + + ok, message, data = usermgr_api.batch_query_users(username_list=username_list) + if data and len(data) and (user_info_func is not None): + data = [user_info_func(i) for i in data] + return ok, message, data + + +# def get_raw_user(username): +# ok, message, _data = _batch_query_users(username_list=[username]) +# if ok: +# # 判断是否能拿到数据 +# if not _data or len(_data) != 1: +# return False, "user do not exists", {} +# _data = _data[0] +# return ok, message, _data + + +def get_user(username, version="v1"): + """ + 获取单个用户信息 + """ + ok, message, _data = _batch_query_users(username_list=[username], version=version) + if ok: + # 判断是否能拿到数据 + if not _data or len(_data) != 1: + return False, "user do not exists", {} + _data = _data[0] + return ok, message, _data + + +def get_categories(): + try: + ok, message, _data = usermgr_api.get_categories() + except Exception: + logger.exception("usermgr_api get_categories fail") + return [] + + if not ok: + logger.error("login get categories from usermgr fail: %s", message) + return [] + + default_cats = [d for d in _data if d.get("default")] + cats = [d for d in _data if not d.get("default")] + default_cats.extend(cats) + + data = [] + for c in default_cats: + data.append( + { + "id": c.get("id"), + "domain": c.get("domain"), + "is_default": c.get("default", False), + } + ) + + return data + + +@cached(cache=TTLCache(maxsize=1024, ttl=60)) +def get_categories_str(): + categories = get_categories() + + if not categories: + return "" + + return ";".join([c.get("domain") for c in categories]) diff --git a/src/login/bklogin/common/utils/__init__.py b/src/login/bklogin/common/utils/__init__.py new file mode 100755 index 000000000..1c6763228 --- /dev/null +++ b/src/login/bklogin/common/utils/__init__.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS +Community Edition) available. +Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. +Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. +You may obtain a copy of the License at http://opensource.org/licenses/MIT +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. +""" diff --git a/src/login/bklogin/common/utils/basic.py b/src/login/bklogin/common/utils/basic.py new file mode 100755 index 000000000..4527aea47 --- /dev/null +++ b/src/login/bklogin/common/utils/basic.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS +Community Edition) available. +Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. +Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. +You may obtain a copy of the License at http://opensource.org/licenses/MIT +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. +""" +from django.utils.html import escape as html_escape + + +def escape_html_return_msg(func): + """ + 装饰器:用于验证信息返回xss转义 + """ + + def wrapper(*args, **kwargs): + result = func(*args, **kwargs) + # 对于字符串类型,进行html转义 + return [html_escape(item) if isinstance(item, str) else item for item in result] + + return wrapper diff --git a/src/login/bklogin/common/utils/time.py b/src/login/bklogin/common/utils/time.py new file mode 100755 index 000000000..fea0bf500 --- /dev/null +++ b/src/login/bklogin/common/utils/time.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS +Community Edition) available. +Copyright (C) 2017-2018 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 + +from bklogin.common.constants import DATETIME_FORMAT_STRING +from django.utils import timezone + + +def _parse_datetime(date_string, format_string, zone=None, target_zone=None): + """ + 将不带时区的字符串转为目标时区的时间 + :param date_string:时间字符串 + :param format_string:时间字符串格式 + :param zone:时间字符串的时区,默认为本地时区 + :param target_zone: 目标时区,默认为本地时区 + """ + # get a naive datetime + naive_dt = datetime.datetime.strptime(date_string, format_string) + # makes a naive datetime.datetime in a given time zone aware. + aware_dt = timezone.make_aware(naive_dt, zone) + # converts an aware datetime.datetime to local time. + target_aware_dt = timezone.localtime(aware_dt, target_zone) + return target_aware_dt + + +def parse_local_datetime(date_string, format_string=DATETIME_FORMAT_STRING, zone=None): + """ + 将不带时区的字符串转为本地时区的时间 + :param date_string:时间字符串 + :param format_string:时间字符串格式 + :param zone:时间字符串的时区,默认为本地时区 + """ + return _parse_datetime(date_string, format_string, zone=zone) + + +def parse_utc_datetime(date_string, format_string=DATETIME_FORMAT_STRING, zone=None): + """ + 将不带时区的字符串转为UTC时区的时间 + :param date_string:时间字符串 + :param format_string:时间字符串格式 + :param zone:时间字符串的时区,默认为本地时区 + """ + return _parse_datetime(date_string, format_string, zone=zone, target_zone=timezone.utc) diff --git a/src/login/bklogin/components/__init__.py b/src/login/bklogin/components/__init__.py new file mode 100755 index 000000000..1c6763228 --- /dev/null +++ b/src/login/bklogin/components/__init__.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS +Community Edition) available. +Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. +Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. +You may obtain a copy of the License at http://opensource.org/licenses/MIT +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. +""" diff --git a/src/login/bklogin/components/http.py b/src/login/bklogin/components/http.py new file mode 100755 index 000000000..cbb611159 --- /dev/null +++ b/src/login/bklogin/components/http.py @@ -0,0 +1,106 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS +Community Edition) available. +Copyright (C) 2017-2018 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 + +import requests +from bklogin.common.log import logger + +""" +all new components should use the http.py here! +""" + + +def _gen_header(): + headers = { + "Content-Type": "application/json", + } + return headers + + +def _http_request(method, url, headers=None, data=None, timeout=None, verify=False, cert=None, cookies=None): + try: + if method == "GET": + resp = requests.get( + url=url, headers=headers, params=data, timeout=timeout, verify=verify, cert=cert, cookies=cookies + ) + elif method == "HEAD": + resp = requests.head(url=url, headers=headers, verify=verify, cert=cert, cookies=cookies) + elif method == "POST": + resp = requests.post( + url=url, headers=headers, json=data, timeout=timeout, verify=verify, cert=cert, cookies=cookies + ) + elif method == "DELETE": + resp = requests.delete( + url=url, headers=headers, json=data, timeout=timeout, verify=verify, cert=cert, cookies=cookies + ) + elif method == "PUT": + resp = requests.put( + url=url, headers=headers, json=data, timeout=timeout, verify=verify, cert=cert, cookies=cookies + ) + else: + return False, None + except requests.exceptions.RequestException: + logger.exception("http request error! method: %s, url: %s, data: %s", method, url, data) + return False, None + else: + if resp.status_code != 200: + content = resp.content[:100] if resp.content else "" + error_msg = "http request fail! method: %s, url: %s, " "response_status_code: %s, response_content: %s" + # if isinstance(content, str): + # try: + # content = content.decode('utf-8') + # except Exception: + # content = content + logger.error(error_msg, method, url, resp.status_code, content) + return False, None + + return True, resp.json() + + +def http_get(url, data, headers=None, verify=False, cert=None, timeout=None, cookies=None): + if not headers: + headers = _gen_header() + return _http_request( + method="GET", url=url, headers=headers, data=data, verify=verify, cert=cert, timeout=timeout, cookies=cookies + ) + + +def http_post(url, data, headers=None, verify=False, cert=None, timeout=None, cookies=None): + if not headers: + headers = _gen_header() + return _http_request( + method="POST", url=url, headers=headers, data=data, timeout=timeout, verify=verify, cert=cert, cookies=cookies + ) + + +def http_put(url, data, headers=None, verify=False, cert=None, timeout=None, cookies=None): + if not headers: + headers = _gen_header() + return _http_request( + method="PUT", url=url, headers=headers, data=data, timeout=timeout, verify=verify, cert=cert, cookies=cookies + ) + + +def http_delete(url, data, headers=None, verify=False, cert=None, timeout=None, cookies=None): + if not headers: + headers = _gen_header() + return _http_request( + method="DELETE", + url=url, + headers=headers, + data=data, + timeout=timeout, + verify=verify, + cert=cert, + cookies=cookies, + ) diff --git a/src/login/bklogin/components/usermgr_api.py b/src/login/bklogin/components/usermgr_api.py new file mode 100755 index 000000000..10c132389 --- /dev/null +++ b/src/login/bklogin/components/usermgr_api.py @@ -0,0 +1,119 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS +Community Edition) available. +Copyright (C) 2017-2018 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 absolute_import, unicode_literals + +from bklogin.common.log import logger +from django.conf import settings + +from .http import http_get, http_post + +""" +usermgr api +""" + +BK_USERMGR_HOST = settings.BK_USERMGR_API_URL + + +def _call_usermgr_api(http_func, url, data, headers=None): + # TODO: 后续添加Token Header进行服务间认证 + try: + ok, _data = http_func(url, data, headers=headers) + if not ok: + return False, -1, "verify from usermgr server fail", None + except Exception: + logger.exception("_call_usermgr_api fail: url=%s, data=%s", url, data) + return False, -1, "verify from usermgr server fail", None + + if not _data.get("result"): + if data and "password" in data: + data["password"] = "******" + logger.info("_call_usermgr_api fail: url=%s, data=%s, _data=%s", url, data, _data) + return False, _data.get("code", -1), _data.get("message", "usermgr api fail"), _data.get("data") + + return True, 0, "ok", _data.get("data") + + +def authenticate(username, password, language="", domain=""): + """ + 认证用户名和密码 + username: 用户名、电话号码、邮箱三选一,如果存在重名,会验证失败 + """ + path = "/api/v1/login/check/" + url = "{host}{path}".format(host=BK_USERMGR_HOST, path=path) + + data = { + "username": username, + "password": password, + } + if domain: + data["domain"] = domain + + ok, code, message, _data = _call_usermgr_api( + http_post, + url, + data, + headers={ + "Blueking-Language": language, + "Content-Type": "application/json", + }, + ) + return ok, code, message, _data or {} + + +def batch_query_users(username_list=[], is_complete=False): + """ + 批量获取用户,可以获取所有 + """ + path = "/api/v1/login/profile/query/" + url = "{host}{path}".format(host=BK_USERMGR_HOST, path=path) + + data = { + "username_list": username_list, + "is_complete": is_complete, + } + + ok, _, message, _data = _call_usermgr_api(http_post, url, data) + return ok, message, _data + + +def upsert_user(username, **kwargs): + """ + 创建或更新用户 + 主要用于ee_login,企业第三方应用某些情况下需要支持将用户存储到用户管理 + """ + path = "/api/v1/login/profile/" + url = "{host}{path}".format(host=BK_USERMGR_HOST, path=path) + + data = { + "username": username, + } + data.update(kwargs) + ok, _, message, _data = _call_usermgr_api(http_post, url, data) + return ok, message, _data + + +def get_categories(): + path = "/api/v2/categories/" + url = "{host}{path}".format(host=BK_USERMGR_HOST, path=path) + + data = { + "no_page": True, + "fields": "domain,id,default", + "lookup_field": "enabled", + "exact_lookups": True, + } + + ok, _, message, _data = _call_usermgr_api(http_get, url, data) + if "results" in _data: + _data = _data["results"] + return ok, message, _data diff --git a/src/login/bklogin/config/__init__.py b/src/login/bklogin/config/__init__.py new file mode 100755 index 000000000..1c6763228 --- /dev/null +++ b/src/login/bklogin/config/__init__.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS +Community Edition) available. +Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. +Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. +You may obtain a copy of the License at http://opensource.org/licenses/MIT +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. +""" diff --git a/src/login/bklogin/config/common/__init__.py b/src/login/bklogin/config/common/__init__.py new file mode 100644 index 000000000..06f549aea --- /dev/null +++ b/src/login/bklogin/config/common/__init__.py @@ -0,0 +1,25 @@ +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS +Community Edition) available. +Copyright (C) 2017-2018 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 os + +import environ + +from bkuser_global.config import init_patch + +init_patch() + +env = environ.Env() +# reading .env file +environ.Env.read_env() + +PROJECT_PATH = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +PROJECT_ROOT, PROJECT_MODULE_NAME = os.path.split(PROJECT_PATH) +BASE_DIR = os.path.dirname(os.path.dirname(PROJECT_PATH)) diff --git a/src/login/bklogin/config/common/django_basic.py b/src/login/bklogin/config/common/django_basic.py new file mode 100644 index 000000000..5fd0c561b --- /dev/null +++ b/src/login/bklogin/config/common/django_basic.py @@ -0,0 +1,129 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS +Community Edition) available. +Copyright (C) 2017-2018 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 os + +from . import PROJECT_ROOT, env + +ALLOWED_HOSTS = ["*"] + +# Generic Django project settings +DEBUG = env.bool("DEBUG", False) + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = "o7(025idh*fj@)ohujum-ilfxl^n=@d&$xz!_$$7s$8jopd5r#" + +CSRF_COOKIE_NAME = "bklogin_csrftoken" +# CSRF 验证失败处理函数 +CSRF_FAILURE_VIEW = "bklogin.bkauth.views.csrf_failure" + +ROOT_URLCONF = "bklogin.urls" +SITE_URL = "/" + +# Django 3 required +DEFAULT_AUTO_FIELD = "django.db.models.AutoField" + +# Application definition +INSTALLED_APPS = ( + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "django_prometheus", + "bklogin.bkaccount", + "bklogin.bkauth", + "bklogin.bk_i18n", +) + +MIDDLEWARE = ( + "django_prometheus.middleware.PrometheusBeforeMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.locale.LocaleMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", + "django.middleware.security.SecurityMiddleware", + "bklogin.bkauth.middlewares.LoginMiddleware", + "bklogin.bk_i18n.middlewares.LanguageMiddleware", + "bklogin.bk_i18n.middlewares.ApiLanguageMiddleware", + "bklogin.bk_i18n.middlewares.TimezoneMiddleware", + "django_prometheus.middleware.PrometheusAfterMiddleware", +) + + +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + # django template dir + "DIRS": ( + # 绝对路径,比如"/home/html/django_templates" or "C:/www/django/templates". + os.path.join(PROJECT_ROOT, "bklogin/templates"), + ), + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.template.context_processors.csrf", + "bklogin.common.context_processors.site_settings", + "django.template.context_processors.i18n", + "django.contrib.messages.context_processors.messages", + ], + }, + }, +] + +# =============================================================================== +# 静态资源设置 +# =============================================================================== +STATICFILES_DIRS = (os.path.join(PROJECT_ROOT, "static/"),) +STATIC_VERSION = "0.2.3" +STATIC_URL = "/static/" +STATIC_ROOT = os.path.join(PROJECT_ROOT, "staticfiles/") + +# CSS 文件后缀名 +CSS_SUFFIX = "min.css" +# JS 文件后缀名 +JS_SUFFIX = "min.js" + +# ============================================================================== +# Django 项目配置 +# ============================================================================== +USE_I18N = True +USE_L10N = True + +# timezone +# Default time zone for localization in the UI. +# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name +TIME_ZONE = "Asia/Shanghai" +USE_TZ = True +TIMEZONE_SESSION_KEY = "django_timezone" + +# language +LANGUAGES = ( + ("en", "English"), + ("zh-hans", "简体中文"), +) +LANGUAGE_CODE = "zh-hans" +LANGUAGE_COOKIE_NAME = "blueking_language" +LANGUAGE_COOKIE_PATH = "/" +LOCALE_PATHS = (os.path.join(PROJECT_ROOT, "locale"),) + +# ============================================================================== +# AUTHENTICATION +# ============================================================================== +AUTH_USER_MODEL = "bkauth.User" diff --git a/src/login/bklogin/config/common/logging.py b/src/login/bklogin/config/common/logging.py new file mode 100644 index 000000000..385ac99e8 --- /dev/null +++ b/src/login/bklogin/config/common/logging.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS +Community Edition) available. +Copyright (C) 2017-2018 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 pathlib import Path + +from . import PROJECT_ROOT, env + +# logging config +LOG_LEVEL = env("LOG_LEVEL", default="INFO") +LOGGING_DIR = env("LOGGING_DIR", default=Path(PROJECT_ROOT) / "logs") diff --git a/src/login/bklogin/config/common/platform.py b/src/login/bklogin/config/common/platform.py new file mode 100644 index 000000000..0e2a99793 --- /dev/null +++ b/src/login/bklogin/config/common/platform.py @@ -0,0 +1,84 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS +Community Edition) available. +Copyright (C) 2017-2018 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 os + +from bklogin.config.common.django_basic import SITE_URL +from bklogin.config.common.plugin import CUSTOM_AUTHENTICATION_BACKEND, LOGIN_TYPE + +from . import env + +EDITION = env.str("EDITION", default="ce") + +# 用于加密登录态票据(bk_token) +BKKRILL_ENCRYPT_SECRET_KEY = env.str("ENCRYPT_SECRET_KEY") + +# 与 ESB 通信的密钥 +ESB_TOKEN = env.str("BK_PAAS_SECRET_KEY") + +# domain +BK_LOGIN_PUBLIC_ADDR = env.str("BK_LOGIN_PUBLIC_ADDR") +# schema = http/https, default http +HTTP_SCHEMA = env.str("BK_LOGIN_HTTP_SCHEMA", "http") + +# session in cookie secure +IS_SESSION_COOKIE_SECURE = env.bool("IS_SESSION_COOKIE_SECURE", False) +if HTTP_SCHEMA == "https" and IS_SESSION_COOKIE_SECURE: + SESSION_COOKIE_SECURE = True + + +# cookie访问域 +BK_COOKIE_DOMAIN = "." + env.str("BK_DOMAIN") + +# 用户管理API访问地址 +BK_USERMGR_API_URL = env.str("BK_USERMGR_API_URL", "http://bkuserapi-web") +BK_USERMGR_SAAS_URL = env.str("BK_USERMGR_SAAS_URL", "http://bkusersaas-web") + +# external config +# license +CERTIFICATE_DIR = env.str("BK_LOGIN_LOGIN_CERT_PATH", "") +CERTIFICATE_SERVER_DOMAIN = env.str("BK_LOGIN_LOGIN_CERT_SERVER_LOCAL_ADDR", "") + +# cookie名称 +BK_COOKIE_NAME = "bk_token" +LANGUAGE_COOKIE_DOMAIN = BK_COOKIE_DOMAIN +# cookie 有效期,默认为1天 +BK_COOKIE_AGE = env.int("BK_LOGIN_LOGIN_COOKIE_AGE", 60 * 60 * 24) +# bk_token 校验有效期校验时间允许误差,防止多台机器时间不同步,默认1分钟 +BK_TOKEN_OFFSET_ERROR_TIME = env.int("BK_LOGIN_LOGIN_TOKEN_OFFSET_ERROR_TIME", 60) +# 无操作 失效期,默认2个小时. 长时间误操作, 登录态已过期 +BK_INACTIVE_COOKIE_AGE = env.int("BK_LOGIN_LOGIN_INACTIVE_COOKIE_AGE", 60 * 60 * 2) + + +# ============================================================================== +# 企业证书校验相关 +# ============================================================================== +CLIENT_CERT_FILE_PATH = os.path.join(CERTIFICATE_DIR, "platform.cert") +CLIENT_KEY_FILE_PATH = os.path.join(CERTIFICATE_DIR, "platform.key") +CERTIFICATE_SERVER_URL = f"https://{CERTIFICATE_SERVER_DOMAIN}/certificate" + +# =============================================================================== +# AUTHENTICATION +# =============================================================================== +LOGIN_URL = SITE_URL + +LOGOUT_URL = "%slogout/" % SITE_URL + +LOGIN_COMPLETE_URL = f"{HTTP_SCHEMA}://{BK_LOGIN_PUBLIC_ADDR}{SITE_URL}" + + +AUTHENTICATION_BACKENDS_DICT = { + "bk_login": ["bklogin.backends.bk.BkUserBackend"], + "custom_login": [CUSTOM_AUTHENTICATION_BACKEND], +} +AUTHENTICATION_BACKENDS = AUTHENTICATION_BACKENDS_DICT.get( + LOGIN_TYPE, ["bklogin.bkaccount.backends.BkRemoteUserBackend"] +) diff --git a/src/login/bklogin/config/common/plugin.py b/src/login/bklogin/config/common/plugin.py new file mode 100644 index 000000000..a014210eb --- /dev/null +++ b/src/login/bklogin/config/common/plugin.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS +Community Edition) available. +Copyright (C) 2017-2018 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 bklogin.config.common.django_basic import INSTALLED_APPS, ROOT_URLCONF + +################## +# Login Config # +################## +# 蓝鲸登录方式:bk_login,自定义登录方式:custom_login +LOGIN_TYPE = "bk_login" +CUSTOM_LOGIN_VIEW = "" +CUSTOM_AUTHENTICATION_BACKEND = "" +try: + custom_conf_module_path = "bklogin.ee_login.settings_login" + + custom_conf_module = __import__(custom_conf_module_path, globals(), locals(), ["*"]) + LOGIN_TYPE = getattr(custom_conf_module, "LOGIN_TYPE", "bk_login") + + CUSTOM_LOGIN_VIEW = getattr(custom_conf_module, "CUSTOM_LOGIN_VIEW", "") + CUSTOM_AUTHENTICATION_BACKEND = getattr(custom_conf_module, "CUSTOM_AUTHENTICATION_BACKEND", "") + # 支持自定义登录 patch 原有的所有URL 和 添加自定义 Application START + ROOT_URLCONF = getattr(custom_conf_module, "ROOT_URLCONF", None) or ROOT_URLCONF + if LOGIN_TYPE == "custom_login": + INSTALLED_APPS = tuple( # type: ignore + list(INSTALLED_APPS) + + getattr( + custom_conf_module, + "CUSTOM_INSTALLED_APPS", + [], + ) + ) + # 支持自定义登录 patch 原有的所有URL 和 添加自定义 Application END +except ImportError as e: + print("load custom_login settings fail!", e) + LOGIN_TYPE = "bk_login" diff --git a/src/login/bklogin/config/common/storage.py b/src/login/bklogin/config/common/storage.py new file mode 100644 index 000000000..d3289c3b7 --- /dev/null +++ b/src/login/bklogin/config/common/storage.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -* +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS +Community Edition) available. +Copyright (C) 2017-2018 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 bkuser_global.config import get_db_config + +from . import env + +# ============================================================================== +# 数据库 +# ============================================================================== +DB_PREFIX = "DATABASE" + +DATABASES = get_db_config(env, DB_PREFIX) + + +# ============================================================================== +# 缓存 +# ============================================================================== +CACHES = { + "default": { + "BACKEND": "django.core.cache.backends.locmem.LocMemCache", + "TIMEOUT": 30, + "OPTIONS": {"MAX_ENTRIES": 1000}, + } +} diff --git a/src/login/bklogin/config/overlays/__init__.py b/src/login/bklogin/config/overlays/__init__.py new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/src/login/bklogin/config/overlays/__init__.py @@ -0,0 +1 @@ + diff --git a/src/login/bklogin/config/overlays/prod.py b/src/login/bklogin/config/overlays/prod.py new file mode 100644 index 000000000..b61a48998 --- /dev/null +++ b/src/login/bklogin/config/overlays/prod.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS +Community Edition) available. +Copyright (C) 2017-2018 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 bklogin.config.common.django_basic import * # noqa +from bklogin.config.common.logging import * # noqa +from bklogin.config.common.platform import * # noqa +from bklogin.config.common.storage import * # noqa + +from bkuser_global.logging import LoggingType, get_logging + +SITE_URL = "/login/" +LOGGING = get_logging(logging_type=LoggingType.STDOUT, log_level=LOG_LEVEL, package_name="bkuser_core") diff --git a/src/login/bklogin/ee_login/__init__.py b/src/login/bklogin/ee_login/__init__.py new file mode 100755 index 000000000..1c6763228 --- /dev/null +++ b/src/login/bklogin/ee_login/__init__.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS +Community Edition) available. +Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. +Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. +You may obtain a copy of the License at http://opensource.org/licenses/MIT +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. +""" diff --git a/src/login/bklogin/ee_login/settings_login.py b/src/login/bklogin/ee_login/settings_login.py new file mode 100755 index 000000000..82ef8bbae --- /dev/null +++ b/src/login/bklogin/ee_login/settings_login.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS +Community Edition) available. +Copyright (C) 2017-2018 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. +""" + +# 蓝鲸登录方式:bk_login +# 企业内部Username-password登录方式:enterprise_ldap +# 自定义登录方式:custom_login +LOGIN_TYPE = "bk_login" + +# 默认bk_login,无需设置其他配置 + +########################### +# 自定义登录 custom_login # +########################### +# 配置自定义登录请求和登录回调的响应函数, 如:CUSTOM_LOGIN_VIEW = 'ee_official_login.oauth.google.views.login' +CUSTOM_LOGIN_VIEW = "" +# 配置自定义验证是否登录的认证函数, 如:CUSTOM_AUTHENTICATION_BACKEND = 'ee_official_login.oauth.google.backends.OauthBackend' +CUSTOM_AUTHENTICATION_BACKEND = "" diff --git a/src/login/bklogin/ee_login/settings_login_mock.py b/src/login/bklogin/ee_login/settings_login_mock.py new file mode 100755 index 000000000..60f5c1465 --- /dev/null +++ b/src/login/bklogin/ee_login/settings_login_mock.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS +Community Edition) available. +Copyright (C) 2017-2018 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. +""" + +# 蓝鲸登录方式:bk_login +# 企业内部Username-password登录方式:enterprise_ldap +# 自定义登录方式:custom_login +LOGIN_TYPE = "custom_login" + +# 默认bk_login,无需设置其他配置 + +########################### +# 自定义登录 custom_login # +########################### +# 配置自定义登录请求和登录回调的响应函数, 如:CUSTOM_LOGIN_VIEW = 'ee_official_login.oauth.google.views.login' +CUSTOM_LOGIN_VIEW = "ee_official_login.mock.views.login" +# 配置自定义验证是否登录的认证函数, 如:CUSTOM_AUTHENTICATION_BACKEND = 'ee_official_login.oauth.google.backends.OauthBackend' +CUSTOM_AUTHENTICATION_BACKEND = "ee_official_login.mock.backends.MockBackend" diff --git a/src/login/bklogin/ee_login/settings_login_oauth_google.py b/src/login/bklogin/ee_login/settings_login_oauth_google.py new file mode 100755 index 000000000..ba16df1dd --- /dev/null +++ b/src/login/bklogin/ee_login/settings_login_oauth_google.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS +Community Edition) available. +Copyright (C) 2017-2018 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. +""" + +# 蓝鲸登录方式:bk_login +# 企业内部Username-password登录方式:enterprise_ldap +# 自定义登录方式:custom_login +LOGIN_TYPE = "custom_login" + +# 默认bk_login,无需设置其他配置 + +########################### +# 自定义登录 custom_login # +########################### +# 配置自定义登录请求和登录回调的响应函数, 如:CUSTOM_LOGIN_VIEW = 'ee_official_login.oauth.google.views.login' +CUSTOM_LOGIN_VIEW = "ee_official_login.oauth.google.views.login" +# 配置自定义验证是否登录的认证函数, 如:CUSTOM_AUTHENTICATION_BACKEND = 'ee_official_login.oauth.google.backends.OauthBackend' +CUSTOM_AUTHENTICATION_BACKEND = "ee_official_login.oauth.google.backends.OauthBackend" diff --git a/src/login/bklogin/ee_official_login/__init__.py b/src/login/bklogin/ee_official_login/__init__.py new file mode 100755 index 000000000..1c6763228 --- /dev/null +++ b/src/login/bklogin/ee_official_login/__init__.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS +Community Edition) available. +Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. +Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. +You may obtain a copy of the License at http://opensource.org/licenses/MIT +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. +""" diff --git a/src/login/bklogin/ee_official_login/mock/__init__.py b/src/login/bklogin/ee_official_login/mock/__init__.py new file mode 100755 index 000000000..1c6763228 --- /dev/null +++ b/src/login/bklogin/ee_official_login/mock/__init__.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS +Community Edition) available. +Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. +Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. +You may obtain a copy of the License at http://opensource.org/licenses/MIT +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. +""" diff --git a/src/login/bklogin/ee_official_login/mock/backends.py b/src/login/bklogin/ee_official_login/mock/backends.py new file mode 100755 index 000000000..c4284e1e6 --- /dev/null +++ b/src/login/bklogin/ee_official_login/mock/backends.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS +Community Edition) available. +Copyright (C) 2017-2018 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 bklogin.common.log import logger +from django.contrib.auth import get_user_model +from django.contrib.auth.backends import ModelBackend + + +class MockBackend(ModelBackend): + """ + mock认证服务 + + username == "admin" 且 password == "blueking" 时认证通过 + + 注意: 打logger.debug用于调试, 可以在日志路径下login.log查看到对应日志 + """ + + def authenticate(self, username=None, password=None): + if not (username == "admin" and password == "blueking"): + logger.debug("MockBackend authenticate fail, username/password should be admin/blueking") + return None + + # 获取User类 + UserModel = get_user_model() + # 初始化User对象 -> bkauth/models.py:User -> 从userinfo获取对应字段进行初始化 + user = UserModel() + user.username = username + user.display_name = "mockadmin" + user.email = "mockadmin@mock.com" + + # 同步用户到用户管理 sync to usermgr + # ok, message = user.sync_to_usermgr() + ok, message = True, "success" + if not ok: + logger.error("login success, but sync user to usermgr fail: %s", message) + return None + + return user diff --git a/src/login/bklogin/ee_official_login/mock/views.py b/src/login/bklogin/ee_official_login/mock/views.py new file mode 100755 index 000000000..00e179e25 --- /dev/null +++ b/src/login/bklogin/ee_official_login/mock/views.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS +Community Edition) available. +Copyright (C) 2017-2018 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 bklogin.bkauth.actions import login_failed_response, login_success_response +from bklogin.bkauth.constants import REDIRECT_FIELD_NAME +from bklogin.bkauth.forms import BkAuthenticationForm +from bklogin.bkauth.utils import set_bk_token_invalid +from bklogin.common.log import logger +from django.contrib.auth import authenticate +from django.contrib.sites.shortcuts import get_current_site +from django.template.response import TemplateResponse + + +def login(request): + """ + 登录处理 + """ + redirect_to = request.GET.get(REDIRECT_FIELD_NAME, "") + + # 复用bkauth的登录页面 + if request.method == "POST": + form = BkAuthenticationForm(request, data=request.POST) + + username = form.data["username"] + password = form.data["password"] + + # will call MockBackend.authenticate + user = authenticate(username=username, password=password) + if user is None: + logger.debug("custom_login:mock user is None, will redirect_to=%s", redirect_to) + # 直接调用蓝鲸登录失败处理方法 + return login_failed_response(request, redirect_to, app_id=None) + # 成功,则调用蓝鲸登录成功的处理函数,并返回响应 + logger.debug("custom_login:mock login success, will redirect_to=%s", redirect_to) + return login_success_response(request, user, redirect_to, app_id=None) + # GET + else: + form = BkAuthenticationForm(request) + current_site = get_current_site(request) + context = { + "form": form, + REDIRECT_FIELD_NAME: redirect_to, + "site": current_site, + "site_name": current_site.name, + # set to default + "error_message": "", + "app_id": "", + "is_license_ok": True, + "reset_password_url": "", + "login_redirect_to": "", + } + + template_name = "account/login.html" + response = TemplateResponse(request, template_name, context) + response = set_bk_token_invalid(request, response) + return response diff --git a/src/login/bklogin/ee_official_login/oauth/__init__.py b/src/login/bklogin/ee_official_login/oauth/__init__.py new file mode 100755 index 000000000..1c6763228 --- /dev/null +++ b/src/login/bklogin/ee_official_login/oauth/__init__.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS +Community Edition) available. +Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. +Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. +You may obtain a copy of the License at http://opensource.org/licenses/MIT +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. +""" diff --git a/src/login/bklogin/ee_official_login/oauth/google/__init__.py b/src/login/bklogin/ee_official_login/oauth/google/__init__.py new file mode 100755 index 000000000..1c6763228 --- /dev/null +++ b/src/login/bklogin/ee_official_login/oauth/google/__init__.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS +Community Edition) available. +Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. +Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. +You may obtain a copy of the License at http://opensource.org/licenses/MIT +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. +""" diff --git a/src/login/bklogin/ee_official_login/oauth/google/backends.py b/src/login/bklogin/ee_official_login/oauth/google/backends.py new file mode 100755 index 000000000..381442c99 --- /dev/null +++ b/src/login/bklogin/ee_official_login/oauth/google/backends.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS +Community Edition) available. +Copyright (C) 2017-2018 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 bklogin.common.log import logger +from django.contrib.auth import get_user_model +from django.contrib.auth.backends import ModelBackend + +from .utils import get_access_token, get_scope_data + + +class OauthBackend(ModelBackend): + """ + 自定义认证方法 + + 注意: 打logger.debug用于调试, 可以在日志路径下login.log查看到对应日志 + """ + + def authenticate(self, code=None): + # Google登录验证 + try: + # 调用接口验证登录票据CODE,并获取access_token + access_token = get_access_token(code) + if not access_token: + logger.debug("OauthBackend get_access_token fail") + return None + # 通过access_token 获取用户信息 + userinfo = get_scope_data(access_token) + if not userinfo: + logger.debug("OauthBackend get_scope_data fail") + return None + + logger.debug("OauthBackend get userinfo=%s", userinfo) + + # 验证通过 + username = userinfo.get("username") + + # 获取User类 + UserModel = get_user_model() + # 初始化User对象 -> bkauth/models.py:User -> 从userinfo获取对应字段进行初始化 + # 新建用户时, username/display_name必须 + user = UserModel() + user.username = username + user.display_name = userinfo.get("display_name") + user.email = userinfo.get("email") + user.telephone = userinfo.get("telephone") + + # 同步用户到用户管理 sync to usermgr + ok, message = user.sync_to_usermgr() + if not ok: + logger.error("login success, but sync user to usermgr fail: %s", message) + return None + + return user + + except Exception: + logger.exception("Google login backend validation error!") + return None diff --git a/src/login/bklogin/ee_official_login/oauth/google/settings.py b/src/login/bklogin/ee_official_login/oauth/google/settings.py new file mode 100755 index 000000000..44209387a --- /dev/null +++ b/src/login/bklogin/ee_official_login/oauth/google/settings.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS +Community Edition) available. +Copyright (C) 2017-2018 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. +""" + +# google oauth2.0 登录URL +GOOGLE_OAUTH_LOGIN_URL = "https://accounts.google.com/o/oauth2/auth" + +# 通过认证Code获取Access_token的API URL +ACCESS_TOKEN_URL = "https://www.googleapis.com/oauth2/v3/token" + +# 获取google 用户信息的API URL +SCOPE_URL = "https://www.googleapis.com/userinfo/v2/me" + +# Google OAuth 2.0 客户端 ID +CLIENT_ID = "" + +# Google OAuth 2.0 客户端 密钥 +CLIENT_SECRET = "" diff --git a/src/login/bklogin/ee_official_login/oauth/google/utils.py b/src/login/bklogin/ee_official_login/oauth/google/utils.py new file mode 100755 index 000000000..deef321b0 --- /dev/null +++ b/src/login/bklogin/ee_official_login/oauth/google/utils.py @@ -0,0 +1,105 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS +Community Edition) available. +Copyright (C) 2017-2018 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 random +import urllib.error +import urllib.parse +import urllib.request +from builtins import range, str + +import requests +from bklogin.common.log import logger +from django.conf import settings as bk_settings + +from . import settings as google_setting + + +def gen_oauth_state_security_token(length=32): + """ + 生成随机的state,防止csrf + """ + allowed_chars = "abcdefghijkmnpqrstuvwxyzABCDEFGHIJKLMNPQRSTUVWXYZ0123456789" + state = "".join(random.choice(allowed_chars) for _ in range(length)) + return state + + +def gen_oauth_login_url(extra_param): + """ + 生成跳转登录的URL + """ + # 由于google校验redirect_uri是精准匹配的,所有redirect_uri中无法带参数,只能放置在state中处理 + extra_param = {} if extra_param is None or not isinstance(extra_param, dict) else extra_param + extra_param["security_token"] = gen_oauth_state_security_token() + state = "&".join(["%s=%s" % (k, v) for k, v in list(extra_param.items()) if v is not None and v != ""]) + # 跳转到 google 登录的URL + google_oauth_login_url = "%s?%s" % ( + google_setting.GOOGLE_OAUTH_LOGIN_URL, + urllib.parse.urlencode( + { + "response_type": "code", + "client_id": google_setting.CLIENT_ID, + "redirect_uri": bk_settings.LOGIN_COMPLETE_URL, + "scope": "email", + "state": state, + } + ), + ) + return google_oauth_login_url, state + + +def get_access_token(code): + """ + 调用接口验证CODE,并获取access_token + """ + params = { + "grant_type": "authorization_code", + "code": code, + "redirect_uri": bk_settings.LOGIN_COMPLETE_URL, + "client_id": google_setting.CLIENT_ID, + "client_secret": google_setting.CLIENT_SECRET, + } + resp = requests.post(url=google_setting.ACCESS_TOKEN_URL, params=params) + if resp.status_code != 200: + # 记录错误日志 + content = resp.content[:100] if resp.content else "" + error_msg = ( + "http enterprise request error! type: %s, url: %s, data: %s, " + "response_status_code: %s, response_content: %s" + ) + logger.error(error_msg % ("POST", google_setting.ACCESS_TOKEN_URL, str(params), resp.status_code, content)) + return None + data = resp.json() + return data.get("access_token") + + +def get_scope_data(access_token): + """ + scope要求的数据 + """ + params = {"access_token": access_token} + resp = requests.get(google_setting.SCOPE_URL, params=params) + if resp.status_code != 200: + # 记录错误日志 + content = resp.content[:100] if resp.content else "" + error_msg = ( + "http enterprise request error! type: %s, url: %s, data: %s, " + "response_status_code: %s, response_content: %s" + ) + logger.error(error_msg % ("GET", google_setting.SCOPE_URL, str(params), resp.status_code, content)) + return None + data = resp.json() + userinfo = { + "username": data.get("email", ""), + "display_name": data.get("email", ""), + "email": data.get("email", ""), + "telephone": data.get("phone", ""), + } + return userinfo diff --git a/src/login/bklogin/ee_official_login/oauth/google/views.py b/src/login/bklogin/ee_official_login/oauth/google/views.py new file mode 100755 index 000000000..446a5b56d --- /dev/null +++ b/src/login/bklogin/ee_official_login/oauth/google/views.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS +Community Edition) available. +Copyright (C) 2017-2018 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 urllib.parse + +from bklogin.bkauth import actions +from bklogin.bkauth.constants import REDIRECT_FIELD_NAME +from bklogin.common.log import logger +from django.contrib.auth import authenticate + +from .utils import gen_oauth_login_url + + +def login(request): + """ + 登录处理 + """ + # 获取用户实际请求的URL, 目前account.REDIRECT_FIELD_NAME = 'c_url' + redirect_to = request.GET.get(REDIRECT_FIELD_NAME, "") + # 获取用户实际访问的蓝鲸应用 + app_id = request.GET.get("app_id", "") + + # 来自注销 + is_from_logout = bool(request.GET.get("is_from_logout") or 0) + + # google登录回调后会自动添加code参数 + code = request.GET.get("code") + # 若没有code参数,则表示需要跳转到google登录 + if code is None or is_from_logout: + # 生成跳转到google登录的链接 + google_oauth_login_url, state = gen_oauth_login_url({"app_id": app_id, REDIRECT_FIELD_NAME: redirect_to}) + # 将state 设置于session,Oauth2.0特有的,防止csrf攻击的 + request.session["state"] = state + # 直接调用蓝鲸登录重定向方法 + response = actions.login_redirect_response(request, google_oauth_login_url, is_from_logout) + logger.debug( + "custom_login:oauth.google code is None or is_from_logout! code=%s, is_from_logout=%s", + code, + is_from_logout, + ) + return response + + # 已经有企业认证票据参数(如code参数),表示企业登录后的回调或企业认证票据还存在 + # oauth2.0 特有处理逻辑,防止csrf攻击 + # 处理state参数 + state = request.GET.get("state", "") + state_dict = dict(urllib.parse.parse_qsl(state)) + app_id = state_dict.get("app_id") + redirect_to = state_dict.get(REDIRECT_FIELD_NAME, "") + state_from_session = request.session.get("state") + # 校验state,防止csrf攻击 + if state != state_from_session: + logger.debug( + "custom_login:oauth.google state != state_from_session [state=%s, state_from_session=%s]", + state, + state_from_session, + ) + return actions.login_failed_response(request, redirect_to, app_id) + + # 验证用户登录是否OK + user = authenticate(code=code) + if user is None: + logger.debug("custom_login:oauth.google user is None, will redirect_to=%s", redirect_to) + # 直接调用蓝鲸登录失败处理方法 + return actions.login_failed_response(request, redirect_to, app_id) + # 成功,则调用蓝鲸登录成功的处理函数,并返回响应 + logger.debug("custom_login:oauth.google login success, will redirect_to=%s", redirect_to) + return actions.login_success_response(request, user, redirect_to, app_id) diff --git a/src/login/bklogin/healthz/__init__.py b/src/login/bklogin/healthz/__init__.py new file mode 100755 index 000000000..1c6763228 --- /dev/null +++ b/src/login/bklogin/healthz/__init__.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS +Community Edition) available. +Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. +Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. +You may obtain a copy of the License at http://opensource.org/licenses/MIT +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. +""" diff --git a/src/login/bklogin/healthz/urls.py b/src/login/bklogin/healthz/urls.py new file mode 100755 index 000000000..2bb6b2fe0 --- /dev/null +++ b/src/login/bklogin/healthz/urls.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS +Community Edition) available. +Copyright (C) 2017-2018 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 bklogin.healthz import views +from django.conf.urls import url + +urlpatterns = [url("^$", views.healthz)] diff --git a/src/login/bklogin/healthz/views.py b/src/login/bklogin/healthz/views.py new file mode 100755 index 000000000..bc1a5dfe8 --- /dev/null +++ b/src/login/bklogin/healthz/views.py @@ -0,0 +1,130 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS +Community Edition) available. +Copyright (C) 2017-2018 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 os +from builtins import str + +from bklogin.bkauth.decorators import login_exempt +from bklogin.common.exceptions import LoginErrorCodes +from bklogin.common.license import check_license +from django.conf import settings +from django.http import HttpResponse, JsonResponse +from django.utils.translation import ugettext as _ + +# ==================== helpers ========================= + +LOGIN_MODULE_CODE = "1302000" + + +def _gen_json_response(ok, code, message, data): + """ + ok: True/False + code: 平台 1302000 / 模块 1302100 / 具体错误 1302105 + message: 报错信息 + data: dict, 内容自定义 + """ + return JsonResponse({"ok": ok, "code:": code, "message": message, "data": data}, status=200) + + +def _gen_success_json_response(data): + """ + 成功 + """ + return _gen_json_response(ok=True, code=LOGIN_MODULE_CODE, message="OK", data=data) + + +def _gen_fail_json_response(code, message, data): + """ + 失败 + """ + return _gen_json_response(ok=False, code=code, message=message, data=data) + + +# ==================== check ========================= + + +def _check_settings(): + """ + check settings, 注意不暴露密码等敏感信息 + """ + # check settings, 注意不暴露密码等敏感信息 + try: + settings.ESB_TOKEN + { + "debug": settings.DEBUG, + "env": os.getenv("BK_ENV", "unknow"), + "cookie_domain": settings.BK_COOKIE_DOMAIN, + "mysql": { + "host": settings.DATABASES.get("default", {}).get("HOST"), + "port": settings.DATABASES.get("default", {}).get("PORT"), + "user": settings.DATABASES.get("default", {}).get("USER"), + "database": settings.DATABASES.get("default", {}).get("NAME"), + }, + } + except Exception as e: + return False, _(u"配置文件不正确, 缺失对应配置: %s") % str(e), LoginErrorCodes.E1302001_BASE_SETTINGS_ERROR + + return True, "ok", 0 + + +def _check_database(): + try: + from bkaccount.models import BkToken + + objs = BkToken.objects.all()[:3] + [o.token for o in objs] + except Exception as e: + return False, _(u"数据库连接存在问题: %s") % str(e), LoginErrorCodes.E1302002_BASE_DATABASE_ERROR + + return True, "ok", 0 + + +def _check_license(): + # check license + is_license_ok, message, valid_start_time, valid_end_time = check_license() + if not is_license_ok: + return False, _(u"企业证书无效:%s; 只影响桌面版本信息的展示") % message, LoginErrorCodes.E1302005_BASE_LICENSE_ERROR + + return True, "ok", 0 + + +@login_exempt +def healthz(request): + """ + health check + """ + data = {} + + # 强依赖 + _check_funcs = [ + ("settings", _check_settings), + ("database", _check_database), + # ("license", _check_license), + ] + + if settings.EDITION == "ee": + _check_funcs.append(("license", _check_license)) + + for name, func in _check_funcs: + is_health, message, code = func() + if is_health: + data[name] = "ok" + else: + return _gen_fail_json_response(code=code, message=message, data={}) + + return _gen_success_json_response(data) + + +@login_exempt +def ping(request): + return HttpResponse("pong", content_type="text/plain") diff --git a/src/login/bklogin/metadata/__init__.py b/src/login/bklogin/metadata/__init__.py new file mode 100755 index 000000000..1c6763228 --- /dev/null +++ b/src/login/bklogin/metadata/__init__.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS +Community Edition) available. +Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. +Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. +You may obtain a copy of the License at http://opensource.org/licenses/MIT +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. +""" diff --git a/src/login/bklogin/metadata/urls.py b/src/login/bklogin/metadata/urls.py new file mode 100755 index 000000000..4f992e47f --- /dev/null +++ b/src/login/bklogin/metadata/urls.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS +Community Edition) available. +Copyright (C) 2017-2018 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 bklogin.metadata import views +from django.conf.urls import url + +urlpatterns = [url("^website/$", views.website_metadata)] diff --git a/src/login/bklogin/metadata/views.py b/src/login/bklogin/metadata/views.py new file mode 100755 index 000000000..7481944ae --- /dev/null +++ b/src/login/bklogin/metadata/views.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS +Community Edition) available. +Copyright (C) 2017-2018 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 bklogin.bkauth.decorators import login_exempt +from django.shortcuts import render +from django.views.decorators.clickjacking import xframe_options_exempt + + +@xframe_options_exempt +@login_exempt +def website_metadata(request): + return render(request, "metadata/website.json") diff --git a/src/login/bklogin/particles.json b/src/login/bklogin/particles.json new file mode 100644 index 000000000..10f282c29 --- /dev/null +++ b/src/login/bklogin/particles.json @@ -0,0 +1,116 @@ +{ + "particles": { + "number": { + "value": 80, + "density": { + "enable": true, + "value_area": 800 + } + }, + "color": { + "value": "#ffffff" + }, + "shape": { + "type": "circle", + "stroke": { + "width": 0, + "color": "#000000" + }, + "polygon": { + "nb_sides": 5 + }, + "image": { + "src": "img/github.svg", + "width": 100, + "height": 100 + } + }, + "opacity": { + "value": 0.5, + "random": false, + "anim": { + "enable": false, + "speed": 1, + "opacity_min": 0.1, + "sync": false + } + }, + "size": { + "value": 5, + "random": true, + "anim": { + "enable": false, + "speed": 40, + "size_min": 0.1, + "sync": false + } + }, + "line_linked": { + "enable": true, + "distance": 150, + "color": "#ffffff", + "opacity": 0.4, + "width": 1 + }, + "move": { + "enable": true, + "speed": 6, + "direction": "none", + "random": false, + "straight": false, + "out_mode": "out", + "attract": { + "enable": false, + "rotateX": 600, + "rotateY": 1200 + } + } + }, + "interactivity": { + "detect_on": "canvas", + "events": { + "onhover": { + "enable": true, + "mode": "repulse" + }, + "onclick": { + "enable": true, + "mode": "push" + }, + "resize": true + }, + "modes": { + "grab": { + "distance": 400, + "line_linked": { + "opacity": 1 + } + }, + "bubble": { + "distance": 400, + "size": 40, + "duration": 2, + "opacity": 8, + "speed": 3 + }, + "repulse": { + "distance": 200 + }, + "push": { + "particles_nb": 4 + }, + "remove": { + "particles_nb": 2 + } + } + }, + "retina_detect": true, + "config_demo": { + "hide_card": false, + "background_color": "#b61924", + "background_image": "", + "background_position": "50% 50%", + "background_repeat": "no-repeat", + "background_size": "cover" + } +} \ No newline at end of file diff --git a/src/login/bklogin/templates/401.html b/src/login/bklogin/templates/401.html new file mode 100755 index 000000000..a15ef69bf --- /dev/null +++ b/src/login/bklogin/templates/401.html @@ -0,0 +1,25 @@ +{% load i18n %} + + + + +{% trans '未登录蓝鲸智云平台(401页)' %} + + + + + + + + + + diff --git a/src/login/bklogin/templates/403.html b/src/login/bklogin/templates/403.html new file mode 100755 index 000000000..88925036a --- /dev/null +++ b/src/login/bklogin/templates/403.html @@ -0,0 +1,24 @@ +{% load i18n %} + + + + +{% trans '您没有访问权限(403页)' %} + + + + + + + + + + diff --git a/src/login/bklogin/templates/404.html b/src/login/bklogin/templates/404.html new file mode 100755 index 000000000..0581ce225 --- /dev/null +++ b/src/login/bklogin/templates/404.html @@ -0,0 +1,23 @@ +{% load i18n %} + + + + +{% trans '页面找不到(404页)' %} + + + + + + +
+ +

{% trans '页面找不到了' %}

+
+ + diff --git a/src/login/bklogin/templates/500.html b/src/login/bklogin/templates/500.html new file mode 100755 index 000000000..a10df0b4c --- /dev/null +++ b/src/login/bklogin/templates/500.html @@ -0,0 +1,24 @@ +{% load i18n %} + + + + +{% trans '系统异常(500页)' %} + + + + + + +
+ +

{% trans '系统出现异常' %}

+

{% trans '努力恢复中,请稍后再试......' %}

+
+ + diff --git a/src/login/bklogin/templates/50x.html b/src/login/bklogin/templates/50x.html new file mode 100755 index 000000000..d171d5646 --- /dev/null +++ b/src/login/bklogin/templates/50x.html @@ -0,0 +1,23 @@ +{% load i18n %} + + + + +{% trans '服务故障,努力修复中...' %} + + + + + +
+ +

{% trans '服务故障,努力修复中...' %}

+

{% trans '服务出现故障,我们正在紧急修复,给您带来不便,敬请谅解。' %}

+
+ + diff --git a/src/login/bklogin/templates/account/agreement.part b/src/login/bklogin/templates/account/agreement.part new file mode 100755 index 000000000..e6d9acfcc --- /dev/null +++ b/src/login/bklogin/templates/account/agreement.part @@ -0,0 +1,127 @@ +{% load i18n %} +
+
+ +
+ {% blocktrans trimmed %} +
腾讯蓝鲸智云软件许可及服务协议
+
+

【首部及导言】

+

欢迎您使用腾讯蓝鲸智云软件及服务。

+

为使用腾讯蓝鲸智云软件(以下简称“本软件”)及服务,您应当阅读并遵守《腾讯蓝鲸智云软件许可及服务协议》(以下简称“本协议”),以及《腾讯服务协议》。请您务必审慎阅读、充分理解各条款内容,特别是免除或者限制责任的条款,以及开通或使用某项服务的单独协议,并选择接受或不接受。限制、免责条款可能以加粗形式提示您注意。

+

除非您已阅读并接受本协议所有条款,否则您无权下载、安装或使用本软件及相关服务。您的下载、安装、使用、登录等行为即视为您已阅读并同意上述协议的约束。

+

一、【协议的范围】

+

1.1【协议适用主体范围】

+

本协议是您与腾讯之间关于您下载、安装、使用、复制本软件,以及使用腾讯相关服务所订立的协议。

+

1.2【协议关系及冲突条款】

+

本协议被视为《腾讯服务协议》(链接地址:http://www.qq.com/contract.shtml,若链接地址变更的,则以变更后的链接地址所对应的内容为准;其他链接地址变更的情形,均适用前述约定。)的补充协议,是其不可分割的组成部分,与其构成统一整体。本协议与上述内容存在冲突的,以本协议为准。

+

本协议内容同时包括腾讯可能不断发布的关于本服务的相关协议、业务规则等内容。上述内容一经正式发布,即为本协议不可分割的组成部分,您同样应当遵守。

+

二、【关于本服务】

+

2.1【本服务的内容】

+

本服务内容是指蓝鲸智云产品以及相关服务,包括但不限于提供的基础运维平台(如“配置平台”、“作业平台”、“管控平台”等),PaaS服务(如“AppEngine”、“开发者中心”、“应用开发框架”、“组件”、“前端Magicbox”等),SaaS服务(如监控告警、持续集成、持续部署、辅助运营等),以及支撑上述服务的其他相关产品,为用户提供完善的基础服务设施,以使用户快速、便捷的创建、部署和管理应用的软件许可及服务(以下简称“本服务”)。

+

2.2 【本服务的形式】

+

您使用本服务需要下载腾讯蓝鲸智云产品软件,对于这些软件,腾讯给予您一项个人的、不可转让及非排他性的许可。您仅可为访问或使用本服务的目的而使用这些软件及服务。

+

2.3 【本服务许可的范围】

+

2.3.1 腾讯给予您一项不可转让及非排他性的许可,以使用本软件。您可以为非商业目的在单一台终端设备上安装、使用、显示、运行本软件。

+

2.3.2 您可以为使用本软件及服务的目的复制本软件的一个副本,仅用作备份。备份副本必须包含原软件中含有的所有著作权信息。

+

2.3.3 本条及本协议其他条款未明示授权的其他一切权利仍由腾讯保留,您在行使这些权利时须另外取得腾讯的书面许可。腾讯如果未行使前述任何权利,并不构成对该权利的放弃。

+

三、【软件的获取】

+

3.1 您可以直接从腾讯的网站上获取本软件,也可以从得到腾讯授权的第三方获取。

+

3.2 如果您从未经腾讯授权的第三方获取本软件或与本软件名称相同的安装程序,腾讯无法保证该软件能够正常使用,并对因此给您造成的损失不予负责。

+

四、【软件的安装与卸载】

+

4.1 腾讯可能为不同的需求开发了不同的软件版本,您应当根据实际情况选择下载合适的版本进行安装。

+

4.2 下载安装程序后,您需要按照该程序提示的步骤正确安装。

+

4.3 为提供更加优质、安全的服务,在本软件安装时腾讯可能推荐您安装其他软件,您可以选择安装或不安装。

+

4.4 如果您不再需要使用本软件或者需要安装新版软件,可以自行卸载。如果您愿意帮助腾讯改进产品服务,请告知卸载的原因。

+

五、【软件的更新】

+

5.1 为了改善用户体验、完善服务内容,腾讯将不断努力开发新的服务,并为您不时提供软件更新(这些更新可能会采取软件替换、修改、功能强化、版本升级等形式)。

+

5.2 为了保证本软件及服务的安全性和功能的一致性,腾讯有权不经向您特别通知而对软件进行更新,或者对软件的部分功能效果进行改变或限制。

+

5.3 本软件新版本发布后,旧版本的软件可能无法使用。腾讯不保证旧版本软件继续可用及相应的客户服务,请您随时核对并下载最新版本。

+

六、【用户个人信息保护】

+

6.1保护用户个人信息是腾讯的一项基本原则。腾讯将按照本协议及《隐私政策》(链接地址:http://www.qq.com/privacy.htm)的规定收集、使用、储存和分享您的个人信息。本协议对个人信息保护规定的内容与上述《隐私政策》有相冲突的,及本协议对个人信息保护相关内容未作明确规定的,均应以《隐私政策》的内容为准。

+

6.2腾讯将会采取合理的措施保护用户的个人信息。除法律法规规定的情形外,未经用户许可腾讯不会向第三方公开、透露用户个人信息。腾讯对相关信息采用专业加密存储与传输方式,保障用户个人信息的安全。

+

6.3 您在注册帐号或使用本服务的过程中,可能需要提供一些必要的信息,若国家法律法规或政策有特殊规定的,您需要提供真实的身份信息。基于某些产品功能,腾讯会需要您提供使用该功能的非关联用户身份的相关信息,您同意腾讯基于上述目的收集上述信息,若您不提供或提供的信息不完整,则无法使用本服务或在使用过程中受到限制。

+

七、【主权利义务条款】

+

7.1 【帐号使用规范】

+

7.1.1 用户有责任妥善保管注册账户信息及账户密码的安全,用户需要对注册账户以及密码下的行为承担法律责任。用户同意在任何情况下不向他人透露账户及密码信息。在您怀疑他人在使用您的帐号时,请您及时处理。

+

7.1.2 管理员账号使用者,则可以创建多个账号,所创建的所有账号,由创建的人对账号承担保密的责任。

+

7.1.3 非管理员账号使用者,应妥善保管账号密码的安全,若存在密码修改等事宜,应求助所创建账号的管理员。

+

7.1.4 腾讯蓝鲸智云产品的账户密码安全,均由软件使用者承担相关法律责任。

+

7.2【用户注意事项】

+

7.2.1 您理解并同意:为了向您提供有效的服务,本软件会利用您终端的处理器和带宽等资源。本软件使用过程中可能产生数据流量的费用,用户需自行向运营商了解相关资费信息,并自行承担相关费用。

+

7.2.2 您理解并同意:

+

7.2.2.1 您在本软件的应用市场中添加的第三方软件,由第三方享有一切合法权利。因第三方软件引发的任何纠纷,由该第三方负责解决,腾讯不承担任何责任。 腾讯不对第三方软件或技术提供客服支持,若用户需要获取支持,请与该软件或技术提供商联系。

+

7.2.2.2 本软件对涉及到的第三方软件的安装信息及升级信息等一切信息的安全性、合法性、兼容性、无害性等不承担任何担保及保证,由此而产生的任何法律纠纷,由该第三方负责解决,腾讯不承担任何责任。

+

7.2.2.3 本软件所涉及到的任何第三方软件,其一切法律权利归第三方权利人所享有,用户下载、安装、使用第三方软件受该软件许可协议所约束。在第三方软件使用过程中所产生的任何纠纷,均由该第三方负责解决,腾讯不承担任何责任。

+

7.2.2.4 本软件供用户用来下载、安装腾讯和/或第三方软件,并不能识别用户利用本软件下载、安装的第三方软件是否有合法来源。若您为有关软件的权利人,不愿本软件为您的软件提供用户下载、安装、使用的服务,可按本协议约定的联系方式联系我们(联系邮箱:【*】,联系电话:【*】),我们将会积极配合进行处理。

+

7.2.3 您在使用本软件某一特定服务时,该服务可能会另有单独的协议、相关业务规则等(以下统称为“单独协议”),您在使用该项服务前请阅读并同意相关的单独协议。

+

7.2.4 您理解并同意腾讯将会尽其商业上的合理努力保障您在本软件及服务中的数据存储安全,但是,腾讯并不能就此提供完全保证,包括但不限于以下情形:

+

7.2.4.1 腾讯不对您在本软件及服务中相关数据的删除或储存失败负责;

+

7.2.4.2 腾讯有权根据实际情况自行决定单个用户在本软件及服务中数据的最长储存期限,并在服务器上为其分配数据最大存储空间等。您可根据自己的需要自行备份本软件及服务中的相关数据;

+

7.2.4.3 如果您停止使用本软件及服务或服务被终止或取消,腾讯可以从服务器上永久地删除您的数据。服务停止、终止或取消后,腾讯没有义务向您返还任何数据。

+

7.3【第三方产品和服务】

+

7.3.1 您在本软件的应用市场中添加第三方提供的产品或服务时,除遵守本协议约定外,还应遵守第三方的用户协议。腾讯和第三方对可能出现的纠纷在法律规定和约定的范围内各自承担责任。

+

7.3.2 腾讯不保证您在应用市场中添加的第三方产品或服务的安全性、准确性、有效性及其他不确定的风险,由此若引发的任何争议及损害,与腾讯无关,腾讯不承担任何责任。

+

八、【用户行为规范】

+

8.1【信息内容规范】

+

8.1.1 本条所述信息内容是指用户使用本软件及服务过程中所制作、复制、发布、传播的任何内容。

+

8.1.2 您理解并同意,腾讯蓝鲸智云一直致力于为用户提供完善的基础服务设施,您不得利用本软件及服务制作、复制、发布、传播如下干扰腾讯蓝鲸智云正常运营,以及侵犯其他用户或第三方合法权益的内容,包括但不限于:

+

8.1.2.1 发布、传送、传播、储存违反国家法律、危害国家安全统一、社会稳定、公序良俗、社会公德以及侮辱、诽谤、淫秽或含有任何性或性暗示的、暴力的内容;

+

8.1.2.2 发布、传送、传播、储存侵害他人名誉权、肖像权、知识产权、商业秘密等合法权利的内容;

+

8.1.2.3 涉及他人隐私、个人信息或资料的;

+

8.1.2.4 发表、传送、传播骚扰、广告信息及垃圾信息;

+

8.1.2.5 其他违反法律法规、政策及公序良俗、社会公德或干扰【腾讯蓝鲸智云】正常运营和侵犯其他用户或第三方合法权益内容的信息。

+

8.2【软件使用规范】

+

除非法律允许或腾讯书面许可,您使用本软件过程中不得从事下列行为:

+

8.2.1 删除本软件及其副本上关于著作权的信息;

+

8.2.2 对本软件进行反向工程、反向汇编、反向编译,或者以其他方式尝试发现本软件的源代码;

+

8.2.3 对腾讯拥有知识产权的内容进行使用、出租、出借、复制、修改、链接、转载、汇编、发表、出版、建立镜像站点等;

+

8.2.4 对本软件或者本软件运行过程中释放到任何终端内存中的数据、软件运行过程中客户端与服务器端的交互数据,以及本软件运行所必需的系统数据,进行复制、修改、 增加、删除、挂接运行或创作任何衍生作品,形式包括但不限于使用插件、外挂或非腾讯经授权的第三方工具/服务接入本软件和相关系统;

+

8.2.5 通过修改或伪造软件运行中的指令、数据,增加、删减、变动软件的功能或运行效果,或者将用于上述用途的软件、方法进行运营或向公众传播,无论这些行为是否为商业目的;

+

8.2.6 通过非腾讯开发、授权的第三方软件、插件、外挂、系统,登录或使用腾讯软件及服务,或制作、发布、传播上述工具;

+

8.2.7 自行或者授权他人、第三方软件对本软件及其组件、模块、数据进行干扰;

+

8.2.8 其他未经腾讯明示授权的行为。

+

8.3【服务运营规范】

+

除非法律允许或腾讯书面许可,您使用本服务过程中不得从事下列行为:

+

8.3.1 提交、发布虚假信息,或冒充、利用他人名义的;

+

8.3.2 诱导其他用户点击链接页面或分享信息的;

+

8.3.3 虚构事实、隐瞒真相以误导、欺骗他人的;

+

8.3.4 侵害他人名誉权、肖像权、知识产权、商业秘密等合法权利的;

+

8.3.5 未经腾讯书面许可利用帐号和任何功能,以及第三方运营平台进行推广或互相推广的;

+

8.3.6 利用帐号或本软件及服务从事任何违法犯罪活动的;

+

8.3.7 制作、发布与以上行为相关的方法、工具,或对此类方法、工具进行运营或传播,无论这些行为是否为商业目的;

+

8.3.8 其他违反法律法规规定、侵犯其他用户合法权益、干扰产品正常运营或腾讯未明示授权的行为。

+

8.4 【对自己行为负责】

+

您充分了解并同意,您必须为自己注册帐号下的一切行为负责,包括您所发表的任何内容以及由此产生的任何后果。您应对本服务中的内容自行加以判断,并承担因使用内容而引起的所有风险,包括因对内容的正确性、完整性或实用性的依赖而产生的风险。腾讯无法且不会对因前述风险而导致的任何损失或损害承担责任。

+

8.5【违约处理】

+

8.5.1 如果腾讯发现或收到他人举报或投诉用户违反本协议约定的,腾讯有权不经通知随时对相关内容进行删除,并视行为情节对违规帐号处以包括但不限于警告、限制或禁止使用全部或部分功能、帐号封禁直至注销的处罚,并公告处理结果。

+

8.5.2 您理解并同意,腾讯有权依合理判断对违反有关法律法规或本协议规定的行为进行处罚,对违法违规的任何用户采取适当的法律行动,并依据法律法规保存有关信息向有关部门报告等,用户应独自承担由此而产生的一切法律责任。

+

8.5.3 您理解并同意,因您违反本协议或相关服务条款的规定,导致或产生第三方主张的任何索赔、要求或损失,您应当独立承担责任;腾讯因此遭受损失的,您也应当一并赔偿。

+

九、【知识产权声明】

+

9.1 腾讯是本软件的知识产权权利人。本软件的一切著作权、商标权、专利权、商业秘密等知识产权,以及与本软件相关的所有信息内容(包括但不限于文字、图片、音频、视频、图表、界面设计、版面框架、有关数据或电子文档等)均受中华人民共和国法律法规和相应的国际条约保护,腾讯享有上述知识产权。

+

9.2 未经腾讯书面同意,您不得为任何商业或非商业目的自行或许可任何第三方实施、利用、转让上述知识产权,腾讯保留追究上述行为法律责任的权利。

+

十、【终端安全责任】

+

10.1 您理解并同意,本软件同大多数互联网软件一样,可能会受多种因素影响,包括但不限于用户原因、网络服务质量、社会环境等;也可能会受各种安全问题的侵扰,包括但不限于他人非法利用用户资料,进行现实中的骚扰;用户下载安装的其他软件或访问的其他网站中可能含有病毒、木马程序或其他恶意程序,威胁您的终端设备信息和数据安全,继而影响本软件的正常使用等。因此,您应加强信息安全及个人信息的保护意识,注意密码保护,以免遭受损失。

+

10.2 您不得制作、发布、使用、传播用于窃取其他用户帐号及个人信息、财产的恶意程序。

+

10.3 维护软件安全与正常使用是腾讯和您的共同责任,腾讯将按照行业标准合理审慎地采取必要技术措施保护您的终端设备信息和数据安全,但是您承认和同意腾讯并不能就此提供完全保证。

+

十一、【第三方软件或技术】

+

11.1 本软件可能会使用第三方软件或技术(包括本软件可能使用的开源代码和公共领域代码等,下同),这种使用已经获得合法授权。

+

11.2 本软件如果使用了第三方的软件或技术,腾讯将按照相关法规或约定,对相关的协议或其他文件,可能通过本协议附件、在本软件安装包特定文件夹中打包等形式进行展示,它们可能会以“软件使用许可协议”、“授权协议”、“开源代码许可证”或其他形式来表达。前述通过各种形式展现的相关协议或其他文件,均是本协议不可分割的组成部分,与本协议具有同等的法律效力,您应当遵守这些要求。如果您没有遵守这些要求,该第三方或者国家机关可能会对您提起诉讼、罚款或采取其他制裁措施,并要求腾讯给予协助,您应当自行承担法律责任。

+

11.3 如因本软件使用的第三方软件或技术引发的任何纠纷,应由该第三方负责解决,腾讯不承担任何责任。腾讯不对第三方软件或技术提供客服支持,若您需要获取支持,请与第三方联系。

+

十二、【其他】

+

12.1 您使用本软件即视为您已阅读并同意受本协议的约束。腾讯有权在必要时修改本协议条款。您可以在本软件的最新版本中查阅相关协议条款。本协议条款变更后,如果您继续使用本软件,即视为您已接受修改后的协议。如果您不接受修改后的协议,应当停止使用本软件。

+

12.2 本协议签订地为中华人民共和国广东省深圳市南山区。

+

12.3 本协议的成立、生效、履行、解释及纠纷解决,适用中华人民共和国大陆地区法律(不包括冲突法)。

+

12.4 若您和腾讯之间发生任何纠纷或争议,首先应友好协商解决;协商不成的,您同意将纠纷或争议提交本协议签订地有管辖权的人民法院管辖。

+

12.5 本协议所有条款的标题仅为阅读方便,本身并无实际涵义,不能作为本协议涵义解释的依据。

+

12.6 本协议条款无论因何种原因部分无效或不可执行,其余条款仍有效,对双方具有约束力。

+

12.7 本协议可能由多种语言书就。如果存在中文版本与其他语言的版本相冲突的地方,以中文版本为准。(正文完)

+

腾讯公司

+
+ {% endblocktrans %} +
+ +
+
\ No newline at end of file diff --git a/src/login/bklogin/templates/account/base.html b/src/login/bklogin/templates/account/base.html new file mode 100755 index 000000000..6af0109ba --- /dev/null +++ b/src/login/bklogin/templates/account/base.html @@ -0,0 +1,175 @@ +{% load i18n %} + + + + + + {% trans '用户管理|蓝鲸智云企业版' %} + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ {% block body_content %}{% endblock %} +
+ + + +
+ + + + + + + + + + + + + + + + + + + + {% block script %}{% endblock %} + + diff --git a/src/login/bklogin/templates/account/login.html b/src/login/bklogin/templates/account/login.html new file mode 100755 index 000000000..73b1e41cf --- /dev/null +++ b/src/login/bklogin/templates/account/login.html @@ -0,0 +1,122 @@ +{% load i18n %} + + + + + + + + + + {% trans '登录|蓝鲸智云企业版' %} + + +
+ + {% endif %} +
+ +
+ + + {% if "/plain/" not in APP_PATH %} + + {% endif %} + + + {% include "account/agreement.part" %} + +
+ {% trans '您的浏览器非Chrome,建议您使用最新版本的Chrome浏览,以保证最好的体验效果' %} +
+ +
+ {% trans '企业证书校验无效,请联系系统管理员处理' %} +
+ + + + + + + diff --git a/src/login/bklogin/templates/account/login_ce.html b/src/login/bklogin/templates/account/login_ce.html new file mode 100755 index 000000000..10f916015 --- /dev/null +++ b/src/login/bklogin/templates/account/login_ce.html @@ -0,0 +1,135 @@ +{% load i18n %} + + + + + + + + + + {% trans '登录|蓝鲸智云' %} + + {% if is_plain %} + + {% endif %} + + +
+
+
+ +
+
+ +
+
+ +
+ + +
+ + {% include "account/agreement.part" %} + +
+ {% trans '您的浏览器非Chrome,建议您使用最新版本的Chrome浏览,以保证最好的体验效果' %} +
+ + + + + + + + + + + + + diff --git a/src/login/bklogin/templates/account/no_right.html b/src/login/bklogin/templates/account/no_right.html new file mode 100755 index 000000000..ce1bc6059 --- /dev/null +++ b/src/login/bklogin/templates/account/no_right.html @@ -0,0 +1,8 @@ +{% extends "account/base.html" %} +{% load i18n %} +{% block body_content %} +
+
{% trans '你不是管理员, 没有用户管理的权限!' %}
+
{% trans '请找管理员申请权限!' %}
+
+{% endblock %} \ No newline at end of file diff --git a/src/login/bklogin/templates/account/user_table.part b/src/login/bklogin/templates/account/user_table.part new file mode 100755 index 000000000..b9d932039 --- /dev/null +++ b/src/login/bklogin/templates/account/user_table.part @@ -0,0 +1,116 @@ +{% load i18n %} + + + + + + + + + + + + + + {% if records %} + {% for obj in records %} + + + + + + + + + {% endfor %} + {% else %} + + {% endif %} + +
{% trans '用户名' %}{% trans '中文名' %}{% trans '联系电话' %}{% trans '常用邮箱' %}{% trans '角色' %}{% trans '操作' %}
+ + + + + + + + + {% if request.user.is_superuser %} + + {% else %} + + {% endif %} + + + + + + {% if request.user.is_superuser %} + + {% endif %} +
{% trans '没有数据' %}
+ +
+ + +
diff --git a/src/login/bklogin/templates/account/users.html b/src/login/bklogin/templates/account/users.html new file mode 100755 index 000000000..049d69ade --- /dev/null +++ b/src/login/bklogin/templates/account/users.html @@ -0,0 +1,102 @@ +{% extends "account/base.html" %} +{% load i18n %} +{% block body_content %} +