Compare commits

..

449 Commits

Author SHA1 Message Date
pycook
62829c885b release v2.3.3 2023-09-15 17:57:39 +08:00
pycook
260aed6462 dashboard ui update 2023-09-15 17:36:10 +08:00
simontigers
3841999cca feat: init resource for backend (#176) 2023-09-15 15:30:30 +08:00
pycook
14c03ce5d2 enhance dashboard 2023-09-15 15:26:20 +08:00
pycook
f463ecd6e6 cmdb-api/api/lib/resp_format.py 2023-09-12 20:01:30 +08:00
pycook
adc0cfd5c5 Detect circular dependencies when adding CIType relationships 2023-09-12 20:00:56 +08:00
wang-liang0615
086481657e 计算属性 触发计算 (#174) 2023-09-11 19:16:05 +08:00
pycook
d2f84ae3dc fix upload template and add /api/v0.1/attributes/<int:attr_id>/calc_computed_attribute 2023-09-11 19:15:31 +08:00
pycook
0196c8a82c release 2.3.2 2023-09-07 13:44:51 +08:00
pycook
bed2323fc1 Merge pull request #172 from veops/dev_ui
新建ci及批量导入时,新建关系
2023-09-07 11:04:49 +08:00
wang-liang0615
be9b308f56 新建ci及批量导入时,新建关系 2023-09-07 10:25:18 +08:00
pycook
8ba658ea1b Merge branch 'master' of github.com:veops/cmdb 2023-09-07 10:12:55 +08:00
pycook
0aa668cfa0 Add CI relationship when creating CI, the text value removes the escape 2023-09-07 10:12:42 +08:00
pycook
e20fd33a53 Merge pull request #171 from ronething/fix/makefile
optimize: makefile help
2023-09-05 20:34:16 +08:00
ashing
7462de63de fix: review
Signed-off-by: ashing <axingfly@gmail.com>
2023-09-05 20:33:07 +08:00
ashing
5f9ba069ad fix: review
Signed-off-by: ashing <axingfly@gmail.com>
2023-09-05 20:29:29 +08:00
ashing
5dc0d95ff8 fix: review
Signed-off-by: ashing <axingfly@gmail.com>
2023-09-05 20:21:20 +08:00
ashing
e5536b76e6 optimize: makefile help
Signed-off-by: ashing <axingfly@gmail.com>
2023-09-05 20:06:31 +08:00
pycook
8b044efd4e Merge pull request #170 from ronething/feat/xx
feat: support docker deploy mysql and redis
2023-09-05 19:28:47 +08:00
ivonGwy
747b5bf494 Merge pull request #169 from veops/doc
add document link
2023-09-05 15:41:52 +08:00
ivonGwy
21067022f6 add document link 2023-09-05 15:40:31 +08:00
ashing
4102c44fb2 feat: support docker deploy mysql and redis
Signed-off-by: ashing <axingfly@gmail.com>
2023-09-05 15:26:50 +08:00
wang-liang0615
600f95ce18 Merge pull request #168 from veops/dev_ui
UI更新
2023-09-05 15:23:43 +08:00
wang-liang0615
950fd38044 Merge branch 'master' of github.com:veops/cmdb into dev_ui 2023-09-05 15:22:18 +08:00
wang-liang0615
01085615b5 模型关联 展示反向关系 2023-09-05 15:22:08 +08:00
pycook
734f1940f9 Merge branch 'master' of github.com:veops/cmdb 2023-09-05 14:49:53 +08:00
pycook
c25c1e4e4b move Dockerfile to docs 2023-09-05 14:49:34 +08:00
wang-liang0615
826a8306d3 Merge pull request #167 from veops/dev_ui
sub menu color
2023-09-04 16:34:26 +08:00
wang-liang0615
740aae573e sub menu color 2023-09-04 16:33:35 +08:00
wang-liang0615
17828a7631 Merge pull request #166 from veops/dev_ui
ui更新
2023-09-04 13:15:27 +08:00
wang-liang0615
02cb497bdc Merge branch 'master' of github.com:veops/cmdb into dev_ui 2023-09-04 13:14:35 +08:00
wang-liang0615
05a7dc41ee sidebar 2023-09-04 13:14:11 +08:00
pycook
459c70ba2d import format 2023-09-02 12:09:41 +08:00
pycook
774f42ac34 format 2023-09-01 18:07:44 +08:00
pycook
420029a5e2 fix delete choice values 2023-08-31 16:02:24 +08:00
pycook
ab8acbfd20 fix delete choice values 2023-08-31 15:18:15 +08:00
wang-liang0615
4468b6a8de Merge pull request #165 from veops/dev_ui
proxy
2023-08-31 13:31:26 +08:00
wang-liang0615
6bf145d085 proxy 2023-08-31 13:28:15 +08:00
pycook
42b1e47e76 Merge pull request #162 from simontigers/cmdb_icon_manage
feat: add cmdb custom icon manage
2023-08-31 11:15:09 +08:00
pycook
673134003a Merge pull request #163 from veops/dev_ui
支持上传自定义图标
2023-08-31 11:14:42 +08:00
hu.sima
ef67885571 feat: add cmdb custom icon manage 2023-08-31 10:49:56 +08:00
wang-liang0615
075bf7217f 支持上传自定义图标 2023-08-31 10:05:11 +08:00
pycook
3b7b8f435c fix update attribute 2023-08-30 13:34:10 +08:00
pycook
2b7f6aeef3 Merge branch 'master' of github.com:veops/cmdb 2023-08-29 14:49:21 +08:00
pycook
544fac8aca The default value of USE_ACL is set to True 2023-08-29 14:49:09 +08:00
pycook
3d0a56ec8c Merge pull request #161 from simontigers/common_setting_format
fix: company info create
2023-08-29 11:01:25 +08:00
hu.sima
d2d8482052 fix: company info create 2023-08-29 10:56:48 +08:00
pycook
a0afae8d2e Merge branch 'master' of github.com:veops/cmdb 2023-08-25 11:01:24 +08:00
pycook
9f3da68636 update ad_ci when deleting ci 2023-08-25 10:59:38 +08:00
wang-liang0615
24b955c288 Merge pull request #160 from veops/dev_ui
前端更新
2023-08-25 10:12:31 +08:00
wang-liang0615
a07b2d37ec fix 新增类型回车键发送两次请求 2023-08-25 10:11:09 +08:00
wang-liang0615
c86fcb4e7b fix 新增类型回车键发送两次请求 2023-08-25 10:08:04 +08:00
pycook
ca7964f24b Merge pull request #158 from EvanSung/perf_20230824_optimize_ad_ci_relation
perf(ad_ci_relation): optimize ad_ci relation
2023-08-24 16:26:43 +08:00
EvanSung
c42ac634fb perf(ad_ci_relation): optimize ad_ci relation 2023-08-24 14:16:12 +08:00
pycook
a6fc3341ce docker-compose add flask db-setup 2023-08-24 11:32:09 +08:00
pycook
fc3f2e25f3 vxe-table-plugin-export-xlsx==2.0.0 2023-08-24 11:06:28 +08:00
pycook
511a5f70c6 add config CACHE_REDIS_PASSWORD and fix delete ci_type 2023-08-23 18:05:28 +08:00
pycook
f8ff4d5e45 fix update ci 2023-08-22 11:34:40 +08:00
pycook
3ab72cceaf Register api and commands with absolute paths 2023-08-21 20:08:23 +08:00
pycook
4ab7e3c70c fix merge conflict 2023-08-21 11:55:49 +08:00
pycook
a7fe75f7df fix g.user 2023-08-21 11:54:33 +08:00
pycook
3474a71a75 version: 2.3.1 2023-08-20 11:24:53 +08:00
pycook
6531baff64 lint 2023-08-20 11:23:55 +08:00
pycook
ed5936250f Merge pull request #157 from EvanSung/fix_20230817_guser_issue
fix(acl): g user issue
2023-08-17 22:12:45 +08:00
EvanSung
52c32e2ab1 fix(acl): g user issue 2023-08-17 18:40:45 +08:00
pycook
d3224625b6 fix MyJSONEncoder 2023-08-16 21:28:27 +08:00
pycook
f158c7e33a Merge pull request #155 from veops/dev_ui
前端更新
2023-08-16 13:01:13 +08:00
pycook
6dc12bb6ac Merge branch 'master' of github.com:veops/cmdb 2023-08-16 13:00:44 +08:00
pycook
b33ae16c00 Delete user without soft delete 2023-08-16 13:00:30 +08:00
wang-liang0615
2caffc2670 Merge branch 'master' of github.com:veops/cmdb into dev_ui 2023-08-16 10:09:47 +08:00
wang-liang0615
f28af51007 delete user 2023-08-16 10:09:25 +08:00
pycook
3a0369559f Merge pull request #154 from simontigers/common_setting_format
fix: init-import-user-from-acl
2023-08-15 20:47:36 +08:00
hu.sima
a74a2c5a94 fix: init-import-user-from-acl 2023-08-15 20:45:28 +08:00
pycook
9fbcb2838e Merge pull request #153 from simontigers/common_setting_format
fix: import_user_from_acl
2023-08-15 20:25:09 +08:00
hu.sima
60a445b972 fix: import_user_from_acl 2023-08-15 20:19:45 +08:00
pycook
bfdd7b6a0e Merge branch 'master' of github.com:veops/cmdb 2023-08-15 19:48:11 +08:00
pycook
ab093d2493 [update] delete roles, users, attributes 2023-08-15 19:47:59 +08:00
wang-liang0615
315a578a31 Merge pull request #152 from veops/dev_ui
前端更新
2023-08-15 19:47:03 +08:00
wang-liang0615
1e16dc5e5b 属性库 2023-08-15 19:34:17 +08:00
wang-liang0615
f67e196acf 属性库 2023-08-15 19:26:49 +08:00
wang-liang0615
439e25d5dd 属性库 2023-08-15 19:21:09 +08:00
wang-liang0615
ea59c0d71f 属性库 2023-08-15 19:10:26 +08:00
wang-liang0615
1137127aab 后台管理-模型关联 关系删除&&筛选 2023-08-15 15:02:46 +08:00
pycook
4ad1b5282e update gitattributes 2023-08-15 13:41:45 +08:00
wang-liang0615
cdd5e4d9aa Merge pull request #150 from EvanSung/optimize_20230810_acl_resource_fe
refactor(fe): reduce the width of resource mgt table
2023-08-10 19:32:49 +08:00
pycook
432de5e847 Merge pull request #148 from simontigers/common_setting_format
fix: default arg value
2023-08-10 19:31:18 +08:00
pycook
3a2339765a Merge pull request #149 from veops/dev_ui
ui更新:password
2023-08-10 19:28:24 +08:00
EvanSung
b5a2af7420 refactor(fe): reduce the width of resource mgt table 2023-08-10 19:23:41 +08:00
wang-liang0615
8b267613d6 Merge branch 'master' of github.com:veops/cmdb into dev_ui 2023-08-10 19:21:28 +08:00
wang-liang0615
b365eb27f6 增加密码明文传输 2023-08-10 19:21:10 +08:00
hu.sima
2125f020b5 fix: default arg value 2023-08-10 19:05:56 +08:00
pycook
ea762e35a0 Merge pull request #147 from simontigers/common_setting_format
fix: remove useless
2023-08-10 19:01:25 +08:00
hu.sima
f11aadf6d4 fix: remove useless 2023-08-10 18:55:32 +08:00
pycook
9cbf133b9f Merge pull request #146 from simontigers/common_setting_format
Common setting format
2023-08-10 18:23:24 +08:00
hu.sima
95e8f9de74 fix: remove unused column 2023-08-10 16:29:52 +08:00
hu.sima
26792147ae style: format common setting 2023-08-10 15:30:01 +08:00
pycook
4f9b581c2e Merge pull request #145 from EvanSung/optimize_20230810_auth_require
optimize(auth): auth request json
2023-08-10 11:24:23 +08:00
EvanSung
e2b1cb3003 optimize(auth): auth request json 2023-08-10 10:43:59 +08:00
pycook
f75a85b48a fix celery config 2023-08-08 16:33:24 +08:00
pycook
313fc80e54 Merge branch 'master' of github.com:veops/cmdb 2023-08-08 13:16:14 +08:00
pycook
e0666689e5 upgrade celery 2023-08-08 13:16:07 +08:00
pycook
7a9fd4f9d6 Merge pull request #144 from veops/dev_ui
UI更新:fix preferenceList=>attrList
2023-08-08 09:21:10 +08:00
wang-liang0615
2fd706be85 Merge branch 'master' of github.com:veops/cmdb into dev_ui 2023-08-08 09:11:24 +08:00
wang-liang0615
3df51bb670 fix preferenceList=>attrList 2023-08-08 09:11:03 +08:00
pycook
9bbbcbe6dc upgrade flask to 2.3.2 and replace g.user with current_user 2023-08-06 21:54:18 +08:00
pycook
16d6b40e8d Merge pull request #138 from lovvvve/fix_ldap
fix ldap login
2023-08-04 11:31:58 +08:00
pycook
ef2d3812a2 Merge pull request #142 from veops/dev_ui
ci 批量更新和删除的异步处理
2023-08-04 09:27:55 +08:00
wang-liang0615
bc653efd04 Merge branch 'master' of github.com:veops/cmdb into dev_ui 2023-08-03 16:54:47 +08:00
wang-liang0615
d891d7365d ci 批量更新和删除的异步处理 2023-08-03 16:54:27 +08:00
pycook
9953b2fc98 Merge pull request #139 from EvanSung/fix-post-acltrigger-session-invalid
fix(trigger): session invalid issue
2023-08-02 19:33:05 +08:00
songbing01249
8de54812dc fix(trigger): session invalid issue 2023-08-02 18:22:42 +08:00
lovvvve
eb7d52cf35 fix ldap login 2023-08-01 11:27:29 +00:00
pycook
6c4a5f2f6b Merge pull request #134 from veops/dependabot/pip/cmdb-api/pillow-9.3.0
Bump pillow from 9.2.0 to 9.3.0 in /cmdb-api
2023-08-01 15:57:02 +08:00
pycook
17c5d4538b Merge pull request #135 from simontigers/remove_pandas
fix: remove pandas
2023-08-01 15:55:15 +08:00
hu.sima
6c3e3f9eed fix: remove pandas 2023-08-01 15:32:44 +08:00
dependabot[bot]
b0494adc17 Bump pillow from 9.2.0 to 9.3.0 in /cmdb-api
Bumps [pillow](https://github.com/python-pillow/Pillow) from 9.2.0 to 9.3.0.
- [Release notes](https://github.com/python-pillow/Pillow/releases)
- [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst)
- [Commits](https://github.com/python-pillow/Pillow/compare/9.2.0...9.3.0)

---
updated-dependencies:
- dependency-name: pillow
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-08-01 05:51:16 +00:00
pycook
fc133f2ae9 Merge branch 'master' of github.com:veops/cmdb 2023-08-01 13:47:34 +08:00
pycook
ac6e3a0318 fix dependabot alerts 2023-08-01 13:47:11 +08:00
pycook
404ec976cc fix dependabot alerts 2023-08-01 13:46:47 +08:00
pycook
4211bbcbc9 Merge pull request #130 from veops/dependabot/pip/cmdb-api/certifi-2023.7.22
Bump certifi from 2023.5.7 to 2023.7.22 in /cmdb-api
2023-08-01 13:14:25 +08:00
pycook
0158636671 Merge pull request #132 from veops/dev_ui
删除角色相关
2023-07-31 19:54:19 +08:00
wang-liang0615
d986bc3bbc 删除角色相关 2023-07-31 19:52:06 +08:00
pycook
044b820548 Merge branch 'master' of github.com:veops/cmdb 2023-07-31 18:39:46 +08:00
pycook
536daa6d4f fix delete ci_type 2023-07-31 18:39:33 +08:00
pycook
b0620b043b Merge pull request #131 from veops/dev_ui
前端acl
2023-07-28 18:03:36 +08:00
wang-liang0615
a88c9cf7f7 common-setting 2023-07-27 15:47:13 +08:00
wang-liang0615
be50f505d1 acl 2023-07-27 15:30:27 +08:00
wang-liang0615
0bb4f633d6 fix acl change page size 2023-07-27 15:08:25 +08:00
dependabot[bot]
78b521f3af Bump certifi from 2023.5.7 to 2023.7.22 in /cmdb-api
Bumps [certifi](https://github.com/certifi/python-certifi) from 2023.5.7 to 2023.7.22.
- [Commits](https://github.com/certifi/python-certifi/compare/2023.05.07...2023.07.22)

---
updated-dependencies:
- dependency-name: certifi
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-25 23:35:30 +00:00
pycook
77bc850d4a Merge pull request #129 from veops/dev_ui
前端更新
2023-07-25 18:19:47 +08:00
wang-liang0615
e52f201ba1 Merge branch 'master' of github.com:veops/cmdb into dev_ui 2023-07-25 13:11:03 +08:00
wang-liang0615
64aea424dc 授权高亮提示 2023-07-25 13:10:45 +08:00
pycook
0655b0e9eb add command cmdb-index-table-upgrade 2023-07-25 10:31:30 +08:00
wang-liang0615
cce0649299 style 新建属性行错乱 2023-07-25 10:18:22 +08:00
pycook
52574c64cc 废弃3个表: c_value_datetime c_value_floats c_value_integers, time类型属性值增加写入校验 2023-07-24 21:55:00 +08:00
pycook
fb904b01a6 禁止删除唯一标识的属性 2023-07-21 15:58:41 +08:00
pycook
63af79ec45 Merge branch 'master' of github.com:veops/cmdb 2023-07-20 18:37:14 +08:00
pycook
38af86317a fix docker-compose 2023-07-20 18:36:32 +08:00
pycook
03bac86588 Merge pull request #127 from veops/dev_ui
fix currentValueType
2023-07-20 15:39:11 +08:00
wang-liang0615
130b68cadd fix currentValueType 2023-07-20 15:30:12 +08:00
pycook
65000f8141 更新架构图 2023-07-20 11:01:25 +08:00
pycook
23692ad50b update readme 2023-07-20 11:01:25 +08:00
pycook
16cd34e8b8 update readme 2023-07-20 11:01:25 +08:00
pycook
985f67ee47 lint 2023-07-20 11:01:25 +08:00
wang-liang0615
8d95f8d57d 角色授权 2023-07-20 11:01:25 +08:00
pycook
cf6230008d 清理空间 2023-07-20 11:01:25 +08:00
songbing01249
ec97fa84d8 fix(ci_type_group_manager): fix resources issues 2023-07-20 11:01:25 +08:00
pycook
76f074704b update cmdb_api.md 2023-07-20 10:56:58 +08:00
wang-liang0615
e5addab3af 删除fullscreen相关代码 2023-07-19 17:46:27 +08:00
wang-liang0615
1c6be9e281 format 2023-07-19 15:36:46 +08:00
wang-liang0615
9552892c68 ops table getVxetableRef 2023-07-19 14:39:57 +08:00
wang-liang0615
b59e1af318 编译 acl 2023-07-19 13:52:24 +08:00
wang-liang0615
d164d883ab Merge branch 'master' of github.com:veops/cmdb into dev_ui 2023-07-18 15:16:50 +08:00
wang-liang0615
1fef160d9e 删除不必要文件 2023-07-18 15:16:32 +08:00
wang-liang0615
2e537d390a acl 样式升级 2023-07-18 15:14:35 +08:00
pycook
5b9fe15afa Merge pull request #119 from veops/dev_ui
模型属性 is_index
2023-07-17 18:15:28 +08:00
wang-liang0615
89fa5f2243 Merge branch 'master' of github.com:veops/cmdb into dev_ui 2023-07-17 17:21:26 +08:00
wang-liang0615
652a5c7fb8 模型属性 is_index 2023-07-17 17:19:44 +08:00
pycook
afb6adec89 update local.md 2023-07-17 13:32:34 +08:00
pycook
a9db4285ab Merge pull request #118 from veops/dev_ui
Dev UI
2023-07-14 18:06:02 +08:00
wang-liang0615
a04bdc29a5 删除usedfc 2023-07-14 15:20:58 +08:00
wang-liang0615
91e0e076a7 删除usedfc 2023-07-14 15:20:36 +08:00
wang-liang0615
339a7b857e acl 前端 2023-07-14 14:34:35 +08:00
pycook
e86e5ad1fd PyJWT==2.4.0 2023-07-13 17:07:33 +08:00
pycook
c50a69de77 Merge pull request #117 from lovvvve/patch-4
Update click_cmdb.py
2023-07-13 15:51:36 +08:00
lovvvve
4d16e9e6d9 Update click_cmdb.py
add-user remove --is_admin
2023-07-13 15:49:35 +08:00
pycook
fcea4dcb9f Merge pull request #116 from lovvvve/patch-3
Update click_cmdb.py
2023-07-13 15:23:24 +08:00
lovvvve
f98fd24c62 Update click_cmdb.py
add-user  remove --is_admin
2023-07-13 15:18:47 +08:00
pycook
f10eeb8439 update README 2023-07-13 09:34:17 +08:00
pycook
f070948122 Merge pull request #115 from veops/doc
change screenshot image
2023-07-12 17:22:58 +08:00
ivonGwy
4112bcf547 change screenshot image 2023-07-12 17:21:10 +08:00
pycook
2292756bf7 Merge pull request #114 from veops/doc
change image size
2023-07-12 17:11:20 +08:00
ivonGwy
93e2483974 change image size 2023-07-12 17:07:17 +08:00
pycook
fbb4fcc255 Merge pull request #113 from veops/doc
add qrcode for gzh
2023-07-12 16:49:12 +08:00
ivonGwy
fc77241006 add qrcode for gzh 2023-07-12 16:12:17 +08:00
pycook
0d04ad7d90 update requirements 2023-07-12 15:32:46 +08:00
pycook
e6290e49ea update docker-compose 2023-07-12 11:59:51 +08:00
pycook
97aa2e0ebe remove .gitattributes 2023-07-12 11:50:05 +08:00
pycook
939d9dc3cd md format 2023-07-12 10:14:47 +08:00
pycook
576d2e3bc4 Update README.md 2023-07-12 10:09:11 +08:00
pycook
9a40246d29 Update README.md 2023-07-12 10:01:15 +08:00
pycook
044f95c3be docker-compose 构建后的默认账号密码 2023-07-11 19:40:40 +08:00
pycook
a386de355e docker-compose is ok 2023-07-11 18:22:17 +08:00
pycook
b93afc1790 docker-compose is ok 2023-07-11 18:12:22 +08:00
pycook
77d89677ef 前后端全面升级 2023-07-10 20:13:39 +08:00
pycook
7ec6775f03 友链Spug 2023-07-10 20:07:31 +08:00
pycook
98cc853dbc 前后端全面升级 2023-07-10 20:07:20 +08:00
pycook
f57ff80099 Merge pull request #90 from lovvvve/patch-2
fix: 🐛 db search
2021-11-10 20:18:18 +08:00
lovvvve
51e4b5dd8f fix: 🐛 db search
Escape ":" character in SQLAlchemy
2021-11-10 18:56:42 +08:00
pycook
dbf44a020b Merge pull request #77 from x-7/x-7-patch-1
cmdb-api:add attr check in ci_manager update method
2021-04-22 09:14:33 +08:00
x-7
8e578797ef Update ci.py
cmdb-api:add attr check in ci_manager update method
2021-04-21 19:17:05 +08:00
pycook
158de4b946 Merge pull request #69 from lovvvve/patch-1
CiManager.add and AttributeValueManager.create_or_update_attr_value update
2021-01-28 17:09:01 +08:00
lovvvve
3cf234d49e Update value.py
value type 是 int 或 float 时 value 值等于 0 是会删除 的 BUG
2021-01-28 16:57:20 +08:00
lovvvve
a7debc1b3b Update ci.py
兼容 py2
2021-01-28 16:56:04 +08:00
lovvvve
9268da2ffa Update value.py 2021-01-28 16:46:19 +08:00
lovvvve
cfcb092478 Update value.py
feat(AttributeValueManager.create_or_update_attr_value()): AttributeValue update skip The same value
2021-01-28 16:32:53 +08:00
lovvvve
0d8b41b64a Update value.py
feat(AttributeValueManager.create_or_update_attr_value()): AttributeValue update skip The same value
2021-01-28 16:30:17 +08:00
lovvvve
d85715793f Update ci.py
feat(CiManager.add()): Check the attribute is in the ci_type attributes list
2021-01-28 16:27:56 +08:00
pycook
afbdbe4682 Merge pull request #65 from shaohaojiecoder/stable
Stable
2020-12-14 09:20:15 +08:00
shaohaojiecoder
e629abebb7 remove weeds 2020-12-13 16:58:06 +08:00
shaohaojiecoder
029c12365a delay render 2020-12-13 16:42:17 +08:00
pycook
4d000d9805 yarn.lock update 2020-11-22 11:45:33 +08:00
pycook
f1fc66bd2c Fix github security 2020-11-22 11:42:17 +08:00
pycook
d6af4af1d1 upgrade ui packages 2020-11-22 11:13:46 +08:00
pycook
7fe2bdca5f Create codeql-analysis.yml 2020-11-22 10:45:45 +08:00
pycook
1432131d2b Merge pull request #59 from pycook/dependabot/npm_and_yarn/cmdb-ui/dot-prop-4.2.1
Bump dot-prop from 4.2.0 to 4.2.1 in /cmdb-ui
2020-11-22 10:28:34 +08:00
pycook
bc94d039f5 Merge pull request #52 from pycook/dependabot/npm_and_yarn/cmdb-ui/elliptic-6.5.3
Bump elliptic from 6.4.1 to 6.5.3 in /cmdb-ui
2020-11-22 10:27:49 +08:00
pycook
5abafed9c8 Merge pull request #54 from pycook/dependabot/npm_and_yarn/cmdb-ui/quill-1.3.7
Bump quill from 1.3.6 to 1.3.7 in /cmdb-ui
2020-11-22 10:27:15 +08:00
dependabot[bot]
04e249feac Bump dot-prop from 4.2.0 to 4.2.1 in /cmdb-ui
Bumps [dot-prop](https://github.com/sindresorhus/dot-prop) from 4.2.0 to 4.2.1.
- [Release notes](https://github.com/sindresorhus/dot-prop/releases)
- [Commits](https://github.com/sindresorhus/dot-prop/compare/v4.2.0...v4.2.1)

Signed-off-by: dependabot[bot] <support@github.com>
2020-11-22 02:26:41 +00:00
pycook
ef3e6bc6b0 Merge pull request #55 from pycook/dependabot/npm_and_yarn/cmdb-ui/handlebars-4.7.6
Bump handlebars from 4.4.5 to 4.7.6 in /cmdb-ui
2020-11-22 10:26:38 +08:00
pycook
d9d5f8f818 Merge pull request #56 from pycook/dependabot/npm_and_yarn/cmdb-ui/http-proxy-1.18.1
Bump http-proxy from 1.17.0 to 1.18.1 in /cmdb-ui
2020-11-22 10:25:58 +08:00
dependabot[bot]
578da0807c Bump http-proxy from 1.17.0 to 1.18.1 in /cmdb-ui
Bumps [http-proxy](https://github.com/http-party/node-http-proxy) from 1.17.0 to 1.18.1.
- [Release notes](https://github.com/http-party/node-http-proxy/releases)
- [Changelog](https://github.com/http-party/node-http-proxy/blob/master/CHANGELOG.md)
- [Commits](https://github.com/http-party/node-http-proxy/compare/1.17.0...1.18.1)

Signed-off-by: dependabot[bot] <support@github.com>
2020-09-11 00:19:27 +00:00
dependabot[bot]
3eb35f5497 Bump handlebars from 4.4.5 to 4.7.6 in /cmdb-ui
Bumps [handlebars](https://github.com/wycats/handlebars.js) from 4.4.5 to 4.7.6.
- [Release notes](https://github.com/wycats/handlebars.js/releases)
- [Changelog](https://github.com/handlebars-lang/handlebars.js/blob/master/release-notes.md)
- [Commits](https://github.com/wycats/handlebars.js/compare/v4.4.5...v4.7.6)

Signed-off-by: dependabot[bot] <support@github.com>
2020-09-10 03:19:57 +00:00
dependabot[bot]
9669ad04cd Bump quill from 1.3.6 to 1.3.7 in /cmdb-ui
Bumps [quill](https://github.com/quilljs/quill) from 1.3.6 to 1.3.7.
- [Release notes](https://github.com/quilljs/quill/releases)
- [Changelog](https://github.com/quilljs/quill/blob/v1.3.7/CHANGELOG.md)
- [Commits](https://github.com/quilljs/quill/compare/v1.3.6...v1.3.7)

Signed-off-by: dependabot[bot] <support@github.com>
2020-09-04 00:38:10 +00:00
dependabot[bot]
70214807ca Bump elliptic from 6.4.1 to 6.5.3 in /cmdb-ui
Bumps [elliptic](https://github.com/indutny/elliptic) from 6.4.1 to 6.5.3.
- [Release notes](https://github.com/indutny/elliptic/releases)
- [Commits](https://github.com/indutny/elliptic/compare/v6.4.1...v6.5.3)

Signed-off-by: dependabot[bot] <support@github.com>
2020-08-01 08:41:43 +00:00
pycook
7c1c309f7a Merge pull request #51 from pycook/dependabot/npm_and_yarn/cmdb-ui/lodash-4.17.19
Bump lodash from 4.17.14 to 4.17.19 in /cmdb-ui
2020-07-21 18:17:46 +08:00
dependabot[bot]
9b9799ff5e Bump lodash from 4.17.14 to 4.17.19 in /cmdb-ui
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.14 to 4.17.19.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.14...4.17.19)

Signed-off-by: dependabot[bot] <support@github.com>
2020-07-19 03:35:06 +00:00
pycook
b2578b61fa add command: add-user | del-user 2020-06-11 21:37:41 +08:00
pycook
619f47ae13 Merge pull request #49 from pycook/dependabot/npm_and_yarn/cmdb-ui/websocket-extensions-0.1.4
Bump websocket-extensions from 0.1.3 to 0.1.4 in /cmdb-ui
2020-06-08 18:37:31 +08:00
dependabot[bot]
37c5e31799 Bump websocket-extensions from 0.1.3 to 0.1.4 in /cmdb-ui
Bumps [websocket-extensions](https://github.com/faye/websocket-extensions-node) from 0.1.3 to 0.1.4.
- [Release notes](https://github.com/faye/websocket-extensions-node/releases)
- [Changelog](https://github.com/faye/websocket-extensions-node/blob/master/CHANGELOG.md)
- [Commits](https://github.com/faye/websocket-extensions-node/compare/0.1.3...0.1.4)

Signed-off-by: dependabot[bot] <support@github.com>
2020-06-07 15:54:30 +00:00
pycook
ab70b2a655 Merge pull request #48 from lovvvve/master
fix(sso login): sso login redirect
2020-06-01 12:05:05 +08:00
Lovvvve
c285606f4a fix(sso login): sso login redirect 2020-06-01 12:01:55 +08:00
Lovvvve
6d3611bd73 fix(sso login): sso login redirect 2020-06-01 11:48:21 +08:00
pycook
764f6a07e0 fix merge conflict 2020-05-28 20:39:47 +08:00
pycook
ae8d487af4 Readme is in Chinese by default
Committer: pycook <pycook@126.com>

Author:    pycook <pycook@126.com>
2020-05-28 20:35:11 +08:00
pycook
87c6554555 update readme 2020-05-28 20:28:49 +08:00
pycook
f5671c2a2a Fix: spelling mistakes 2020-05-28 20:28:49 +08:00
pycook
43ad3dfa7b release version 2.1 2020-05-28 20:28:49 +08:00
pycook
29fa17a0b8 update readme 2020-04-10 17:22:33 +08:00
pycook
5191d6ed73 Merge branch 'master' of https://github.com/pycook/cmdb 2020-04-07 18:03:03 +08:00
pycook
8348f8e7b1 Fix the judgment of app admin 2020-04-07 18:02:26 +08:00
pycook
75c48a0807 Fix: spelling mistakes 2020-04-01 21:40:51 +08:00
pycook
5b38385f7e release version 2.1 2020-04-01 21:20:47 +08:00
pycook
036e1d236b auth with ldap 2020-04-01 20:30:44 +08:00
pycook
c31be0f753 UI: batch update relation 2020-04-01 11:09:41 +08:00
pycook
764d2fac3f add .eslintrc.js 2020-03-26 17:35:26 +08:00
pycook
f4079e9c3e Merge pull request #42 from pycook/dependabot/npm_and_yarn/cmdb-ui/yarn-1.22.0
Bump yarn from 1.21.1 to 1.22.0 in /cmdb-ui
2020-03-26 17:29:53 +08:00
pycook
2a0ed72235 fix: delete attribute 2020-03-23 15:49:33 +08:00
dependabot[bot]
9e803ae4c7 Bump yarn from 1.21.1 to 1.22.0 in /cmdb-ui
Bumps [yarn](https://github.com/yarnpkg/yarn) from 1.21.1 to 1.22.0.
- [Release notes](https://github.com/yarnpkg/yarn/releases)
- [Changelog](https://github.com/yarnpkg/yarn/blob/master/CHANGELOG.md)
- [Commits](https://github.com/yarnpkg/yarn/compare/v1.21.1...v1.22.0)

Signed-off-by: dependabot[bot] <support@github.com>
2020-03-22 03:14:30 +00:00
pycook
bebdb61adf update deps 2020-03-22 11:14:09 +08:00
pycook
f49cad771b Merge pull request #41 from pycook/dependabot/npm_and_yarn/cmdb-ui/acorn-5.7.4
Bump acorn from 5.7.3 to 5.7.4 in /cmdb-ui
2020-03-15 13:06:51 +08:00
dependabot[bot]
a5b4fbda40 Bump acorn from 5.7.3 to 5.7.4 in /cmdb-ui
Bumps [acorn](https://github.com/acornjs/acorn) from 5.7.3 to 5.7.4.
- [Release notes](https://github.com/acornjs/acorn/releases)
- [Commits](https://github.com/acornjs/acorn/compare/5.7.3...5.7.4)

Signed-off-by: dependabot[bot] <support@github.com>
2020-03-14 18:55:18 +00:00
pycook
2cce2d5cf2 Fix: permission management 2020-03-13 10:30:21 +08:00
pycook
e720b7af66 Merge branch 'develop' of https://github.com/pycook/cmdb into develop 2020-03-10 17:03:23 +08:00
pycook
09e4a5111b Merge pull request #40 from OhBonsai/develop
chore: use wait script to hang api before cache/db/es started
2020-03-10 17:01:58 +08:00
penzai
3539b12503 chore: use wait script to hang api before cache/db/es started 2020-03-10 16:49:40 +08:00
pycook
21d8673b5d Merge pull request #38 from AngrygrayWolf/dev
Change the requirements to support python3.8
2020-03-07 22:06:25 +08:00
what
7154426dc7 Change Pipfile 2020-03-07 21:51:37 +08:00
what
ca75c7dcd0 Change the requirements to support python3.8 2020-03-07 21:20:46 +08:00
pycook
194a2254a6 fix case sensitive of ES search 2020-02-28 14:32:51 +08:00
pycook
26abad14d0 Merge branch 'develop' of https://github.com/pycook/cmdb into develop 2020-02-23 23:13:59 +08:00
pycook
1521a71f9c alter table c_preference_relation_views column name varchar(64) 2020-02-23 23:13:22 +08:00
pycook
d425b455f1 Merge pull request #36 from OhBonsai/develop
test: add ci and ci relation crud test cases
2020-02-23 23:12:36 +08:00
pycook
230307474b version 2.1 and update readme 2020-02-23 20:21:13 +08:00
pycook
69d6b40e39 / redirect to /relation_views 2020-02-23 18:53:28 +08:00
pycook
5dc2f89e7f fix i18n 2020-02-23 18:41:23 +08:00
pycook
9eaca4d6a0 add library future 2020-02-21 23:29:20 +08:00
pycook
3680a462f5 Remove Chinese comments 2020-02-21 23:14:26 +08:00
pycook
3ac50e7cd8 lint 2020-02-21 22:46:12 +08:00
pycook
21b2cc1d5d The resource view is made into a two-level menu 2020-02-21 22:44:10 +08:00
penzai
cd5448cc7d test: add ci and ci relation crud test cases 2020-02-18 22:05:13 +08:00
pycook
10610bdb4b logo left justify 2020-02-16 19:18:51 +08:00
pycook
b5c2156387 Merge pull request #35 from shaohaojiecoder/i18n
I18n
2020-02-16 19:06:58 +08:00
pycook
b05ae0d1a7 Merge pull request #34 from OhBonsai/develop
add test cases
2020-02-16 19:06:24 +08:00
shaohaojiecoder
bbf6138d43 Merge branch 'develop' into i18n 2020-02-16 18:19:09 +08:00
shaohaojiecoder
1ba3e6a680 add a log pic 2020-02-16 18:18:15 +08:00
penzai
64045c1f93 test: add some test cases 2020-02-16 18:03:33 +08:00
penzai
5a3e55813c model: allow origin and ticket_id nullable in OperationRecord 2020-02-16 18:03:08 +08:00
penzai
bc72e58886 auth: add user in flask.g when auth by jwt 2020-02-16 18:02:24 +08:00
pycook
9e78955ba1 ACL i18n 2020-02-16 17:36:03 +08:00
pycook
136853d9a4 Merge pull request #33 from shaohaojiecoder/i18n
fix local storage for defalut lang
2020-02-16 14:51:59 +08:00
pycook
036e3ad00d modeling i18n 2020-02-16 14:50:17 +08:00
shaohaojiecoder
5ce6c93237 fix local storage for defalut lang 2020-02-16 13:47:30 +08:00
pycook
43dba7f7ed Merge pull request #32 from OhBonsai/develop
fix: recycle import by celery task
2020-02-16 10:38:27 +08:00
penzai
f4879d20d6 fix: recycle import by celery task 2020-02-16 09:39:33 +08:00
pycook
740e4c6034 i18n 2020-02-15 20:57:47 +08:00
pycook
0f2baa1d94 Merge pull request #31 from shaohaojiecoder/i18n
I18n
2020-02-11 09:50:50 +08:00
pycook
405b0af72c Merge branch 'develop' into i18n 2020-02-11 09:50:11 +08:00
shaohaojiecoder
a4e5178979 fix meta title 2020-02-09 19:50:23 +08:00
shaohaojiecoder
c14fe23283 add i18n basic structure 2020-02-09 17:54:57 +08:00
shaohaojiecoder
b3a058f908 add something 2020-02-09 17:22:17 +08:00
shaohaojiecoder
bd82a0e27c add some 2020-02-08 22:27:56 +08:00
pycook
f22a5c3543 Define display fields 2020-02-08 17:39:42 +08:00
pycook
ed81c3f091 Define display fields 2020-02-08 17:36:54 +08:00
shaohaojiecoder
07814b85f9 add basic 2020-02-07 22:05:52 +08:00
pycook
db52b28d6b fix jwt decode 2020-02-06 09:59:24 +08:00
pycook
fc85ba21c8 Merge pull request #29 from OhBonsai/master
fix ci_type_attr_group update bug and add ci_type test case
2020-02-04 12:47:15 +08:00
Bonsai
6c5ee3fcd9 Merge pull request #2 from pycook/master
merge from main repo
2020-02-04 10:56:03 +08:00
penzai
40f1ef88a9 test: add ci_type test cases 2020-02-04 10:54:16 +08:00
penzai
bce422ffc8 fix: update attribute group without name params will fail. #tests/test_cmdb_ci_type.py::test_update_attribute_group_ci_type 2020-02-04 10:53:07 +08:00
pycook
7c79066532 Merge branch 'master' of https://github.com/pycook/cmdb 2020-01-19 18:18:43 +08:00
pycook
1129ac93fb Merge pull request #28 from shaohaojiecoder/master
fix drag group and attrs
2020-01-19 18:18:23 +08:00
haojie.shao
5ab0e7e737 fix drag group and attrs 2020-01-19 18:14:53 +08:00
pycook
23319c7417 /ci_types/<int:type_id>/attributes/transfer and /ci_types/<int:type_id>/attribute_groups/transfer 2020-01-19 17:59:32 +08:00
pycook
c74f85cabb Merge pull request #26 from OhBonsai/master
test: add basic test code and attribute create api test case
2020-01-17 15:34:02 +08:00
penzai
fce2b689fb Merge remote-tracking branch 'origin/master' 2020-01-17 15:10:16 +08:00
penzai
105327bb0c test: add basic test code and attribute create api test case 2020-01-17 15:08:46 +08:00
Bonsai
745c43d0a4 Merge pull request #1 from pycook/master
merge master
2020-01-17 10:05:05 +08:00
pycook
3130d94568 [fix] cycle import 2020-01-15 11:52:33 +08:00
pycook
04a66eb239 flush cache when delete attribute 2020-01-15 09:06:31 +08:00
pycook
68390ec6f1 [fix] delete CIType's attribute 2020-01-14 20:52:36 +08:00
pycook
17392be138 api docs update 2020-01-06 21:58:06 +08:00
pycook
f2fdb29221 api docs update 2020-01-06 21:54:33 +08:00
pycook
4a18698423 update README 2019-12-31 22:53:21 +08:00
pycook
95ccee04f9 update README 2019-12-31 22:50:01 +08:00
pycook
b60628247b Update README.md
update
2019-12-31 22:37:36 +08:00
pycook
a6d7699ab4 Merge pull request #24 from fxiang21/master
add Readme of English
2019-12-31 22:32:48 +08:00
fxiang21
4b21bcc438 add Readme of English 2019-12-31 22:25:42 +08:00
pycook
33dce2f0f3 Update README.md
[fix] flask db-setup
2019-12-31 11:06:59 +08:00
pycook
d43b827fe5 [fix] fuzzy search 2019-12-25 13:36:43 +08:00
pycook
aec8bade41 [fix] security alerts 2019-12-25 10:19:03 +08:00
pycook
89ae89a449 [fix] validate attribute is required 2019-12-24 20:37:32 +08:00
pycook
945f90e386 disable eslint warning 2019-12-24 15:15:03 +08:00
pycook
2ba6a16613 support JSON type 2019-12-23 18:51:33 +08:00
pycook
6089039366 fix sidebar menu in mobile 2019-12-23 11:58:41 +08:00
pycook
e1e5307084 add yarn.lock 2019-12-23 11:27:47 +08:00
pycook
2ff7fce9dd flask init-acl 2019-12-20 12:57:39 +09:00
pycook
fc4d3e0c1a update makefile 2019-12-18 23:36:58 +09:00
pycook
f66a94712e Modify code organization 2019-12-18 23:33:22 +09:00
pycook
24664c7686 catch abort exception when getting relation views 2019-12-13 09:59:38 +08:00
pycook
1d668bab6e update 2019-12-12 21:45:19 +08:00
pycook
3d4b84909e fix delete relation view 2019-12-12 21:36:33 +08:00
pycook
8341e742eb [fix] update attribute which is list 2019-12-11 18:12:10 +08:00
pycook
a71ba83de0 release 2.0 2019-12-11 12:43:55 +08:00
pycook
9668131c18 V2.0 2019-12-11 12:14:23 +08:00
pycook
4a744dcad9 fix relation tree 2019-12-10 15:35:59 +08:00
pycook
2a420225e2 Merge pull request #22 from lovvvve/FixDelCi_type
fix(ci_type api): fix the judgment condition of deleting ci_type
2019-12-10 14:41:27 +08:00
Lovvvve
ff67785618 fix(ci_type api): fix the judgment condition of deleting ci_type 2019-12-10 14:31:27 +08:00
pycook
dfe1ba55d5 sidebar scroll 2019-12-09 17:16:38 +08:00
pycook
90b1b6b7af fix relation view 2019-12-09 12:03:58 +08:00
pycook
d5fbe42ed7 relation view bugfix 2019-12-08 00:20:55 +08:00
pycook
f424ad6864 acl done and bugfix 2019-12-06 22:33:31 +08:00
pycook
16b724bd40 ACL: permission management [doing] 2019-12-04 18:14:09 +08:00
pycook
f70ed54cad update readme 2019-12-04 09:26:01 +08:00
pycook
dd64564160 remove print 2019-12-03 22:13:14 +08:00
pycook
cc2cdbcc9f fix delete ci relation 2019-12-03 21:57:44 +08:00
pycook
81fe850627 fix get second cis api 2019-12-03 20:10:27 +08:00
pycook
487d9f76f6 关系视图定义支持两只方式 2019-12-03 19:54:01 +08:00
pycook
92dd4c5dfe relation view has been optimised 2019-12-03 19:10:54 +08:00
pycook
8ee7c6daf8 version 1.5: update docker file 2019-11-30 23:07:12 +08:00
pycook
882b158d18 cmdb.sql update 2019-11-29 22:21:41 +08:00
pycook
85222443c0 relation view [done] 2019-11-29 18:11:18 +08:00
pycook
1696ecf49d relation view [doing] 2019-11-28 21:17:06 +08:00
pycook
73b92ff533 relation view define [done] 2019-11-27 18:25:53 +08:00
pycook
e977bb15a5 GPLv2 2019-11-25 20:35:05 +08:00
pycook
7c46d6cdbf change to GPLv2 2019-11-25 20:33:56 +08:00
pycook
4d11c1f7db License change to GPLv3 2019-11-25 19:42:37 +08:00
pycook
0a563deb11 UI: relation type define [done] 2019-11-25 19:23:51 +08:00
pycook
ba80ec4403 /acl/resources add param resource_type_id 2019-11-24 22:33:57 +08:00
pycook
3b7cc4595b fix grant 2019-11-24 22:29:51 +08:00
pycook
9fe47657a6 Merge pull request #20 from kdyq007/master
[更新] 新增角色、资源、权限页面
2019-11-24 17:29:58 +08:00
kdyq007
5a4a6caa07 Merge branch 'master' of https://github.com/kdyq007/cmdb 2019-11-24 17:21:27 +08:00
kdyq007
9dadbe1599 Merge pull request #6 from pycook/master
fix acl api
2019-11-24 16:43:53 +08:00
pycook
40d016f513 fix acl api 2019-11-24 16:35:28 +08:00
kdyq007
655edaa7c8 [更新] 完成权限管理 2019-11-24 15:40:38 +08:00
kdyq007
7fa5cff919 [更新] 完成权限管理页面 2019-11-24 15:22:18 +08:00
kdyq007
d19834ed5d Merge pull request #5 from pycook/master
同步
2019-11-23 21:53:46 +08:00
pycook
b6be430aa3 fix 2019-11-23 21:50:45 +08:00
kdyq007
63792c242f [更新] 完成资源类型页面 2019-11-23 20:16:31 +08:00
kdyq007
10f7029722 [保存] 完成资源类型权限显示 2019-11-23 18:08:52 +08:00
pycook
ba176542dc fix acl resource 2019-11-23 17:42:33 +08:00
kdyq007
aae3b6e2ff Merge pull request #4 from pycook/master
fix acl resource_type
2019-11-23 17:36:42 +08:00
pycook
b370c7d46e fix acl resource_type 2019-11-23 17:24:43 +08:00
kdyq007
efa5a8ea5d Merge pull request #3 from pycook/master
同步
2019-11-23 14:52:41 +08:00
pycook
fd532626ac relative view api [done] 2019-11-22 18:18:22 +08:00
pycook
617337c614 Realize /api/v0.1/ci_relations/s [done] 2019-11-21 18:21:03 +08:00
kdyq007
9a3d24ac81 [更新] 保存一下 2019-11-20 19:02:36 +08:00
kdyq007
454dd4c56b Merge branch 'master' of https://github.com/kdyq007/cmdb 2019-11-19 21:52:33 +08:00
kdyq007
88ad72d4dc Merge pull request #2 from pycook/master
update
2019-11-19 21:52:02 +08:00
kdyq007
8d1517d550 [更新] 完成基础role和user管理 2019-11-19 21:49:51 +08:00
pycook
d3a8ef5966 fix get user by uid 2019-11-19 21:46:53 +08:00
pycook
e5baa5012d acl: resource type api 2019-11-19 21:41:46 +08:00
pycook
a1f63b00dd fix search 2019-11-19 18:32:35 +08:00
pycook
47ded84231 elastic search [done] 2019-11-19 18:16:31 +08:00
kdyq007
224a48a5f3 [更新] 去除app_id 2019-11-18 22:22:38 +08:00
pycook
0e7c52df71 es search update 2019-11-18 22:05:59 +08:00
pycook
ff701cc770 search by elasticsearch [doing] 2019-11-18 20:02:25 +08:00
kdyq007
6a7bb725cc Merge pull request #1 from pycook/master
怎么玩的?反向pull request
2019-11-18 18:31:14 +08:00
pycook
0a13186c13 fix acl api 2019-11-18 12:02:02 +08:00
kdyq007
a0ffeb9950 [更新] 完成角色管理页面 2019-11-17 21:08:04 +08:00
kdyq007
6c70ec6d53 [更新] 完成roles基本接口 2019-11-17 17:09:24 +08:00
qiqi
4b5f82699a [更新] 完成用户管理页面 2019-11-17 09:32:39 +08:00
pycook
f78c3b928b pep8 2019-11-15 18:03:06 +08:00
pycook
332659c1d5 update acl 2019-11-15 16:54:56 +08:00
pycook
3beb2706dc Merge pull request #18 from kdyq007/master
[更新] 修改图片路径、压缩图片
2019-11-14 21:59:38 +08:00
qiqi
a14111e1ce [更新] 优化格式 2019-11-14 21:51:58 +08:00
qiqi
c4320c14f9 [更新] 更换图片位置、压缩图片 2019-11-14 21:48:36 +08:00
qiqi
4c5442748f [更新] 优化说明文件格式 2019-11-14 21:00:24 +08:00
qiqi
a81750acba [更新] 新增Q群 README.md 2019-11-14 20:55:48 +08:00
pycook
0439e2462b update acl 2019-11-14 18:35:31 +08:00
pycook
3b62bd7ac9 update readme 2019-11-13 14:02:02 +08:00
pycook
f6add52721 python3.7 timezone fix 2019-11-13 13:56:44 +08:00
pycook
c85e535288 update acl 2019-11-13 13:25:42 +08:00
pycook
c0c6d116b5 docker images use aliyun 2019-11-13 11:56:17 +08:00
pycook
39153e92d1 update Makefile and support for install by make 2019-11-12 11:55:04 +08:00
pycook
42bcc2e510 fix py3 2019-11-12 11:15:25 +08:00
pycook
398fbb25dc merge Dockerfile 2019-11-12 10:40:37 +08:00
pycook
4b312d4f99 delete docs/Dockerfile 2019-11-11 23:12:50 +08:00
pycook
10414155a5 fix timezone 2019-11-11 23:11:12 +08:00
pycook
feda0c37e7 update README 2019-11-11 16:10:02 +08:00
pycook
173c120b64 flask init-cache 2019-11-11 15:46:57 +08:00
pycook
5f2a0d1a7b Remove package-lock.json and remove some compile warnings 2019-11-11 13:16:07 +08:00
pycook
50f894a01d add command init-cache 2019-11-11 11:27:43 +08:00
pycook
66e93e73af Merge branch 'master' of https://github.com/pycook/cmdb 2019-11-11 09:20:07 +08:00
pycook
58ad9d3f05 vue lint 2019-11-11 00:25:22 +08:00
pycook
08c96039e9 gunicorn==19.5.0 2019-11-10 19:10:23 +08:00
pycook
ca0dd97626 Docker to production 2019-11-10 19:06:38 +08:00
pycook
7810ee3974 Partially completed backend development of permissions management 2019-11-08 17:42:13 +08:00
pycook
2cfea7ef08 Update README.md
docker 一键安装说明补充
2019-11-08 15:26:22 +08:00
pycook
0cee6cea25 fix py2.7 unicode encoding error 2019-11-08 15:15:31 +08:00
pycook
5d13ba2f26 users drop is_admin 2019-11-08 14:58:21 +08:00
pycook
a583433530 fix unicode encode error 2019-11-08 14:37:53 +08:00
fxiang21
733ac3b2b4 移除多余的docker-start目录 2019-11-08 09:20:34 +08:00
fxiang21
ef6300255a 修复nginx转发问题 2019-11-08 09:20:27 +08:00
fxiang21
aad37dcf0b 添加容器化部署方式 2019-11-08 09:20:09 +08:00
pycook
cce10d39ea code format 2019-11-07 19:18:31 +08:00
pycook
c521dd447e Update README.md
pipenv run flask run -h 0.0.0.0
2019-11-05 17:52:45 +08:00
pycook
4d0cd4ba56 Update README.md
如果是非本机访问, 要修改ui/.env里VUE_APP_API_BASE_URL里的IP地址
2019-11-05 17:44:48 +08:00
pycook
7291274cb1 Update README.md
如果是非本机访问, 要修改ui/.env里VUE_APP_API_BASE_URL里的IP地址
2019-11-05 17:43:40 +08:00
pycook
44f2e383c3 update overview jpeg url 2019-11-01 11:37:16 +08:00
pycook
1f8219b418 fix add integer list 2019-11-01 11:27:24 +08:00
pycook
cb2f170ded mkdir logs, ignore *.log 2019-11-01 10:45:35 +08:00
pycook
1241a23ba8 Update README.md
add cmdb.sql
2019-10-28 21:48:46 +08:00
pycook
7d7744b7dc add docs/cmdb.sql 2019-10-28 21:46:41 +08:00
pycook
9c7d51127a fix date picker 2019-10-24 20:43:59 +08:00
pycook
b5a987f6b4 choice value tip fix 2019-10-24 20:43:58 +08:00
pycook
7bbc68bfd5 fix delete ci type 2019-10-24 20:43:58 +08:00
pycook
99d11e11ce fix ci types show 2019-10-24 20:43:58 +08:00
pycook
7b96ac4638 attribute alias must be unique 2019-10-24 20:43:58 +08:00
pycook
0a36330852 fix attributes paginate 2019-10-24 20:43:54 +08:00
pycook
9105f92c82 update README 2019-10-24 20:43:51 +08:00
pycook
57541ab486 Update README.md
create tables fix
2019-10-24 20:43:51 +08:00
pycook
a0fcbd220e attributes paginate and fix update value 2019-10-24 20:43:51 +08:00
pycook
d54b404eb6 add docs 2019-10-24 20:43:51 +08:00
pycook
620c5bb5eb ci search return unique key 2019-10-24 20:43:51 +08:00
pycook
0fde1d699d invalid username or password -> 403 2019-10-24 20:43:51 +08:00
shaohaojiecoder
61f77cf311 add batch module 2019-10-24 20:43:51 +08:00
lilixiang
13476128d5 add 添加属性库和模型模块 2019-10-24 20:43:51 +08:00
pycook
5cdb4ecd2a Revert "add 添加属性库和模型模块" 2019-10-24 20:43:51 +08:00
lilixiang
64c3b9da3b add 添加属性库和模型模块 2019-10-24 20:43:51 +08:00
pycook
55dad7a58c cache强制unicode编码 2019-08-30 09:46:24 +08:00
pycook
38dabc35e5 add .gitattributes 2019-08-28 21:08:28 +08:00
pycook
5b4f95a50e add ui 2019-08-28 20:51:51 +08:00
pycook
f3046d3c91 remove ui 2019-08-28 20:48:23 +08:00
pycook
5faae9af67 remove ui 2019-08-28 20:48:04 +08:00
pycook
c0b50642e0 update README 2019-08-28 20:45:59 +08:00
pycook
12ca296879 升级后端并开源UI 2019-08-28 20:34:10 +08:00
pycook
420c6cea2b delete。。。 2016-08-26 13:46:03 +08:00
pycook
ccc4bb48fa pep8 2016-06-27 10:50:32 +08:00
168 changed files with 28828 additions and 22264 deletions

View File

@@ -1,60 +0,0 @@
name: Bug Report
description: File a bug report
title: "[Bug]: "
labels: ["☢️ bug"]
assignees:
- Selina316
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report!
- type: input
id: contact
attributes:
label: Contact Details
description: How can we get in touch with you if we need more info?
placeholder: ex. email@example.com
validations:
required: false
- type: dropdown
id: aspects
attributes:
label: This bug is related to UI or API?
multiple: true
options:
- UI
- API
- type: textarea
id: happened
attributes:
label: What happened?
description: Also tell us, what did you expect to happen?
placeholder: Tell us what you see!
value: "A bug happened!"
validations:
required: true
- type: input
id: version
attributes:
label: Version
description: What version of our software are you running?
value: "newest"
validations:
required: true
- type: dropdown
id: browsers
attributes:
label: What browsers are you seeing the problem on?
multiple: true
options:
- Firefox
- Chrome
- Safari
- Microsoft Edge
- type: textarea
id: logs
attributes:
label: Relevant log output
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
render: shell

View File

@@ -1,44 +0,0 @@
name: Feature wanted
description: A new feature would be good
title: "[Feature]: "
labels: ["✏️ feature"]
assignees:
- pycook
body:
- type: markdown
attributes:
value: |
Thank you for your feature suggestion; we will evaluate it carefully!
- type: input
id: contact
attributes:
label: Contact Details
description: How can we get in touch with you if we need more info?
placeholder: ex. email@example.com
validations:
required: false
- type: dropdown
id: aspects
attributes:
label: feature is related to UI or API aspects?
multiple: true
options:
- UI
- API
- type: textarea
id: feature
attributes:
label: What is your advice?
description: Also tell us, what did you expect to happen?
placeholder: Tell us what you want!
value: "everyone wants this feature!"
validations:
required: true
- type: input
id: version
attributes:
label: Version
description: What version of our software are you running?
value: "newest"
validations:
required: true

View File

@@ -1,36 +0,0 @@
name: Help wanted
description: I have a question
title: "[help wanted]: "
labels: ["help wanted"]
assignees:
- ivonGwy
body:
- type: markdown
attributes:
value: |
Please tell us what's you need!
- type: input
id: contact
attributes:
label: Contact Details
description: How can we get in touch with you if we need more info?
placeholder: ex. email@example.com
validations:
required: false
- type: textarea
id: question
attributes:
label: What is your question?
description: Also tell us, how can we help?
placeholder: Tell us what you need!
value: "i have a question!"
validations:
required: true
- type: input
id: version
attributes:
label: Version
description: What version of our software are you running?
value: "newest"
validations:
required: true

View File

@@ -1,60 +0,0 @@
name: Bug Report
description: File a bug report
title: "[Bug]: "
labels: ["bug"]
assignees:
- pycook
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report!
- type: input
id: contact
attributes:
label: Contact Details
description: How can we get in touch with you if we need more info?
placeholder: ex. email@example.com
validations:
required: false
- type: dropdown
id: type
attributes:
label: bug is related to UI or API aspects?
multiple: true
options:
- UI
- API
- type: textarea
id: what-happened
attributes:
label: What happened?
description: Also tell us, what did you expect to happen?
placeholder: Tell us what you see!
value: "A bug happened!"
validations:
required: true
- type: textarea
id: version
attributes:
label: Version
description: What version of our software are you running?
default: 2.3.5
validations:
required: true
- type: dropdown
id: browsers
attributes:
label: What browsers are you seeing the problem on?
multiple: true
options:
- Firefox
- Chrome
- Safari
- Microsoft Edge
- type: textarea
id: logs
attributes:
label: Relevant log output
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
render: shell

View File

@@ -1,6 +0,0 @@
blank_issues_enabled: false
contact_links:
- name: veops official website
url: https://veops.cn/#hero
about: you can contact us here.

View File

@@ -1,44 +0,0 @@
name: Feature wanted
description: A new feature would be good
title: "[Feature]: "
labels: ["feature"]
assignees:
- pycook
body:
- type: markdown
attributes:
value: |
Thank you for your feature suggestion; we will evaluate it carefully!
- type: input
id: contact
attributes:
label: Contact Details
description: How can we get in touch with you if we need more info?
placeholder: ex. email@example.com
validations:
required: false
- type: dropdown
id: type
attributes:
label: feature is related to UI or API aspects?
multiple: true
options:
- UI
- API
- type: textarea
id: describe the feature
attributes:
label: What is your advice?
description: Also tell us, what did you expect to happen?
placeholder: Tell us what you want!
value: "everyone wants this feature!"
validations:
required: true
- type: textarea
id: version
attributes:
label: Version
description: What version of our software are you running?
default: 2.3.5
validations:
required: true

0
.github/config.yml vendored
View File

2
.gitignore vendored
View File

@@ -40,7 +40,6 @@ nosetests.xml
.pytest_cache
cmdb-api/test-output
cmdb-api/api/uploaded_files
cmdb-api/migrations/versions
# Translations
*.mo
@@ -70,7 +69,6 @@ settings.py
# UI
cmdb-ui/node_modules
cmdb-ui/dist
cmdb-ui/yarn.lock
# Log files
cmdb-ui/npm-debug.log*

View File

@@ -9,7 +9,7 @@ help: ## display this help
env: ## create a development environment using pipenv
sudo easy_install pip && \
pip install pipenv -i https://repo.huaweicloud.com/repository/pypi/simple && \
pip install pipenv -i https://pypi.douban.com/simple && \
npm install yarn && \
make deps
.PHONY: env
@@ -36,7 +36,7 @@ api: ## start api server
.PHONY: api
worker: ## start async tasks worker
cd cmdb-api && pipenv run celery -A celery_worker.celery worker -E -Q one_cmdb_async --autoscale=5,2 --logfile=one_cmdb_async.log -D && pipenv run celery -A celery_worker.celery worker -E -Q acl_async --autoscale=2,1 --logfile=one_acl_async.log -D
cd cmdb-api && pipenv run celery -A celery_worker.celery worker -E -Q one_cmdb_async --concurrency=1 -D && pipenv run celery -A celery_worker.celery worker -E -Q acl_async --concurrency=1 -D
.PHONY: worker
ui: ## start ui server

View File

@@ -1,20 +1,12 @@
![维易开源CMDB](docs/images/logo.png)
<p align="center">
<a href="https://veops.cn"><img src="docs/images/logo.png" alt="维易CMDB" width="300"/></a>
</p>
<h3 align="center">简单、轻量、通用的运维配置管理数据库</h3>
<p align="center">
<a href="https://github.com/veops/cmdb/blob/master/LICENSE"><img src="https://img.shields.io/badge/License-AGPLv3-brightgreen" alt="License: GPLv3"></a>
<a href="https:https://github.com/sendya/ant-design-pro-vue"><img src="https://img.shields.io/badge/UI-Ant%20Design%20Pro%20Vue-brightgreen" alt="UI"></a>
<a href="https://github.com/pallets/flask"><img src="https://img.shields.io/badge/API-Flask-brightgreen" alt="API"></a>
</p>
------------------------------
[![License](https://img.shields.io/badge/License-AGPLv3-brightgreen)](https://github.com/veops/cmdb/blob/master/LICENSE)
[![UI](https://img.shields.io/badge/UI-Ant%20Design%20Pro%20Vue-brightgreen)](https://github.com/sendya/ant-design-pro-vue)
[![API](https://img.shields.io/badge/API-Flask-brightgreen)](https://github.com/pallets/flask)
[English](docs/README_en.md) / [中文](README.md)
- 产品文档https://veops.cn/docs/
- 在线体验<a href="https://cmdb.veops.cn" target="_blank">CMDB</a>
- 在线体验: <a href="https://cmdb.veops.cn" target="_blank">CMDB</a>
- username: demo 或者 admin
- password: 123456
@@ -23,43 +15,45 @@
## 系统介绍
### 系统概览
### 整体架构
<img src=docs/images/dashboard.png />
<img src=docs/images/view.jpg />
[查看更多展示](docs/screenshot.md)
### 相关文档
### 相关文章
- <a href="https://mp.weixin.qq.com/s/v3eANth64UBW5xdyOkK3tg" target="_blank">概要设计</a>
- <a href="https://zhuanlan.zhihu.com/p/98453732" target="_blank">设计文档</a>
- <a href="https://github.com/veops/cmdb/tree/master/docs/cmdb_api.md" target="_blank">API 文档</a>
- <a href="https://mp.weixin.qq.com/s/rQaf4AES7YJsyNQG_MKOLg" target="_blank">自动发现</a>
- 更多文章可以在公众号 **维易科技OneOps** 里查看
- <a href="https://mp.weixin.qq.com/s/EflmmJ-qdUkddTx2hRt3pA" target="_blank">树形视图实践</a>
### 特点
- 灵活性
1. 配置灵活,不设定任何运维场景,有内置模板
2. 自动发现、入库 IT 资产
1. 规范并统一纳管复杂数据资产
2. 自动发现、入库 IT 资产
- 安全性
1. 细粒度权限控制
1. 细粒度访问控制
2. 完备操作日志
- 多应用
1. 丰富视图展示维度
2. API简单强大
3. 支持定义属性触发器、计算属性
2. 提供 Restful API
3. 自定义字段触发器
### 主要功能
- 模型属性支持索引、多值、默认排序、字体颜色,支持计算属性
- 支持自动发现、定时巡检、文件导入
- 支持资源、层级、关系视图展示
- 支持资源、树形、关系视图展示
- 支持模型间关系配置和展示
- 细粒度访问控制,完备的操作日志
- 支持跨模型搜索
### 系统概览
- 服务树
![服务树](docs/images/0.png "首页展示")
[查看更多展示](docs/screenshot.md)
### 更多功能
@@ -73,36 +67,22 @@
## 安装
### Docker 一键快速构建
> 方法一
- 第一步: 先安装 docker 环境, 以及docker-compose
- 第二步: 拷贝项目
```shell
git clone https://github.com/veops/cmdb.git
```
- 第三步:进入主目录,执行:
- 进入主目录(先安装 docker 环境)
```
docker-compose up -d
```
> 方法二, 该方法适用于linux系统
- 第一步: 先安装 docker 环境, 以及docker-compose
- 第二步: 直接使用项目根目录下的install.sh 文件进行 `安装`、`启动`、`暂停`、`查状态`、`删除`、`卸载`
```shell
curl -so install.sh https://raw.githubusercontent.com/veops/cmdb/master/install.sh
sh install.sh install
```
- 浏览器打开: [http://127.0.0.1:8000](http://127.0.0.1:8000)
- username: demo 或者 admin
- password: 123456
### [本地开发环境搭建](docs/local.md)
### [Makefile 安装](docs/makefile.md)
## 验证
- 浏览器打开: [http://127.0.0.1:8000](http://127.0.0.1:8000)
- username: demo 或者 admin
- password: 123456
---
_**欢迎关注公众号(维易科技OneOps),关注后可加入微信群,进行产品和技术交流。**_
_**欢迎关注我们的公众号点击联系我们加入微信、QQ群(336164978),获得更多产品、行业相关资讯**_
![公众号: 维易科技OneOps](docs/images/wechat.png)
![公众号: 维易科技OneOps](docs/images/qrcode_for_gzh.jpg)

View File

@@ -6,7 +6,7 @@ name = "pypi"
[packages]
# Flask
Flask = "==2.3.2"
Werkzeug = ">=2.3.6"
Werkzeug = "==2.3.6"
click = ">=5.0"
# Api
Flask-RESTful = "==0.3.10"
@@ -21,36 +21,33 @@ Flask-Migrate = "==2.5.2"
gunicorn = "==21.0.1"
supervisor = "==4.0.3"
# Auth
Flask-Login = ">=0.6.2"
Flask-Login = "==0.6.2"
Flask-Bcrypt = "==1.0.1"
Flask-Cors = ">=3.0.8"
ldap3 = "==2.9.1"
python-ldap = "==3.4.0"
pycryptodome = "==3.12.0"
cryptography = ">=41.0.2"
# Caching
Flask-Caching = ">=1.0.0"
# Environment variable parsing
environs = "==4.2.0"
marshmallow = "==2.20.2"
# async tasks
celery = ">=5.3.1"
celery = "==5.3.1"
celery_once = "==3.0.1"
more-itertools = "==5.0.0"
kombu = ">=5.3.1"
kombu = "==5.3.1"
# common setting
timeout-decorator = "==0.5.0"
WTForms = "==3.0.0"
email-validator = "==1.3.1"
treelib = "==1.6.1"
flasgger = "==0.9.5"
Pillow = ">=10.0.1"
Pillow = "==9.3.0"
# other
six = "==1.16.0"
six = "==1.12.0"
bs4 = ">=0.0.1"
toposort = ">=1.5"
requests = ">=2.22.0"
requests_oauthlib = "==1.3.1"
markdownify = "==0.11.6"
PyJWT = "==2.4.0"
elasticsearch = "==7.17.9"
future = "==0.18.3"
@@ -59,9 +56,6 @@ Jinja2 = "==3.1.2"
jinja2schema = "==0.1.4"
msgpack-python = "==0.5.6"
alembic = "==1.7.7"
hvac = "==2.0.0"
colorama = ">=0.4.6"
pycryptodomex = ">=3.19.0"
[dev-packages]
# Testing
@@ -78,3 +72,4 @@ flake8-isort = "==2.7.0"
isort = "==4.3.21"
pep8-naming = "==0.8.2"
pydocstyle = "==3.0.0"

View File

@@ -7,7 +7,6 @@ import os
import sys
from inspect import getmembers
from logging.handlers import RotatingFileHandler
from pathlib import Path
from flask import Flask
from flask import jsonify
@@ -18,14 +17,11 @@ from flask.json.provider import DefaultJSONProvider
import api.views.entry
from api.extensions import (bcrypt, cache, celery, cors, db, es, login_manager, migrate, rd)
from api.extensions import inner_secrets
from api.flask_cas import CAS
from api.lib.secrets.secrets import InnerKVManger
from api.models.acl import User
HERE = os.path.abspath(os.path.dirname(__file__))
PROJECT_ROOT = os.path.join(HERE, os.pardir)
BASE_DIR = Path(__file__).resolve().parent.parent
@login_manager.user_loader
@@ -80,6 +76,15 @@ class MyJSONEncoder(DefaultJSONProvider):
return o
def create_acl_app(config_object="settings"):
app = Flask(__name__.split(".")[0])
app.config.from_object(config_object)
register_extensions(app)
return app
def create_app(config_object="settings"):
"""Create application factory, as explained here: http://flask.pocoo.org/docs/patterns/appfactories/.
@@ -120,7 +125,7 @@ def register_extensions(app):
db.init_app(app)
cors.init_app(app)
login_manager.init_app(app)
migrate.init_app(app, db, directory=f"{BASE_DIR}/migrations")
migrate.init_app(app, db)
rd.init_app(app)
if app.config.get('USE_ES'):
es.init_app(app)
@@ -128,10 +133,6 @@ def register_extensions(app):
app.config.update(app.config.get("CELERY"))
celery.conf.update(app.config)
if app.config.get('SECRETS_ENGINE') == 'inner':
with app.app_context():
inner_secrets.init_app(app, InnerKVManger())
def register_blueprints(app):
for item in getmembers(api.views.entry):

View File

@@ -1,8 +1,6 @@
import click
from flask.cli import with_appcontext
from api.lib.perm.acl.user import UserCRUD
@click.command()
@with_appcontext
@@ -25,18 +23,50 @@ def init_acl():
role_rebuild.apply_async(args=(role.id, app.id), queue=ACL_QUEUE)
@click.command()
@with_appcontext
def add_user():
"""
create a user
is_admin: default is False
"""
username = click.prompt('Enter username', confirmation_prompt=False)
password = click.prompt('Enter password', hide_input=True, confirmation_prompt=True)
email = click.prompt('Enter email ', confirmation_prompt=False)
UserCRUD.add(username=username, password=password, email=email)
# @click.command()
# @with_appcontext
# def acl_clean():
# from api.models.acl import Resource
# from api.models.acl import Permission
# from api.models.acl import RolePermission
#
# perms = RolePermission.get_by(to_dict=False)
#
# for r in perms:
# perm = Permission.get_by_id(r.perm_id)
# if perm and perm.app_id != r.app_id:
# resource_id = r.resource_id
# resource = Resource.get_by_id(resource_id)
# perm_name = perm.name
# existed = Permission.get_by(resource_type_id=resource.resource_type_id, name=perm_name, first=True,
# to_dict=False)
# if existed is not None:
# other = RolePermission.get_by(rid=r.rid, perm_id=existed.id, resource_id=resource_id)
# if not other:
# r.update(perm_id=existed.id)
# else:
# r.soft_delete()
# else:
# r.soft_delete()
#
#
# @click.command()
# @with_appcontext
# def acl_has_resource_role():
# from api.models.acl import Role
# from api.models.acl import App
# from api.lib.perm.acl.cache import HasResourceRoleCache
# from api.lib.perm.acl.role import RoleCRUD
#
# roles = Role.get_by(to_dict=False)
# apps = App.get_by(to_dict=False)
# for role in roles:
# if role.app_id:
# res = RoleCRUD.recursive_resources(role.id, role.app_id)
# if res.get('resources') or res.get('groups'):
# HasResourceRoleCache.add(role.id, role.app_id)
# else:
# for app in apps:
# res = RoleCRUD.recursive_resources(role.id, app.id)
# if res.get('resources') or res.get('groups'):
# HasResourceRoleCache.add(role.id, app.id)

View File

@@ -7,7 +7,6 @@ import json
import time
import click
import requests
from flask import current_app
from flask.cli import with_appcontext
from flask_login import login_user
@@ -16,6 +15,7 @@ import api.lib.cmdb.ci
from api.extensions import db
from api.extensions import rd
from api.lib.cmdb.cache import AttributeCache
from api.lib.cmdb.ci_type import CITypeTriggerManager
from api.lib.cmdb.const import PermEnum
from api.lib.cmdb.const import REDIS_PREFIX_CI
from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION
@@ -24,14 +24,12 @@ from api.lib.cmdb.const import RoleEnum
from api.lib.cmdb.const import ValueTypeEnum
from api.lib.exception import AbortException
from api.lib.perm.acl.acl import ACLManager
from api.lib.perm.acl.acl import UserCache
from api.lib.perm.acl.cache import AppCache
from api.lib.perm.acl.cache import UserCache
from api.lib.perm.acl.resource import ResourceCRUD
from api.lib.perm.acl.resource import ResourceTypeCRUD
from api.lib.perm.acl.role import RoleCRUD
from api.lib.secrets.inner import KeyManage
from api.lib.secrets.inner import global_key_threshold
from api.lib.secrets.secrets import InnerKVManger
from api.lib.perm.acl.user import UserCRUD
from api.models.acl import App
from api.models.acl import ResourceType
from api.models.cmdb import Attribute
@@ -56,7 +54,6 @@ def cmdb_init_cache():
if relations:
rd.create_or_update(relations, REDIS_PREFIX_CI_RELATION)
es = None
if current_app.config.get("USE_ES"):
from api.extensions import es
from api.models.cmdb import Attribute
@@ -127,10 +124,10 @@ def cmdb_init_acl():
# 3. add resource and grant
ci_types = CIType.get_by(to_dict=False)
resource_type_id = ResourceType.get_by(name=ResourceTypeEnum.CI, first=True, to_dict=False).id
type_id = ResourceType.get_by(name=ResourceTypeEnum.CI, first=True, to_dict=False).id
for ci_type in ci_types:
try:
ResourceCRUD.add(ci_type.name, resource_type_id, app_id)
ResourceCRUD.add(ci_type.name, type_id, app_id)
except AbortException:
pass
@@ -140,10 +137,10 @@ def cmdb_init_acl():
[PermEnum.READ])
relation_views = PreferenceRelationView.get_by(to_dict=False)
resource_type_id = ResourceType.get_by(name=ResourceTypeEnum.RELATION_VIEW, first=True, to_dict=False).id
type_id = ResourceType.get_by(name=ResourceTypeEnum.RELATION_VIEW, first=True, to_dict=False).id
for view in relation_views:
try:
ResourceCRUD.add(view.name, resource_type_id, app_id)
ResourceCRUD.add(view.name, type_id, app_id)
except AbortException:
pass
@@ -153,6 +150,57 @@ def cmdb_init_acl():
[PermEnum.READ])
@click.command()
@click.option(
'-u',
'--user',
help='username'
)
@click.option(
'-p',
'--password',
help='password'
)
@click.option(
'-m',
'--mail',
help='mail'
)
@with_appcontext
def add_user(user, password, mail):
"""
create a user
is_admin: default is False
Example: flask add-user -u <username> -p <password> -m <mail>
"""
assert user is not None
assert password is not None
assert mail is not None
UserCRUD.add(username=user, password=password, email=mail)
@click.command()
@click.option(
'-u',
'--user',
help='username'
)
@with_appcontext
def del_user(user):
"""
delete a user
Example: flask del-user -u <username>
"""
assert user is not None
from api.models.acl import User
u = User.get_by(username=user, first=True, to_dict=False)
u and UserCRUD.delete(u.uid)
@click.command()
@with_appcontext
def cmdb_counter():
@@ -179,60 +227,50 @@ def cmdb_counter():
@with_appcontext
def cmdb_trigger():
"""
Trigger execution for date attribute
Trigger execution
"""
from api.lib.cmdb.ci import CITriggerManager
current_day = datetime.datetime.today().strftime("%Y-%m-%d")
trigger2cis = dict()
trigger2completed = dict()
i = 0
while True:
try:
db.session.remove()
db.session.remove()
if datetime.datetime.today().strftime("%Y-%m-%d") != current_day:
trigger2cis = dict()
trigger2completed = dict()
current_day = datetime.datetime.today().strftime("%Y-%m-%d")
if datetime.datetime.today().strftime("%Y-%m-%d") != current_day:
trigger2cis = dict()
trigger2completed = dict()
current_day = datetime.datetime.today().strftime("%Y-%m-%d")
if i == 360 or i == 0:
i = 0
try:
triggers = CITypeTrigger.get_by(to_dict=False)
if i == 3 or i == 0:
i = 0
triggers = CITypeTrigger.get_by(to_dict=False, __func_isnot__key_attr_id=None)
for trigger in triggers:
try:
ready_cis = CITriggerManager.waiting_cis(trigger)
except Exception as e:
print(e)
continue
ready_cis = CITypeTriggerManager.waiting_cis(trigger)
if trigger.id not in trigger2cis:
trigger2cis[trigger.id] = (trigger, ready_cis)
else:
cur = trigger2cis[trigger.id]
cur_ci_ids = {i.ci_id for i in cur[1]}
trigger2cis[trigger.id] = (
trigger, cur[1] + [i for i in ready_cis if i.ci_id not in cur_ci_ids
and i.ci_id not in trigger2completed.get(trigger.id, {})])
trigger2cis[trigger.id] = (trigger, cur[1] + [i for i in ready_cis if i.ci_id not in cur_ci_ids
and i.ci_id not in trigger2completed[trigger.id]])
for tid in trigger2cis:
trigger, cis = trigger2cis[tid]
for ci in copy.deepcopy(cis):
if CITriggerManager.trigger_notify(trigger, ci):
trigger2completed.setdefault(trigger.id, set()).add(ci.ci_id)
except Exception as e:
print(e)
for _ci in cis:
if _ci.ci_id == ci.ci_id:
cis.remove(_ci)
for tid in trigger2cis:
trigger, cis = trigger2cis[tid]
for ci in copy.deepcopy(cis):
if CITypeTriggerManager.trigger_notify(trigger, ci):
trigger2completed.setdefault(trigger.id, set()).add(ci.ci_id)
i += 1
time.sleep(10)
except Exception as e:
import traceback
print(traceback.format_exc())
current_app.logger.error("cmdb trigger exception: {}".format(e))
time.sleep(60)
for _ci in cis:
if _ci.ci_id == ci.ci_id:
cis.remove(_ci)
i += 1
time.sleep(10)
@click.command()
@@ -264,197 +302,3 @@ def cmdb_index_table_upgrade():
CIIndexValueDateTime.create(ci_id=i.ci_id, attr_id=i.attr_id, value=i.value, commit=False)
i.delete(commit=False)
db.session.commit()
def valid_address(address):
if not address:
return False
if not address.startswith(("http://127.0.0.1", "https://127.0.0.1")):
response = {
"message": "Address should start with http://127.0.0.1 or https://127.0.0.1",
"status": "failed"
}
KeyManage.print_response(response)
return False
return True
@click.command()
@click.option(
'-a',
'--address',
help='inner cmdb api, http://127.0.0.1:8000',
)
@with_appcontext
def cmdb_inner_secrets_init(address):
"""
init inner secrets for password feature
"""
res, ok = KeyManage(backend=InnerKVManger).init()
if not ok:
if res.get("status") == "failed":
KeyManage.print_response(res)
return
token = res.get("details", {}).get("root_token", "")
if valid_address(address):
token = current_app.config.get("INNER_TRIGGER_TOKEN", "") if not token else token
if not token:
token = click.prompt(f'Enter root token', hide_input=True, confirmation_prompt=False)
assert token is not None
resp = requests.post("{}/api/v0.1/secrets/auto_seal".format(address.strip("/")),
headers={"Inner-Token": token})
if resp.status_code == 200:
KeyManage.print_response(resp.json())
else:
KeyManage.print_response({"message": resp.text or resp.status_code, "status": "failed"})
else:
KeyManage.print_response(res)
@click.command()
@click.option(
'-a',
'--address',
help='inner cmdb api, http://127.0.0.1:8000',
required=True,
)
@with_appcontext
def cmdb_inner_secrets_unseal(address):
"""
unseal the secrets feature
"""
if not valid_address(address):
return
address = "{}/api/v0.1/secrets/unseal".format(address.strip("/"))
for i in range(global_key_threshold):
token = click.prompt(f'Enter unseal token {i + 1}', hide_input=True, confirmation_prompt=False)
assert token is not None
resp = requests.post(address, headers={"Unseal-Token": token})
if resp.status_code == 200:
KeyManage.print_response(resp.json())
if resp.json().get("status") in ["success", "skip"]:
return
else:
KeyManage.print_response({"message": resp.status_code, "status": "failed"})
return
@click.command()
@click.option(
'-a',
'--address',
help='inner cmdb api, http://127.0.0.1:8000',
required=True,
)
@click.option(
'-k',
'--token',
help='root token',
prompt=True,
hide_input=True,
)
@with_appcontext
def cmdb_inner_secrets_seal(address, token):
"""
seal the secrets feature
"""
assert address is not None
assert token is not None
if not valid_address(address):
return
address = "{}/api/v0.1/secrets/seal".format(address.strip("/"))
resp = requests.post(address, headers={
"Inner-Token": token,
})
if resp.status_code == 200:
KeyManage.print_response(resp.json())
else:
KeyManage.print_response({"message": resp.status_code, "status": "failed"})
@click.command()
@with_appcontext
def cmdb_password_data_migrate():
"""
Migrate CI password data, version >= v2.3.6
"""
from api.models.cmdb import CIIndexValueText
from api.models.cmdb import CIValueText
from api.lib.secrets.inner import InnerCrypt
from api.lib.secrets.vault import VaultClient
attrs = Attribute.get_by(to_dict=False)
for attr in attrs:
if attr.is_password:
value_table = CIIndexValueText if attr.is_index else CIValueText
failed = False
for i in value_table.get_by(attr_id=attr.id, to_dict=False):
if current_app.config.get("SECRETS_ENGINE", 'inner') == 'inner':
_, status = InnerCrypt().decrypt(i.value)
if status:
continue
encrypt_value, status = InnerCrypt().encrypt(i.value)
if status:
CIValueText.create(ci_id=i.ci_id, attr_id=attr.id, value=encrypt_value)
else:
failed = True
continue
elif current_app.config.get("SECRETS_ENGINE") == 'vault':
if i.value == '******':
continue
vault = VaultClient(current_app.config.get('VAULT_URL'), current_app.config.get('VAULT_TOKEN'))
try:
vault.update("/{}/{}".format(i.ci_id, i.attr_id), dict(v=i.value))
except Exception as e:
print('save password to vault failed: {}'.format(e))
failed = True
continue
else:
continue
i.delete()
if not failed and attr.is_index:
attr.update(is_index=False)
@click.command()
@with_appcontext
def cmdb_agent_init():
"""
Initialize the agent's permissions and obtain the key and secret
"""
from api.models.acl import User
user = User.get_by(username="cmdb_agent", first=True, to_dict=False)
if user is None:
click.echo(
click.style('user cmdb_agent does not exist, please use flask add-user to create it first', fg='red'))
return
# grant
_app = AppCache.get('cmdb') or App.create(name='cmdb')
app_id = _app.id
ci_types = CIType.get_by(to_dict=False)
resource_type_id = ResourceType.get_by(name=ResourceTypeEnum.CI, first=True, to_dict=False).id
for ci_type in ci_types:
try:
ResourceCRUD.add(ci_type.name, resource_type_id, app_id)
except AbortException:
pass
ACLManager().grant_resource_to_role(ci_type.name,
"cmdb_agent",
ResourceTypeEnum.CI,
[PermEnum.READ, PermEnum.UPDATE, PermEnum.ADD, PermEnum.DELETE])
click.echo("Key : {}".format(click.style(user.key, bg='red')))
click.echo("Secret: {}".format(click.style(user.secret, bg='red')))

View File

@@ -10,6 +10,9 @@ from api.models.common_setting import Employee, Department
class InitEmployee(object):
"""
初始化员工
"""
def __init__(self):
self.log = current_app.logger
@@ -55,8 +58,7 @@ class InitEmployee(object):
self.log.error(ErrFormat.acl_import_user_failed.format(user['username'], str(e)))
self.log.error(e)
@staticmethod
def get_rid_by_uid(uid):
def get_rid_by_uid(self, uid):
from api.models.acl import Role
role = Role.get_by(first=True, uid=uid)
return role['id'] if role is not None else 0
@@ -69,8 +71,7 @@ class InitDepartment(object):
def init(self):
self.init_wide_company()
@staticmethod
def hard_delete(department_id, department_name):
def hard_delete(self, department_id, department_name):
existed_deleted_list = Department.query.filter(
Department.department_name == department_name,
Department.department_id == department_id,
@@ -79,12 +80,11 @@ class InitDepartment(object):
for existed in existed_deleted_list:
existed.delete()
@staticmethod
def get_department(department_name):
def get_department(self, department_name):
return Department.query.filter(
Department.department_name == department_name,
Department.deleted == 0,
).first()
).order_by(Department.created_at.asc()).first()
def run(self, department_id, department_name, department_parent_id):
self.hard_delete(department_id, department_name)
@@ -94,7 +94,7 @@ class InitDepartment(object):
if res.department_id == department_id:
return
else:
res.update(
new_d = res.update(
department_id=department_id,
department_parent_id=department_parent_id,
)
@@ -108,11 +108,11 @@ class InitDepartment(object):
new_d = self.get_department(department_name)
if new_d.department_id != department_id:
new_d.update(
new_d = new_d.update(
department_id=department_id,
department_parent_id=department_parent_id,
)
self.log.info(f"init {department_name} success.")
self.log.info(f"初始化 {department_name} 部门成功.")
def run_common(self, department_id, department_name, department_parent_id):
try:
@@ -123,14 +123,19 @@ class InitDepartment(object):
raise Exception(e)
def init_wide_company(self):
"""
创建 id 0, name 全公司 的部门
"""
department_id = 0
department_name = '全公司'
department_parent_id = -1
self.run_common(department_id, department_name, department_parent_id)
@staticmethod
def create_acl_role_with_department():
def create_acl_role_with_department(self):
"""
当前所有部门在ACL创建 role
"""
acl = ACLManager('acl')
role_name_map = {role['name']: role for role in acl.get_all_roles()}
@@ -141,7 +146,7 @@ class InitDepartment(object):
continue
role = role_name_map.get(department.department_name)
if not role:
if role is None:
payload = {
'app_id': 'acl',
'name': department.department_name,
@@ -160,65 +165,50 @@ class InitDepartment(object):
acl = self.check_app('backend')
resources_types = acl.get_all_resources_types()
perms = ['read', 'grant', 'delete', 'update']
acl_rid = self.get_admin_user_rid()
results = list(filter(lambda t: t['name'] == '操作权限', resources_types['groups']))
if len(results) == 0:
payload = dict(
app_id=acl.app_name,
name='操作权限',
description='',
perms=perms
perms=['read', 'grant', 'delete', 'update']
)
resource_type = acl.create_resources_type(payload)
else:
resource_type = results[0]
resource_type_id = resource_type['id']
existed_perms = resources_types.get('id2perms', {}).get(resource_type_id, [])
existed_perms = [p['name'] for p in existed_perms]
new_perms = []
for perm in perms:
if perm not in existed_perms:
new_perms.append(perm)
if len(new_perms) > 0:
resource_type['perms'] = existed_perms + new_perms
acl.update_resources_type(resource_type_id, resource_type)
resource_list = acl.get_resource_by_type(None, None, resource_type['id'])
for name in ['公司信息']:
payload = dict(
type_id=resource_type['id'],
app_id=acl.app_name,
name=name,
)
try:
acl.create_resource(payload)
except Exception as e:
if '已经存在' in str(e):
pass
else:
raise Exception(e)
for name in ['公司信息', '公司架构', '通知设置']:
target = list(filter(lambda r: r['name'] == name, resource_list))
if len(target) == 0:
payload = dict(
type_id=resource_type['id'],
app_id=acl.app_name,
name=name,
)
resource = acl.create_resource(payload)
else:
resource = target[0]
if acl_rid > 0:
acl.grant_resource(acl_rid, resource['id'], perms)
@staticmethod
def check_app(app_name):
def check_app(self, app_name):
acl = ACLManager(app_name)
payload = dict(
name=app_name,
description=app_name
)
app = acl.validate_app()
if not app:
acl.create_app(payload)
return acl
try:
app = acl.validate_app()
if app:
return acl
@staticmethod
def get_admin_user_rid():
admin = Employee.get_by(first=True, username='admin', to_dict=False)
return admin.acl_rid if admin else 0
acl.create_app(payload)
except Exception as e:
current_app.logger.error(e)
if '不存在' in str(e):
acl.create_app(payload)
return acl
raise Exception(e)
@click.command()
@@ -240,62 +230,3 @@ def init_department():
cli.init_wide_company()
cli.create_acl_role_with_department()
cli.init_backend_resource()
@click.command()
@with_appcontext
def common_check_new_columns():
"""
add new columns to tables
"""
from api.extensions import db
from sqlalchemy import inspect, text
def get_model_by_table_name(_table_name):
registry = getattr(db.Model, 'registry', None)
class_registry = getattr(registry, '_class_registry', None)
for _model in class_registry.values():
if hasattr(_model, '__tablename__') and _model.__tablename__ == _table_name:
return _model
return None
def add_new_column(target_table_name, new_column):
column_type = new_column.type.compile(engine.dialect)
default_value = new_column.default.arg if new_column.default else None
sql = "ALTER TABLE " + target_table_name + " ADD COLUMN " + new_column.name + " " + column_type
if new_column.comment:
sql += f" comment '{new_column.comment}'"
if column_type == 'JSON':
pass
elif default_value:
if column_type.startswith('VAR') or column_type.startswith('Text'):
if default_value is None or len(default_value) == 0:
pass
else:
sql += f" DEFAULT {default_value}"
sql = text(sql)
db.session.execute(sql)
engine = db.get_engine()
inspector = inspect(engine)
table_names = inspector.get_table_names()
for table_name in table_names:
existed_columns = inspector.get_columns(table_name)
existed_column_name_list = [c['name'] for c in existed_columns]
model = get_model_by_table_name(table_name)
if model is None:
continue
model_columns = getattr(getattr(getattr(model, '__table__'), 'columns'), '_all_columns')
for column in model_columns:
if column.name not in existed_column_name_list:
try:
add_new_column(table_name, column)
current_app.logger.info(f"add new column [{column.name}] in table [{table_name}] success.")
except Exception as e:
current_app.logger.error(f"add new column [{column.name}] in table [{table_name}] err:")
current_app.logger.error(e)

View File

@@ -84,6 +84,66 @@ def clean():
os.remove(full_pathname)
@click.command()
@click.option("--url", default=None, help="Url to test (ex. /static/image.png)")
@click.option(
"--order", default="rule", help="Property on Rule to order by (default: rule)"
)
@with_appcontext
def urls(url, order):
"""Display all of the url matching routes for the project.
Borrowed from Flask-Script, converted to use Click.
"""
rows = []
column_headers = ("Rule", "Endpoint", "Arguments")
if url:
try:
rule, arguments = current_app.url_map.bind("localhost").match(
url, return_rule=True
)
rows.append((rule.rule, rule.endpoint, arguments))
column_length = 3
except (NotFound, MethodNotAllowed) as e:
rows.append(("<{}>".format(e), None, None))
column_length = 1
else:
rules = sorted(
current_app.url_map.iter_rules(), key=lambda rule: getattr(rule, order)
)
for rule in rules:
rows.append((rule.rule, rule.endpoint, None))
column_length = 2
str_template = ""
table_width = 0
if column_length >= 1:
max_rule_length = max(len(r[0]) for r in rows)
max_rule_length = max_rule_length if max_rule_length > 4 else 4
str_template += "{:" + str(max_rule_length) + "}"
table_width += max_rule_length
if column_length >= 2:
max_endpoint_length = max(len(str(r[1])) for r in rows)
max_endpoint_length = max_endpoint_length if max_endpoint_length > 8 else 8
str_template += " {:" + str(max_endpoint_length) + "}"
table_width += 2 + max_endpoint_length
if column_length >= 3:
max_arguments_length = max(len(str(r[2])) for r in rows)
max_arguments_length = max_arguments_length if max_arguments_length > 9 else 9
str_template += " {:" + str(max_arguments_length) + "}"
table_width += 2 + max_arguments_length
click.echo(str_template.format(*column_headers[:column_length]))
click.echo("-" * table_width)
for row in rows:
click.echo(str_template.format(*row[:column_length]))
@click.command()
@with_appcontext
def db_setup():

View File

@@ -9,7 +9,6 @@ from flask_login import LoginManager
from flask_migrate import Migrate
from flask_sqlalchemy import SQLAlchemy
from api.lib.secrets.inner import KeyManage
from api.lib.utils import ESHandler
from api.lib.utils import RedisHandler
@@ -22,4 +21,3 @@ celery = Celery()
cors = CORS(supports_credentials=True)
rd = RedisHandler()
es = ESHandler()
inner_secrets = KeyManage()

View File

@@ -1,5 +1,6 @@
# -*- coding:utf-8 -*-
# -*- coding:utf-8 -*-
import requests
from flask import abort
from flask import current_app
from flask import session
@@ -22,7 +23,6 @@ from api.lib.cmdb.utils import ValueTypeMap
from api.lib.decorator import kwargs_required
from api.lib.perm.acl.acl import is_app_admin
from api.lib.perm.acl.acl import validate_permission
from api.lib.webhook import webhook_request
from api.models.cmdb import Attribute
from api.models.cmdb import CIType
from api.models.cmdb import CITypeAttribute
@@ -40,11 +40,15 @@ class AttributeManager(object):
pass
@staticmethod
def _get_choice_values_from_webhook(choice_webhook, payload=None):
ret_key = choice_webhook.get('ret_key')
def _get_choice_values_from_web_hook(choice_web_hook):
url = choice_web_hook.get('url')
ret_key = choice_web_hook.get('ret_key')
headers = choice_web_hook.get('headers') or {}
payload = choice_web_hook.get('payload') or {}
method = (choice_web_hook.get('method') or 'GET').lower()
try:
res = webhook_request(choice_webhook, payload or {}).json()
res = getattr(requests, method)(url, headers=headers, data=payload).json()
if ret_key:
ret_key_list = ret_key.strip().split("##")
for key in ret_key_list[:-1]:
@@ -59,57 +63,19 @@ class AttributeManager(object):
current_app.logger.error("get choice values failed: {}".format(e))
return []
@staticmethod
def _get_choice_values_from_other(choice_other):
from api.lib.cmdb.search import SearchError
from api.lib.cmdb.search.ci import search
if choice_other.get('type_ids'):
type_ids = choice_other.get('type_ids')
attr_id = choice_other.get('attr_id')
other_filter = choice_other.get('filter') or ''
query = "_type:({}),{}".format(";".join(map(str, type_ids)), other_filter)
s = search(query, fl=[str(attr_id)], facet=[str(attr_id)], count=1)
try:
_, _, _, _, _, facet = s.search()
return [[i[0], {}] for i in (list(facet.values()) or [[]])[0]]
except SearchError as e:
current_app.logger.error("get choice values from other ci failed: {}".format(e))
return []
elif choice_other.get('script'):
try:
x = compile(choice_other['script'], '', "exec")
local_ns = {}
exec(x, {}, local_ns)
res = local_ns['ChoiceValue']().values() or []
return [[i, {}] for i in res]
except Exception as e:
current_app.logger.error("get choice values from script: {}".format(e))
return []
@classmethod
def get_choice_values(cls, attr_id, value_type, choice_web_hook, choice_other,
choice_web_hook_parse=True, choice_other_parse=True):
def get_choice_values(cls, attr_id, value_type, choice_web_hook, choice_web_hook_parse=True):
if choice_web_hook:
if choice_web_hook_parse and isinstance(choice_web_hook, dict):
return cls._get_choice_values_from_webhook(choice_web_hook)
else:
return []
elif choice_other:
if choice_other_parse and isinstance(choice_other, dict):
return cls._get_choice_values_from_other(choice_other)
if choice_web_hook_parse:
if isinstance(choice_web_hook, dict):
return cls._get_choice_values_from_web_hook(choice_web_hook)
else:
return []
choice_table = ValueTypeMap.choice.get(value_type)
if not choice_table:
return []
choice_values = choice_table.get_by(fl=["value", "option"], attr_id=attr_id)
return [[ValueTypeMap.serialize[value_type](choice_value['value']), choice_value['option']]
for choice_value in choice_values]
return [[choice_value['value'], choice_value['option']] for choice_value in choice_values]
@staticmethod
def add_choice_values(_id, value_type, choice_values):
@@ -156,8 +122,7 @@ class AttributeManager(object):
res = list()
for attr in attrs:
attr["is_choice"] and attr.update(
dict(choice_value=cls.get_choice_values(attr["id"], attr["value_type"],
attr["choice_web_hook"], attr.get("choice_other"))))
dict(choice_value=cls.get_choice_values(attr["id"], attr["value_type"], attr["choice_web_hook"])))
attr['is_choice'] and attr.pop('choice_web_hook', None)
res.append(attr)
@@ -167,38 +132,29 @@ class AttributeManager(object):
def get_attribute_by_name(self, name):
attr = Attribute.get_by(name=name, first=True)
if attr.get("is_choice"):
attr["choice_value"] = self.get_choice_values(attr["id"], attr["value_type"],
attr["choice_web_hook"], attr.get("choice_other"))
attr["choice_value"] = self.get_choice_values(attr["id"], attr["value_type"], attr["choice_web_hook"])
return attr
def get_attribute_by_alias(self, alias):
attr = Attribute.get_by(alias=alias, first=True)
if attr.get("is_choice"):
attr["choice_value"] = self.get_choice_values(attr["id"], attr["value_type"],
attr["choice_web_hook"], attr.get("choice_other"))
attr["choice_value"] = self.get_choice_values(attr["id"], attr["value_type"], attr["choice_web_hook"])
return attr
def get_attribute_by_id(self, _id):
attr = Attribute.get_by_id(_id).to_dict()
if attr.get("is_choice"):
attr["choice_value"] = self.get_choice_values(attr["id"], attr["value_type"],
attr["choice_web_hook"], attr.get("choice_other"))
attr["choice_value"] = self.get_choice_values(attr["id"], attr["value_type"], attr["choice_web_hook"])
return attr
def get_attribute(self, key, choice_web_hook_parse=True, choice_other_parse=True):
def get_attribute(self, key, choice_web_hook_parse=True):
attr = AttributeCache.get(key).to_dict()
if attr.get("is_choice"):
attr["choice_value"] = self.get_choice_values(
attr["id"],
attr["value_type"],
attr["choice_web_hook"],
attr.get("choice_other"),
choice_web_hook_parse=choice_web_hook_parse,
choice_other_parse=choice_other_parse,
)
attr["id"], attr["value_type"], attr["choice_web_hook"], choice_web_hook_parse=choice_web_hook_parse)
return attr
@@ -225,22 +181,12 @@ class AttributeManager(object):
def add(cls, **kwargs):
choice_value = kwargs.pop("choice_value", [])
kwargs.pop("is_choice", None)
is_choice = True if choice_value or kwargs.get('choice_web_hook') or kwargs.get('choice_other') else False
is_choice = True if choice_value or kwargs.get('choice_web_hook') else False
name = kwargs.pop("name")
if name in BUILTIN_KEYWORDS:
return abort(400, ErrFormat.attribute_name_cannot_be_builtin)
while kwargs.get('choice_other'):
if isinstance(kwargs['choice_other'], dict):
if kwargs['choice_other'].get('script'):
break
if kwargs['choice_other'].get('type_ids') and kwargs['choice_other'].get('attr_id'):
break
return abort(400, ErrFormat.attribute_choice_other_invalid)
alias = kwargs.pop("alias", "")
alias = name if not alias else alias
Attribute.get_by(name=name, first=True) and abort(400, ErrFormat.attribute_name_duplicate.format(name))
@@ -250,8 +196,6 @@ class AttributeManager(object):
kwargs.get('is_computed') and cls.can_create_computed_attribute()
kwargs.get('choice_other') and kwargs['choice_other'].get('script') and cls.can_create_computed_attribute()
attr = Attribute.create(flush=True,
name=name,
alias=alias,
@@ -337,6 +281,9 @@ class AttributeManager(object):
def update(self, _id, **kwargs):
attr = Attribute.get_by_id(_id) or abort(404, ErrFormat.attribute_not_found.format("id={}".format(_id)))
if not self._can_edit_attribute(attr):
return abort(403, ErrFormat.cannot_edit_attribute)
if kwargs.get("name"):
other = Attribute.get_by(name=kwargs['name'], first=True, to_dict=False)
if other and other.id != attr.id:
@@ -354,22 +301,12 @@ class AttributeManager(object):
self._change_index(attr, attr.is_index, kwargs['is_index'])
while kwargs.get('choice_other'):
if isinstance(kwargs['choice_other'], dict):
if kwargs['choice_other'].get('script'):
break
if kwargs['choice_other'].get('type_ids') and kwargs['choice_other'].get('attr_id'):
break
return abort(400, ErrFormat.attribute_choice_other_invalid)
existed2 = attr.to_dict()
if not existed2['choice_web_hook'] and not existed2.get('choice_other') and existed2['is_choice']:
existed2['choice_value'] = self.get_choice_values(attr.id, attr.value_type, None, None)
if not existed2['choice_web_hook'] and existed2['is_choice']:
existed2['choice_value'] = self.get_choice_values(attr.id, attr.value_type, attr.choice_web_hook)
choice_value = kwargs.pop("choice_value", False)
is_choice = True if choice_value or kwargs.get('choice_web_hook') or kwargs.get('choice_other') else False
is_choice = True if choice_value or kwargs.get('choice_web_hook') else False
kwargs['is_choice'] = is_choice
if kwargs.get('default') and not (isinstance(kwargs['default'], dict) and 'default' in kwargs['default']):
@@ -377,14 +314,6 @@ class AttributeManager(object):
kwargs.get('is_computed') and self.can_create_computed_attribute()
is_changed = False
for k in kwargs:
if kwargs[k] != getattr(attr, k, None):
is_changed = True
if is_changed and not self._can_edit_attribute(attr):
return abort(403, ErrFormat.cannot_edit_attribute)
attr.update(flush=True, filter_none=False, **kwargs)
if is_choice and choice_value:

View File

@@ -36,10 +36,9 @@ def parse_plugin_script(script):
attributes = []
try:
x = compile(script, '', "exec")
local_ns = {}
exec(x, {}, local_ns)
unique_key = local_ns['AutoDiscovery']().unique_key
attrs = local_ns['AutoDiscovery']().attributes() or []
exec(x)
unique_key = locals()['AutoDiscovery']().unique_key
attrs = locals()['AutoDiscovery']().attributes() or []
except Exception as e:
return abort(400, str(e))

View File

@@ -335,20 +335,14 @@ class CMDBCounterCache(object):
def attribute_counter(custom):
from api.lib.cmdb.search import SearchError
from api.lib.cmdb.search.ci import search
from api.lib.cmdb.utils import ValueTypeMap
custom.setdefault('options', {})
type_id = custom.get('type_id')
attr_id = custom.get('attr_id')
type_ids = custom['options'].get('type_ids') or (type_id and [type_id])
attr_ids = list(map(str, custom['options'].get('attr_ids') or (attr_id and [attr_id])))
try:
attr2value_type = [AttributeCache.get(i).value_type for i in attr_ids]
except AttributeError:
return
other_filter = custom['options'].get('filter')
other_filter = "{}".format(other_filter) if other_filter else ''
other_filter = "({})".format(other_filter) if other_filter else ''
if custom['options'].get('ret') == 'cis':
query = "_type:({}),{}".format(";".join(map(str, type_ids)), other_filter)
@@ -371,7 +365,7 @@ class CMDBCounterCache(object):
current_app.logger.error(e)
return
for i in (list(facet.values()) or [[]])[0]:
result[ValueTypeMap.serialize2[attr2value_type[0]](str(i[0]))] = i[1]
result[i[0]] = i[1]
if len(attr_ids) == 1:
return result
@@ -386,7 +380,7 @@ class CMDBCounterCache(object):
return
result[v] = dict()
for i in (list(facet.values()) or [[]])[0]:
result[v][ValueTypeMap.serialize2[attr2value_type[1]](str(i[0]))] = i[1]
result[v][i[0]] = i[1]
if len(attr_ids) == 2:
return result
@@ -406,7 +400,7 @@ class CMDBCounterCache(object):
return
result[v1][v2] = dict()
for i in (list(facet.values()) or [[]])[0]:
result[v1][v2][ValueTypeMap.serialize2[attr2value_type[2]](str(i[0]))] = i[1]
result[v1][v2][i[0]] = i[1]
return result

View File

@@ -4,7 +4,6 @@
import copy
import datetime
import json
import threading
from flask import abort
from flask import current_app
@@ -25,45 +24,34 @@ from api.lib.cmdb.const import CMDB_QUEUE
from api.lib.cmdb.const import ConstraintEnum
from api.lib.cmdb.const import ExistPolicy
from api.lib.cmdb.const import OperateType
from api.lib.cmdb.const import PermEnum
from api.lib.cmdb.const import PermEnum, ResourceTypeEnum
from api.lib.cmdb.const import REDIS_PREFIX_CI
from api.lib.cmdb.const import ResourceTypeEnum
from api.lib.cmdb.const import RetKey
from api.lib.cmdb.const import ValueTypeEnum
from api.lib.cmdb.history import AttributeHistoryManger
from api.lib.cmdb.history import CIRelationHistoryManager
from api.lib.cmdb.history import CITriggerHistoryManager
from api.lib.cmdb.perms import CIFilterPermsCRUD
from api.lib.cmdb.resp_format import ErrFormat
from api.lib.cmdb.utils import TableMap
from api.lib.cmdb.utils import ValueTypeMap
from api.lib.cmdb.value import AttributeValueManager
from api.lib.decorator import kwargs_required
from api.lib.notify import notify_send
from api.lib.perm.acl.acl import ACLManager
from api.lib.perm.acl.acl import is_app_admin
from api.lib.perm.acl.acl import validate_permission
from api.lib.secrets.inner import InnerCrypt
from api.lib.secrets.vault import VaultClient
from api.lib.utils import Lock
from api.lib.utils import handle_arg_list
from api.lib.webhook import webhook_request
from api.models.cmdb import AttributeHistory
from api.models.cmdb import AutoDiscoveryCI
from api.models.cmdb import CI
from api.models.cmdb import CIRelation
from api.models.cmdb import CITypeAttribute
from api.models.cmdb import CITypeRelation
from api.models.cmdb import CITypeTrigger
from api.tasks.cmdb import ci_cache
from api.tasks.cmdb import ci_delete
from api.tasks.cmdb import ci_delete_trigger
from api.tasks.cmdb import ci_relation_add
from api.tasks.cmdb import ci_relation_cache
from api.tasks.cmdb import ci_relation_delete
PRIVILEGED_USERS = {"worker", "cmdb_agent", "agent"}
PASSWORD_DEFAULT_SHOW = "******"
class CIManager(object):
@@ -327,8 +315,6 @@ class CIManager(object):
ci_attr2type_attr = {type_attr.attr_id: type_attr for type_attr, _ in attrs}
ci = None
record_id = None
password_dict = {}
need_lock = current_user.username not in current_app.config.get('PRIVILEGED_USERS', PRIVILEGED_USERS)
with Lock(ci_type_name, need_lock=need_lock):
existed = cls.ci_is_exist(unique_key, unique_value, ci_type.id)
@@ -357,23 +343,14 @@ class CIManager(object):
ci_dict.get(attr.name) is None and ci_dict.get(attr.alias) is None)):
ci_dict[attr.name] = attr.default.get('default')
if (type_attr.is_required and not attr.is_computed and
(attr.name not in ci_dict and attr.alias not in ci_dict)):
if type_attr.is_required and (attr.name not in ci_dict and attr.alias not in ci_dict):
return abort(400, ErrFormat.attribute_value_required.format(attr.name))
else:
for type_attr, attr in attrs:
if attr.default and attr.default.get('default') == AttributeDefaultValueEnum.UPDATED_AT:
ci_dict[attr.name] = now
computed_attrs = []
for _, attr in attrs:
if attr.is_computed:
computed_attrs.append(attr.to_dict())
elif attr.is_password:
if attr.name in ci_dict:
password_dict[attr.id] = ci_dict.pop(attr.name)
elif attr.alias in ci_dict:
password_dict[attr.id] = ci_dict.pop(attr.alias)
computed_attrs = [attr.to_dict() for _, attr in attrs if attr.is_computed] or None
value_manager = AttributeValueManager()
@@ -401,21 +378,16 @@ class CIManager(object):
key2attr = value_manager.valid_attr_value(ci_dict, ci_type.id, ci and ci.id,
ci_type_attrs_name, ci_type_attrs_alias, ci_attr2type_attr)
operate_type = OperateType.UPDATE if ci is not None else OperateType.ADD
try:
ci = ci or CI.create(type_id=ci_type.id, is_auto_discovery=is_auto_discovery)
record_id = value_manager.create_or_update_attr_value(ci, ci_dict, key2attr)
record_id = value_manager.create_or_update_attr_value2(ci, ci_dict, key2attr)
except BadRequest as e:
if existed is None:
cls.delete(ci.id)
raise e
if password_dict:
for attr_id in password_dict:
record_id = cls.save_password(ci.id, attr_id, password_dict[attr_id], record_id, ci_type.id)
if record_id: # has change
ci_cache.apply_async(args=(ci.id, operate_type, record_id), queue=CMDB_QUEUE)
ci_cache.apply_async([ci.id], queue=CMDB_QUEUE)
if ref_ci_dict: # add relations
ci_relation_add.apply_async(args=(ref_ci_dict, ci.id, current_user.uid), queue=CMDB_QUEUE)
@@ -433,16 +405,7 @@ class CIManager(object):
if attr.default and attr.default.get('default') == AttributeDefaultValueEnum.UPDATED_AT:
ci_dict[attr.name] = now
password_dict = dict()
computed_attrs = list()
for _, attr in attrs:
if attr.is_computed:
computed_attrs.append(attr.to_dict())
elif attr.is_password:
if attr.name in ci_dict:
password_dict[attr.id] = ci_dict.pop(attr.name)
elif attr.alias in ci_dict:
password_dict[attr.id] = ci_dict.pop(attr.alias)
computed_attrs = [attr.to_dict() for _, attr in attrs if attr.is_computed] or None
value_manager = AttributeValueManager()
@@ -451,7 +414,6 @@ class CIManager(object):
limit_attrs = self._valid_ci_for_no_read(ci) if not _is_admin else {}
record_id = None
need_lock = current_user.username not in current_app.config.get('PRIVILEGED_USERS', PRIVILEGED_USERS)
with Lock(ci.ci_type.name, need_lock=need_lock):
self._valid_unique_constraint(ci.type_id, ci_dict, ci_id)
@@ -465,16 +427,12 @@ class CIManager(object):
return abort(403, ErrFormat.ci_filter_perm_attr_no_permission.format(k))
try:
record_id = value_manager.create_or_update_attr_value(ci, ci_dict, key2attr)
record_id = value_manager.create_or_update_attr_value2(ci, ci_dict, key2attr)
except BadRequest as e:
raise e
if password_dict:
for attr_id in password_dict:
record_id = self.save_password(ci.id, attr_id, password_dict[attr_id], record_id, ci.type_id)
if record_id: # has change
ci_cache.apply_async(args=(ci_id, OperateType.UPDATE, record_id), queue=CMDB_QUEUE)
ci_cache.apply_async([ci_id], queue=CMDB_QUEUE)
ref_ci_dict = {k: v for k, v in ci_dict.items() if k.startswith("$") and "." in k}
if ref_ci_dict:
@@ -484,10 +442,9 @@ class CIManager(object):
def update_unique_value(ci_id, unique_name, unique_value):
ci = CI.get_by_id(ci_id) or abort(404, ErrFormat.ci_not_found.format("id={}".format(ci_id)))
key2attr = {unique_name: AttributeCache.get(unique_name)}
record_id = AttributeValueManager().create_or_update_attr_value(ci, {unique_name: unique_value}, key2attr)
AttributeValueManager().create_or_update_attr_value(unique_name, unique_value, ci)
ci_cache.apply_async(args=(ci_id, OperateType.UPDATE, record_id), queue=CMDB_QUEUE)
ci_cache.apply_async([ci_id], queue=CMDB_QUEUE)
@classmethod
def delete(cls, ci_id):
@@ -498,18 +455,6 @@ class CIManager(object):
ci_dict = cls.get_cis_by_ids([ci_id])
ci_dict = ci_dict and ci_dict[0]
if ci_dict:
triggers = CITriggerManager.get(ci_dict['_type'])
for trigger in triggers:
option = trigger['option']
if not option.get('enable') or option.get('action') != OperateType.DELETE:
continue
if option.get('filter') and not CITriggerManager.ci_filter(ci_dict.get('_id'), option['filter']):
continue
ci_delete_trigger.apply_async(args=(trigger, OperateType.DELETE, ci_dict), queue=CMDB_QUEUE)
attrs = CITypeAttribute.get_by(type_id=ci.type_id, to_dict=False)
attr_names = set([AttributeCache.get(attr.attr_id).name for attr in attrs])
for attr_name in attr_names:
@@ -532,10 +477,9 @@ class CIManager(object):
db.session.commit()
if ci_dict:
AttributeHistoryManger.add(None, ci_id, [(None, OperateType.DELETE, ci_dict, None)], ci.type_id)
AttributeHistoryManger.add(None, ci_id, [(None, OperateType.DELETE, ci_dict, None)], ci.type_id)
ci_delete.apply_async(args=(ci_id,), queue=CMDB_QUEUE)
ci_delete.apply_async([ci.id], queue=CMDB_QUEUE)
return ci_id
@@ -635,13 +579,10 @@ class CIManager(object):
_fields = list()
for field in fields:
attr = AttributeCache.get(field)
if attr is not None and not attr.is_password:
if attr is not None:
_fields.append(str(attr.id))
filter_fields_sql = "WHERE A.attr_id in ({0})".format(",".join(_fields))
ci2pos = {int(_id): _pos for _pos, _id in enumerate(ci_ids)}
res = [None] * len(ci_ids)
ci_ids = ",".join(map(str, ci_ids))
if value_tables is None:
value_tables = ValueTypeMap.table_name.values()
@@ -652,10 +593,11 @@ class CIManager(object):
# current_app.logger.debug(query_sql)
cis = db.session.execute(query_sql).fetchall()
ci_set = set()
res = list()
ci_dict = dict()
unique_id2obj = dict()
excludes = excludes and set(excludes)
for ci_id, type_id, attr_id, attr_name, attr_alias, value, value_type, is_list, is_password in cis:
for ci_id, type_id, attr_id, attr_name, attr_alias, value, value_type, is_list in cis:
if not fields and excludes and (attr_name in excludes or attr_alias in excludes):
continue
@@ -671,7 +613,7 @@ class CIManager(object):
ci_dict["unique"] = unique_id2obj[ci_type.unique_id] and unique_id2obj[ci_type.unique_id].name
ci_dict["unique_alias"] = unique_id2obj[ci_type.unique_id] and unique_id2obj[ci_type.unique_id].alias
ci_set.add(ci_id)
res[ci2pos[ci_id]] = ci_dict
res.append(ci_dict)
if ret_key == RetKey.NAME:
attr_key = attr_name
@@ -682,14 +624,11 @@ class CIManager(object):
else:
return abort(400, ErrFormat.argument_invalid.format("ret_key"))
if is_password and value:
ci_dict[attr_key] = PASSWORD_DEFAULT_SHOW
value = ValueTypeMap.serialize2[value_type](value)
if is_list:
ci_dict.setdefault(attr_key, []).append(value)
else:
value = ValueTypeMap.serialize2[value_type](value)
if is_list:
ci_dict.setdefault(attr_key, []).append(value)
else:
ci_dict[attr_key] = value
ci_dict[attr_key] = value
return res
@@ -721,84 +660,6 @@ class CIManager(object):
return cls._get_cis_from_db(ci_ids, ret_key, fields, value_tables, excludes=excludes)
@classmethod
def save_password(cls, ci_id, attr_id, value, record_id, type_id):
changed = None
encrypt_value = None
value_table = ValueTypeMap.table[ValueTypeEnum.PASSWORD]
if current_app.config.get('SECRETS_ENGINE') == 'inner':
if value:
encrypt_value, status = InnerCrypt().encrypt(value)
if not status:
current_app.logger.error('save password failed: {}'.format(encrypt_value))
return abort(400, ErrFormat.password_save_failed.format(encrypt_value))
else:
encrypt_value = PASSWORD_DEFAULT_SHOW
existed = value_table.get_by(ci_id=ci_id, attr_id=attr_id, first=True, to_dict=False)
if existed is None:
if value:
value_table.create(ci_id=ci_id, attr_id=attr_id, value=encrypt_value)
changed = [(ci_id, attr_id, OperateType.ADD, '', PASSWORD_DEFAULT_SHOW, type_id)]
elif existed.value != encrypt_value:
if value:
existed.update(ci_id=ci_id, attr_id=attr_id, value=encrypt_value)
changed = [(ci_id, attr_id, OperateType.UPDATE, PASSWORD_DEFAULT_SHOW, PASSWORD_DEFAULT_SHOW, type_id)]
else:
existed.delete()
changed = [(ci_id, attr_id, OperateType.DELETE, PASSWORD_DEFAULT_SHOW, '', type_id)]
if current_app.config.get('SECRETS_ENGINE') == 'vault':
vault = VaultClient(current_app.config.get('VAULT_URL'), current_app.config.get('VAULT_TOKEN'))
if value:
try:
vault.update("/{}/{}".format(ci_id, attr_id), dict(v=value))
except Exception as e:
current_app.logger.error('save password to vault failed: {}'.format(e))
return abort(400, ErrFormat.password_save_failed.format('write vault failed'))
else:
try:
vault.delete("/{}/{}".format(ci_id, attr_id))
except Exception as e:
current_app.logger.warning('delete password to vault failed: {}'.format(e))
if changed is not None:
return AttributeValueManager.write_change2(changed, record_id)
@classmethod
def load_password(cls, ci_id, attr_id):
ci = CI.get_by_id(ci_id) or abort(404, ErrFormat.ci_not_found.format(ci_id))
limit_attrs = cls._valid_ci_for_no_read(ci, ci.ci_type)
if limit_attrs:
attr = AttributeCache.get(attr_id)
if attr and attr.name not in limit_attrs:
return abort(403, ErrFormat.no_permission2)
if current_app.config.get('SECRETS_ENGINE', 'inner') == 'inner':
value_table = ValueTypeMap.table[ValueTypeEnum.PASSWORD]
v = value_table.get_by(ci_id=ci_id, attr_id=attr_id, first=True, to_dict=False)
v = v and v.value
if not v:
return
decrypt_value, status = InnerCrypt().decrypt(v)
if not status:
current_app.logger.error('load password failed: {}'.format(decrypt_value))
return abort(400, ErrFormat.password_load_failed.format(decrypt_value))
return decrypt_value
elif current_app.config.get('SECRETS_ENGINE') == 'vault':
vault = VaultClient(current_app.config.get('VAULT_URL'), current_app.config.get('VAULT_TOKEN'))
data, status = vault.read("/{}/{}".format(ci_id, attr_id))
if not status:
current_app.logger.error('read password from vault failed: {}'.format(data))
return abort(400, ErrFormat.password_load_failed.format(data))
return data.get('v')
class CIRelationManager(object):
"""
@@ -1035,180 +896,3 @@ class CIRelationManager(object):
for parent_id in parents:
for ci_id in ci_ids:
cls.delete_2(parent_id, ci_id)
class CITriggerManager(object):
@staticmethod
def get(type_id):
db.session.remove()
return CITypeTrigger.get_by(type_id=type_id, to_dict=True)
@staticmethod
def _update_old_attr_value(record_id, ci_dict):
attr_history = AttributeHistory.get_by(record_id=record_id, to_dict=False)
attr_dict = dict()
for attr_h in attr_history:
attr_dict['old_{}'.format(AttributeCache.get(attr_h.attr_id).name)] = attr_h.old
ci_dict.update({'old_{}'.format(k): ci_dict[k] for k in ci_dict})
ci_dict.update(attr_dict)
@classmethod
def _exec_webhook(cls, operate_type, webhook, ci_dict, trigger_id, trigger_name, record_id, ci_id=None, app=None):
app = app or current_app
with app.app_context():
if operate_type == OperateType.UPDATE:
cls._update_old_attr_value(record_id, ci_dict)
if ci_id is not None:
ci_dict = CIManager().get_ci_by_id_from_db(ci_id, need_children=False, use_master=False)
try:
response = webhook_request(webhook, ci_dict).text
is_ok = True
except Exception as e:
current_app.logger.warning("exec webhook failed: {}".format(e))
response = e
is_ok = False
CITriggerHistoryManager.add(operate_type,
record_id,
ci_dict.get('_id'),
trigger_id,
trigger_name,
is_ok=is_ok,
webhook=response)
return is_ok
@classmethod
def _exec_notify(cls, operate_type, notify, ci_dict, trigger_id, trigger_name, record_id, ci_id=None, app=None):
app = app or current_app
with app.app_context():
if ci_id is not None:
ci_dict = CIManager().get_ci_by_id_from_db(ci_id, need_children=False, use_master=False)
if operate_type == OperateType.UPDATE:
cls._update_old_attr_value(record_id, ci_dict)
is_ok = True
response = ''
for method in (notify.get('method') or []):
try:
res = notify_send(notify.get('subject'), notify.get('body'), [method],
notify.get('tos'), ci_dict)
response = "{}\n{}".format(response, res)
except Exception as e:
current_app.logger.warning("send notify failed: {}".format(e))
response = "{}\n{}".format(response, e)
is_ok = False
CITriggerHistoryManager.add(operate_type,
record_id,
ci_dict.get('_id'),
trigger_id,
trigger_name,
is_ok=is_ok,
notify=response.strip())
return is_ok
@staticmethod
def ci_filter(ci_id, other_filter):
from api.lib.cmdb.search import SearchError
from api.lib.cmdb.search.ci import search
query = "{},_id:{}".format(other_filter, ci_id)
try:
_, _, _, _, numfound, _ = search(query).search()
return numfound
except SearchError as e:
current_app.logger.warning("ci search failed: {}".format(e))
@classmethod
def fire(cls, operate_type, ci_dict, record_id):
type_id = ci_dict.get('_type')
triggers = cls.get(type_id) or []
for trigger in triggers:
option = trigger['option']
if not option.get('enable'):
continue
if option.get('filter') and not cls.ci_filter(ci_dict.get('_id'), option['filter']):
continue
if option.get('attr_ids') and isinstance(option['attr_ids'], list):
if not (set(option['attr_ids']) &
set([i.attr_id for i in AttributeHistory.get_by(record_id=record_id, to_dict=False)])):
continue
if option.get('action') == operate_type:
cls.fire_by_trigger(trigger, operate_type, ci_dict, record_id)
@classmethod
def fire_by_trigger(cls, trigger, operate_type, ci_dict, record_id=None):
option = trigger['option']
if option.get('webhooks'):
cls._exec_webhook(operate_type, option['webhooks'], ci_dict, trigger['id'],
option.get('name'), record_id)
elif option.get('notifies'):
cls._exec_notify(operate_type, option['notifies'], ci_dict, trigger['id'],
option.get('name'), record_id)
@classmethod
def waiting_cis(cls, trigger):
now = datetime.datetime.today()
config = trigger.option.get('notifies') or {}
delta_time = datetime.timedelta(days=(config.get('before_days', 0) or 0))
attr = AttributeCache.get(trigger.attr_id)
value_table = TableMap(attr=attr).table
values = value_table.get_by(attr_id=attr.id, to_dict=False)
result = []
for v in values:
if (isinstance(v.value, (datetime.date, datetime.datetime)) and
(v.value - delta_time).strftime('%Y%m%d') == now.strftime("%Y%m%d")):
if trigger.option.get('filter') and not cls.ci_filter(v.ci_id, trigger.option['filter']):
continue
result.append(v)
return result
@classmethod
def trigger_notify(cls, trigger, ci):
"""
only for date attribute
:param trigger:
:param ci:
:return:
"""
if (trigger.option.get('notifies', {}).get('notify_at') == datetime.datetime.now().strftime("%H:%M") or
not trigger.option.get('notifies', {}).get('notify_at')):
if trigger.option.get('webhooks'):
threading.Thread(target=cls._exec_webhook, args=(
None, trigger.option['webhooks'], None, trigger.id, trigger.option.get('name'), None, ci.ci_id,
current_app._get_current_object())).start()
elif trigger.option.get('notifies'):
threading.Thread(target=cls._exec_notify, args=(
None, trigger.option['notifies'], None, trigger.id, trigger.option.get('name'), None, ci.ci_id,
current_app._get_current_object())).start()
return True
return False

View File

@@ -1,6 +1,7 @@
# -*- coding:utf-8 -*-
import copy
import datetime
import toposort
from flask import abort
@@ -24,6 +25,7 @@ from api.lib.cmdb.const import ValueTypeEnum
from api.lib.cmdb.history import CITypeHistoryManager
from api.lib.cmdb.relation_type import RelationTypeManager
from api.lib.cmdb.resp_format import ErrFormat
from api.lib.cmdb.utils import TableMap
from api.lib.cmdb.value import AttributeValueManager
from api.lib.decorator import kwargs_required
from api.lib.perm.acl.acl import ACLManager
@@ -352,20 +354,19 @@ class CITypeAttributeManager(object):
return [AttributeCache.get(attr.attr_id).name for attr in CITypeAttributesCache.get(type_id)]
@staticmethod
def get_attributes_by_type_id(type_id, choice_web_hook_parse=True, choice_other_parse=True):
def get_attributes_by_type_id(type_id, choice_web_hook_parse=True):
has_config_perm = ACLManager('cmdb').has_permission(
CITypeManager.get_name_by_id(type_id), ResourceTypeEnum.CI, PermEnum.CONFIG)
attrs = CITypeAttributesCache.get(type_id)
result = list()
for attr in sorted(attrs, key=lambda x: (x.order, x.id)):
attr_dict = AttributeManager().get_attribute(attr.attr_id, choice_web_hook_parse, choice_other_parse)
attr_dict = AttributeManager().get_attribute(attr.attr_id, choice_web_hook_parse)
attr_dict["is_required"] = attr.is_required
attr_dict["order"] = attr.order
attr_dict["default_show"] = attr.default_show
if not has_config_perm:
attr_dict.pop('choice_web_hook', None)
attr_dict.pop('choice_other', None)
result.append(attr_dict)
@@ -373,25 +374,13 @@ class CITypeAttributeManager(object):
@staticmethod
def get_common_attributes(type_ids):
has_config_perm = False
for type_id in type_ids:
has_config_perm |= ACLManager('cmdb').has_permission(
CITypeManager.get_name_by_id(type_id), ResourceTypeEnum.CI, PermEnum.CONFIG)
result = CITypeAttribute.get_by(__func_in___key_type_id=list(map(int, type_ids)), to_dict=False)
attr2types = {}
for i in result:
attr2types.setdefault(i.attr_id, []).append(i.type_id)
attrs = []
for attr_id in attr2types:
if len(attr2types[attr_id]) == len(type_ids):
attr = AttributeManager().get_attribute_by_id(attr_id)
if not has_config_perm:
attr.pop('choice_web_hook', None)
attrs.append(attr)
return attrs
return [AttributeCache.get(attr_id).to_dict() for attr_id in attr2types
if len(attr2types[attr_id]) == len(type_ids)]
@staticmethod
def _check(type_id, attr_ids):
@@ -500,7 +489,7 @@ class CITypeAttributeManager(object):
for ci in CI.get_by(type_id=type_id, to_dict=False):
AttributeValueManager.delete_attr_value(attr_id, ci.id)
ci_cache.apply_async(args=(ci.id, None, None), queue=CMDB_QUEUE)
ci_cache.apply_async([ci.id], queue=CMDB_QUEUE)
CITypeAttributeCache.clean(type_id, attr_id)
@@ -533,7 +522,7 @@ class CITypeAttributeManager(object):
CITypeAttributesCache.clean(type_id)
from api.tasks.cmdb import ci_type_attribute_order_rebuild
ci_type_attribute_order_rebuild.apply_async(args=(type_id, current_user.uid), queue=CMDB_QUEUE)
ci_type_attribute_order_rebuild.apply_async(args=(type_id,), queue=CMDB_QUEUE)
class CITypeRelationManager(object):
@@ -593,8 +582,7 @@ class CITypeRelationManager(object):
def get_children(_id, level):
children = CITypeRelation.get_by(parent_id=_id, to_dict=False)
if children:
result.setdefault(level + 1, []).extend([i.child.to_dict() for i in children])
result[level + 1] = [i.child.to_dict() for i in children]
for i in children:
if i.child_id != _id:
@@ -858,7 +846,7 @@ class CITypeAttributeGroupManager(object):
CITypeAttributesCache.clean(type_id)
from api.tasks.cmdb import ci_type_attribute_order_rebuild
ci_type_attribute_order_rebuild.apply_async(args=(type_id, current_user.uid), queue=CMDB_QUEUE)
ci_type_attribute_order_rebuild.apply_async(args=(type_id,), queue=CMDB_QUEUE)
class CITypeTemplateManager(object):
@@ -1103,7 +1091,7 @@ class CITypeTemplateManager(object):
for ci_type in tpt['ci_types']:
tpt['type2attributes'][ci_type['id']] = CITypeAttributeManager.get_attributes_by_type_id(
ci_type['id'], choice_web_hook_parse=False, choice_other_parse=False)
ci_type['id'], choice_web_hook_parse=False)
tpt['type2attribute_group'][ci_type['id']] = CITypeAttributeGroupManager.get_by_type_id(ci_type['id'])
@@ -1177,18 +1165,16 @@ class CITypeUniqueConstraintManager(object):
class CITypeTriggerManager(object):
@staticmethod
def get(type_id, to_dict=True):
return CITypeTrigger.get_by(type_id=type_id, to_dict=to_dict)
def get(type_id):
return CITypeTrigger.get_by(type_id=type_id, to_dict=True)
@staticmethod
def add(type_id, attr_id, option):
for i in CITypeTrigger.get_by(type_id=type_id, attr_id=attr_id, to_dict=False):
if i.option == option:
return abort(400, ErrFormat.ci_type_trigger_duplicate)
def add(type_id, attr_id, notify):
CITypeTrigger.get_by(type_id=type_id, attr_id=attr_id) and abort(400, ErrFormat.ci_type_trigger_duplicate)
not isinstance(option, dict) and abort(400, ErrFormat.argument_invalid.format("option"))
not isinstance(notify, dict) and abort(400, ErrFormat.argument_invalid.format("notify"))
trigger = CITypeTrigger.create(type_id=type_id, attr_id=attr_id, option=option)
trigger = CITypeTrigger.create(type_id=type_id, attr_id=attr_id, notify=notify)
CITypeHistoryManager.add(CITypeOperateType.ADD_TRIGGER,
type_id,
@@ -1198,12 +1184,12 @@ class CITypeTriggerManager(object):
return trigger.to_dict()
@staticmethod
def update(_id, attr_id, option):
def update(_id, notify):
existed = (CITypeTrigger.get_by_id(_id) or
abort(404, ErrFormat.ci_type_trigger_not_found.format("id={}".format(_id))))
existed2 = existed.to_dict()
new = existed.update(attr_id=attr_id or None, option=option, filter_none=False)
new = existed.update(notify=notify)
CITypeHistoryManager.add(CITypeOperateType.UPDATE_TRIGGER,
existed.type_id,
@@ -1223,3 +1209,35 @@ class CITypeTriggerManager(object):
existed.type_id,
trigger_id=_id,
change=existed.to_dict())
@staticmethod
def waiting_cis(trigger):
now = datetime.datetime.today()
delta_time = datetime.timedelta(days=(trigger.notify.get('before_days', 0) or 0))
attr = AttributeCache.get(trigger.attr_id)
value_table = TableMap(attr=attr).table
values = value_table.get_by(attr_id=attr.id, to_dict=False)
result = []
for v in values:
if (isinstance(v.value, (datetime.date, datetime.datetime)) and
(v.value - delta_time).strftime('%Y%m%d') == now.strftime("%Y%m%d")):
result.append(v)
return result
@staticmethod
def trigger_notify(trigger, ci):
if (trigger.notify.get('notify_at') == datetime.datetime.now().strftime("%H:%M") or
not trigger.notify.get('notify_at')):
from api.tasks.cmdb import trigger_notify
trigger_notify.apply_async(args=(trigger.notify, ci.ci_id), queue=CMDB_QUEUE)
return True
return False

View File

@@ -12,8 +12,6 @@ class ValueTypeEnum(BaseEnum):
DATE = "4"
TIME = "5"
JSON = "6"
PASSWORD = TEXT
LINK = TEXT
class ConstraintEnum(BaseEnum):

View File

@@ -16,7 +16,6 @@ from api.lib.perm.acl.cache import UserCache
from api.models.cmdb import Attribute
from api.models.cmdb import AttributeHistory
from api.models.cmdb import CIRelationHistory
from api.models.cmdb import CITriggerHistory
from api.models.cmdb import CITypeHistory
from api.models.cmdb import CITypeTrigger
from api.models.cmdb import CITypeUniqueConstraint
@@ -287,68 +286,3 @@ class CITypeHistoryManager(object):
change=change)
CITypeHistory.create(**payload)
class CITriggerHistoryManager(object):
@staticmethod
def get(page, page_size, type_id=None, trigger_id=None, operate_type=None):
query = CITriggerHistory.get_by(only_query=True)
if type_id:
query = query.filter(CITriggerHistory.type_id == type_id)
if trigger_id:
query = query.filter(CITriggerHistory.trigger_id == trigger_id)
if operate_type:
query = query.filter(CITriggerHistory.operate_type == operate_type)
numfound = query.count()
query = query.order_by(CITriggerHistory.id.desc())
result = query.offset((page - 1) * page_size).limit(page_size)
result = [i.to_dict() for i in result]
for res in result:
if res.get('trigger_id'):
trigger = CITypeTrigger.get_by_id(res['trigger_id'])
res['trigger'] = trigger and trigger.to_dict()
return numfound, result
@staticmethod
def get_by_ci_id(ci_id):
res = db.session.query(CITriggerHistory, CITypeTrigger).join(
CITypeTrigger, CITypeTrigger.id == CITriggerHistory.trigger_id).filter(
CITriggerHistory.ci_id == ci_id).order_by(CITriggerHistory.id.desc())
result = []
id2trigger = dict()
for i in res:
hist = i.CITriggerHistory
item = dict(is_ok=hist.is_ok,
operate_type=hist.operate_type,
notify=hist.notify,
trigger_id=hist.trigger_id,
trigger_name=hist.trigger_name,
webhook=hist.webhook,
created_at=hist.created_at.strftime('%Y-%m-%d %H:%M:%S'),
record_id=hist.record_id,
hid=hist.id
)
if i.CITypeTrigger.id not in id2trigger:
id2trigger[i.CITypeTrigger.id] = i.CITypeTrigger.to_dict()
result.append(item)
return dict(items=result, id2trigger=id2trigger)
@staticmethod
def add(operate_type, record_id, ci_id, trigger_id, trigger_name, is_ok=False, notify=None, webhook=None):
CITriggerHistory.create(operate_type=operate_type,
record_id=record_id,
ci_id=ci_id,
trigger_id=trigger_id,
trigger_name=trigger_name,
is_ok=is_ok,
notify=notify,
webhook=webhook)

View File

@@ -116,7 +116,7 @@ class PreferenceManager(object):
for i in result:
if i["is_choice"]:
i.update(dict(choice_value=AttributeManager.get_choice_values(
i["id"], i["value_type"], i["choice_web_hook"], i.get("choice_other"))))
i["id"], i["value_type"], i["choice_web_hook"])))
return is_subscribed, result

View File

@@ -23,7 +23,6 @@ class ErrFormat(CommonErrFormat):
cannot_edit_attribute = "您没有权限修改该属性!"
cannot_delete_attribute = "目前只允许 属性创建人、管理员 删除属性!"
attribute_name_cannot_be_builtin = "属性字段名不能是内置字段: id, _id, ci_id, type, _type, ci_type"
attribute_choice_other_invalid = "预定义值: 其他模型请求参数不合法!"
ci_not_found = "CI {} 不存在"
unique_constraint = "多属性联合唯一校验不通过: {}"
@@ -95,6 +94,3 @@ class ErrFormat(CommonErrFormat):
ci_filter_perm_cannot_or_query = "CI过滤授权 暂时不支持 或 查询"
ci_filter_perm_attr_no_permission = "您没有属性 {} 的操作权限!"
ci_filter_perm_ci_no_permission = "您没有该CI的操作权限!"
password_save_failed = "保存密码失败: {}"
password_load_failed = "获取密码失败: {}"

View File

@@ -7,7 +7,6 @@ QUERY_CIS_BY_VALUE_TABLE = """
attr.alias AS attr_alias,
attr.value_type,
attr.is_list,
attr.is_password,
c_cis.type_id,
{0}.ci_id,
{0}.attr_id,
@@ -27,8 +26,7 @@ QUERY_CIS_BY_IDS = """
A.attr_alias,
A.value,
A.value_type,
A.is_list,
A.is_password
A.is_list
FROM
({1}) AS A {0}
ORDER BY A.ci_id;
@@ -45,7 +43,7 @@ FACET_QUERY1 = """
FACET_QUERY = """
SELECT {0}.value,
count(distinct {0}.ci_id)
count({0}.ci_id)
FROM {0}
INNER JOIN ({1}) AS F ON F.ci_id={0}.ci_id
WHERE {0}.attr_id={2:d}

View File

@@ -1,4 +1,4 @@
# -*- coding:utf-8 -*-
# -*- coding:utf-8 -*-
from __future__ import unicode_literals
@@ -9,7 +9,6 @@ import time
from flask import current_app
from flask_login import current_user
from jinja2 import Template
from sqlalchemy import text
from api.extensions import db
from api.lib.cmdb.cache import AttributeCache
@@ -29,7 +28,6 @@ from api.lib.cmdb.search.ci.db.query_sql import QUERY_CI_BY_NO_ATTR
from api.lib.cmdb.search.ci.db.query_sql import QUERY_CI_BY_TYPE
from api.lib.cmdb.search.ci.db.query_sql import QUERY_UNION_CI_ATTRIBUTE_IS_NULL
from api.lib.cmdb.utils import TableMap
from api.lib.cmdb.utils import ValueTypeMap
from api.lib.perm.acl.acl import ACLManager
from api.lib.perm.acl.acl import is_app_admin
from api.lib.utils import handle_arg_list
@@ -143,10 +141,6 @@ class Search(object):
@staticmethod
def _in_query_handler(attr, v, is_not):
new_v = v[1:-1].split(";")
if attr.value_type == ValueTypeEnum.DATE:
new_v = ["{} 00:00:00".format(i) for i in new_v if len(i) == 10]
table_name = TableMap(attr=attr).table_name
in_query = " OR {0}.value ".format(table_name).join(['{0} "{1}"'.format(
"NOT LIKE" if is_not else "LIKE",
@@ -157,11 +151,6 @@ class Search(object):
@staticmethod
def _range_query_handler(attr, v, is_not):
start, end = [x.strip() for x in v[1:-1].split("_TO_")]
if attr.value_type == ValueTypeEnum.DATE:
start = "{} 00:00:00".format(start) if len(start) == 10 else start
end = "{} 00:00:00".format(end) if len(end) == 10 else end
table_name = TableMap(attr=attr).table_name
range_query = "{0} '{1}' AND '{2}'".format(
"NOT BETWEEN" if is_not else "BETWEEN",
@@ -173,14 +162,8 @@ class Search(object):
def _comparison_query_handler(attr, v):
table_name = TableMap(attr=attr).table_name
if v.startswith(">=") or v.startswith("<="):
if attr.value_type == ValueTypeEnum.DATE and len(v[2:]) == 10:
v = "{} 00:00:00".format(v)
comparison_query = "{0} '{1}'".format(v[:2], v[2:].replace("*", "%"))
else:
if attr.value_type == ValueTypeEnum.DATE and len(v[1:]) == 10:
v = "{} 00:00:00".format(v)
comparison_query = "{0} '{1}'".format(v[0], v[1:].replace("*", "%"))
_query_sql = QUERY_CI_BY_ATTR_NAME.format(table_name, attr.id, comparison_query)
return _query_sql
@@ -256,7 +239,7 @@ class Search(object):
attr_id = attr.id
table_name = TableMap(attr=attr).table_name
_v_query_sql = """SELECT {0}.ci_id, {1}.value
_v_query_sql = """SELECT {0}.ci_id, {1}.value
FROM ({2}) AS {0} INNER JOIN {1} ON {1}.ci_id = {0}.ci_id
WHERE {1}.attr_id = {3}""".format("ALIAS", table_name, query_sql, attr_id)
new_table = _v_query_sql
@@ -302,7 +285,7 @@ class Search(object):
query_sql = "SELECT * FROM ({0}) as {1} UNION ALL ({2})".format(query_sql, alias, _query_sql)
elif operator == "~":
query_sql = """SELECT * FROM ({0}) as {1} LEFT JOIN ({2}) as {3} USING(ci_id)
query_sql = """SELECT * FROM ({0}) as {1} LEFT JOIN ({2}) as {3} USING(ci_id)
WHERE {3}.ci_id is NULL""".format(query_sql, alias, _query_sql, alias + "A")
return query_sql
@@ -312,8 +295,8 @@ class Search(object):
start = time.time()
execute = db.session.execute
# current_app.logger.debug(v_query_sql)
res = execute(text(v_query_sql)).fetchall()
current_app.logger.debug(v_query_sql)
res = execute(v_query_sql).fetchall()
end_time = time.time()
current_app.logger.debug("query ci ids time is: {0}".format(end_time - start))
@@ -408,9 +391,6 @@ class Search(object):
is_not = True if operator == "|~" else False
if field_type == ValueTypeEnum.DATE and len(v) == 10:
v = "{} 00:00:00".format(v)
# in query
if v.startswith("(") and v.endswith(")"):
_query_sql = self._in_query_handler(attr, v, is_not)
@@ -526,15 +506,15 @@ class Search(object):
if k:
table_name = TableMap(attr=attr).table_name
query_sql = FACET_QUERY.format(table_name, self.query_sql, attr.id)
result = db.session.execute(text(query_sql)).fetchall()
# current_app.logger.debug(query_sql)
result = db.session.execute(query_sql).fetchall()
facet[k] = result
facet_result = dict()
for k, v in facet.items():
if not k.startswith('_'):
attr = AttributeCache.get(k)
a = getattr(attr, self.ret_key)
facet_result[a] = [(ValueTypeMap.serialize[attr.value_type](f[0]), f[1], a) for f in v]
a = getattr(AttributeCache.get(k), self.ret_key)
facet_result[a] = [(f[0], f[1], a) for f in v]
return facet_result

View File

@@ -35,7 +35,7 @@ class Search(object):
self.sort = sort or ("ci_id" if current_app.config.get("USE_ES") else None)
self.root_id = root_id
self.level = level or 0
self.level = level
self.reverse = reverse
def _get_ids(self):
@@ -104,22 +104,16 @@ class Search(object):
ci_ids=merge_ids).search()
def statistics(self, type_ids):
self.level = int(self.level)
_tmp = []
ids = [self.root_id] if not isinstance(self.root_id, list) else self.root_id
for lv in range(0, self.level):
if not lv:
if type_ids and lv == self.level - 1:
_tmp = list(map(lambda x: [i for i in x if i[1] in type_ids],
(map(lambda x: list(json.loads(x).items()),
[i or '{}' for i in rd.get(ids, REDIS_PREFIX_CI_RELATION) or []]))))
else:
_tmp = list(map(lambda x: list(json.loads(x).items()),
[i or '{}' for i in rd.get(ids, REDIS_PREFIX_CI_RELATION) or []]))
for l in range(0, int(self.level)):
if not l:
_tmp = list(map(lambda x: list(json.loads(x).items()),
[i or '{}' for i in rd.get(ids, REDIS_PREFIX_CI_RELATION) or []]))
else:
for idx, item in enumerate(_tmp):
if item:
if type_ids and lv == self.level - 1:
if type_ids and l == self.level - 1:
__tmp = list(
map(lambda x: [(_id, type_id) for _id, type_id in json.loads(x).items()
if type_id in type_ids],

View File

@@ -12,7 +12,7 @@ import api.models.cmdb as model
from api.lib.cmdb.cache import AttributeCache
from api.lib.cmdb.const import ValueTypeEnum
TIME_RE = re.compile(r"^20|21|22|23|[0-1]\d:[0-5]\d:[0-5]\d$")
TIME_RE = re.compile(r"^(20|21|22|23|[0-1]\d):[0-5]\d:[0-5]\d$")
def string2int(x):
@@ -21,7 +21,7 @@ def string2int(x):
def str2datetime(x):
try:
return datetime.datetime.strptime(x, "%Y-%m-%d").date()
return datetime.datetime.strptime(x, "%Y-%m-%d")
except ValueError:
pass
@@ -44,8 +44,8 @@ class ValueTypeMap(object):
ValueTypeEnum.FLOAT: float,
ValueTypeEnum.TEXT: lambda x: x if isinstance(x, six.string_types) else str(x),
ValueTypeEnum.TIME: lambda x: x if isinstance(x, six.string_types) else str(x),
ValueTypeEnum.DATE: lambda x: x.strftime("%Y-%m-%d") if not isinstance(x, six.string_types) else x,
ValueTypeEnum.DATETIME: lambda x: x.strftime("%Y-%m-%d %H:%M:%S") if not isinstance(x, six.string_types) else x,
ValueTypeEnum.DATE: lambda x: x.strftime("%Y-%m-%d"),
ValueTypeEnum.DATETIME: lambda x: x.strftime("%Y-%m-%d %H:%M:%S"),
ValueTypeEnum.JSON: lambda x: json.loads(x) if isinstance(x, six.string_types) and x else x,
}
@@ -64,8 +64,6 @@ class ValueTypeMap(object):
ValueTypeEnum.FLOAT: model.FloatChoice,
ValueTypeEnum.TEXT: model.TextChoice,
ValueTypeEnum.TIME: model.TextChoice,
ValueTypeEnum.DATE: model.TextChoice,
ValueTypeEnum.DATETIME: model.TextChoice,
}
table = {
@@ -99,7 +97,7 @@ class ValueTypeMap(object):
ValueTypeEnum.DATE: 'text',
ValueTypeEnum.TIME: 'text',
ValueTypeEnum.FLOAT: 'float',
ValueTypeEnum.JSON: 'object',
ValueTypeEnum.JSON: 'object'
}
@@ -112,9 +110,7 @@ class TableMap(object):
@property
def table(self):
attr = AttributeCache.get(self.attr_name) if not self.attr else self.attr
if attr.is_password or attr.is_link:
self.is_index = False
elif attr.value_type not in {ValueTypeEnum.TEXT, ValueTypeEnum.JSON}:
if attr.value_type != ValueTypeEnum.TEXT and attr.value_type != ValueTypeEnum.JSON:
self.is_index = True
elif self.is_index is None:
self.is_index = attr.is_index
@@ -126,9 +122,7 @@ class TableMap(object):
@property
def table_name(self):
attr = AttributeCache.get(self.attr_name) if not self.attr else self.attr
if attr.is_password or attr.is_link:
self.is_index = False
elif attr.value_type not in {ValueTypeEnum.TEXT, ValueTypeEnum.JSON}:
if attr.value_type != ValueTypeEnum.TEXT and attr.value_type != ValueTypeEnum.JSON:
self.is_index = True
elif self.is_index is None:
self.is_index = attr.is_index

View File

@@ -18,6 +18,7 @@ from api.extensions import db
from api.lib.cmdb.attribute import AttributeManager
from api.lib.cmdb.cache import AttributeCache
from api.lib.cmdb.cache import CITypeAttributeCache
from api.lib.cmdb.const import ExistPolicy
from api.lib.cmdb.const import OperateType
from api.lib.cmdb.const import ValueTypeEnum
from api.lib.cmdb.history import AttributeHistoryManger
@@ -66,10 +67,9 @@ class AttributeValueManager(object):
use_master=use_master,
to_dict=False)
field_name = getattr(attr, ret_key)
if attr.is_list:
res[field_name] = [ValueTypeMap.serialize[attr.value_type](i.value) for i in rs]
elif attr.is_password and rs:
res[field_name] = '******' if rs[0].value else ''
else:
res[field_name] = ValueTypeMap.serialize[attr.value_type](rs[0].value) if rs else None
@@ -93,7 +93,7 @@ class AttributeValueManager(object):
@staticmethod
def _check_is_choice(attr, value_type, value):
choice_values = AttributeManager.get_choice_values(attr.id, value_type, attr.choice_web_hook, attr.choice_other)
choice_values = AttributeManager.get_choice_values(attr.id, value_type, attr.choice_web_hook)
if str(value) not in list(map(str, [i[0] for i in choice_values])):
return abort(400, ErrFormat.not_in_choice_values.format(value))
@@ -132,14 +132,14 @@ class AttributeValueManager(object):
return AttributeHistoryManger.add(record_id, ci_id, [(attr_id, operate_type, old, new)], type_id)
@staticmethod
def write_change2(changed, record_id=None):
def _write_change2(changed):
record_id = None
for ci_id, attr_id, operate_type, old, new, type_id in changed:
record_id = AttributeHistoryManger.add(record_id, ci_id, [(attr_id, operate_type, old, new)], type_id,
commit=False, flush=False)
try:
db.session.commit()
except Exception as e:
db.session.rollback()
current_app.logger.error("write change failed: {}".format(str(e)))
return record_id
@@ -235,7 +235,7 @@ class AttributeValueManager(object):
return key2attr
def create_or_update_attr_value(self, ci, ci_dict, key2attr):
def create_or_update_attr_value2(self, ci, ci_dict, key2attr):
"""
add or update attribute value, then write history
:param ci: instance object
@@ -284,9 +284,69 @@ class AttributeValueManager(object):
except Exception as e:
db.session.rollback()
current_app.logger.warning(str(e))
return abort(400, ErrFormat.attribute_value_unknown_error.format(e.args[0]))
return abort(400, ErrFormat.attribute_value_unknown_error.format(str(e)))
return self.write_change2(changed)
return self._write_change2(changed)
def create_or_update_attr_value(self, key, value, ci, _no_attribute_policy=ExistPolicy.IGNORE, record_id=None):
"""
add or update attribute value, then write history
:param key: id, name or alias
:param value:
:param ci: instance object
:param _no_attribute_policy: ignore or reject
:param record_id: op record
:return:
"""
attr = self._get_attr(key)
if attr is None:
if _no_attribute_policy == ExistPolicy.IGNORE:
return
if _no_attribute_policy == ExistPolicy.REJECT:
return abort(400, ErrFormat.attribute_not_found.format(key))
value_table = TableMap(attr=attr).table
try:
if attr.is_list:
value_list = [self._validate(attr, i, value_table, ci) for i in handle_arg_list(value)]
if not value_list:
self._check_is_required(ci.type_id, attr, '')
existed_attrs = value_table.get_by(attr_id=attr.id, ci_id=ci.id, to_dict=False)
existed_values = [i.value for i in existed_attrs]
added = set(value_list) - set(existed_values)
deleted = set(existed_values) - set(value_list)
for v in added:
value_table.create(ci_id=ci.id, attr_id=attr.id, value=v)
record_id = self._write_change(ci.id, attr.id, OperateType.ADD, None, v, record_id, ci.type_id)
for v in deleted:
existed_attr = existed_attrs[existed_values.index(v)]
existed_attr.delete()
record_id = self._write_change(ci.id, attr.id, OperateType.DELETE, v, None, record_id, ci.type_id)
else:
value = self._validate(attr, value, value_table, ci)
existed_attr = value_table.get_by(attr_id=attr.id, ci_id=ci.id, first=True, to_dict=False)
existed_value = existed_attr and existed_attr.value
if existed_value is None and value is not None:
value_table.create(ci_id=ci.id, attr_id=attr.id, value=value)
record_id = self._write_change(ci.id, attr.id, OperateType.ADD, None, value, record_id, ci.type_id)
else:
if existed_value != value:
if value is None:
existed_attr.delete()
else:
existed_attr.update(value=value)
record_id = self._write_change(ci.id, attr.id, OperateType.UPDATE,
existed_value, value, record_id, ci.type_id)
return record_id
except Exception as e:
current_app.logger.warning(str(e))
return abort(400, ErrFormat.attribute_value_invalid2.format("{}({})".format(attr.alias, attr.name), value))
@staticmethod
def delete_attr_value(attr_id, ci_id):

View File

@@ -1,13 +1,12 @@
# -*- coding:utf-8 -*-
from flask import abort
from flask import current_app
from api.lib.common_setting.resp_format import ErrFormat
from api.lib.perm.acl.app import AppCRUD
from api.lib.perm.acl.cache import RoleCache, AppCache
from api.lib.perm.acl.permission import PermissionCRUD
from api.lib.perm.acl.resource import ResourceTypeCRUD, ResourceCRUD
from api.lib.perm.acl.role import RoleCRUD, RoleRelationCRUD
from api.lib.perm.acl.user import UserCRUD
from api.lib.perm.acl.resource import ResourceTypeCRUD, ResourceCRUD
class ACLManager(object):
@@ -80,22 +79,20 @@ class ACLManager(object):
return role.to_dict()
@staticmethod
def delete_role(_id):
def delete_role(_id, payload):
RoleCRUD.delete_role(_id)
return dict(rid=_id)
def get_user_info(self, username):
from api.lib.perm.acl.acl import ACLManager as ACL
user_info = ACL().get_user_info(username, self.app_name)
result = dict(
name=user_info.get('nickname') or username,
username=user_info.get('username') or username,
email=user_info.get('email'),
uid=user_info.get('uid'),
rid=user_info.get('rid'),
role=dict(permissions=user_info.get('parents')),
avatar=user_info.get('avatar')
)
result = dict(name=user_info.get('nickname') or username,
username=user_info.get('username') or username,
email=user_info.get('email'),
uid=user_info.get('uid'),
rid=user_info.get('rid'),
role=dict(permissions=user_info.get('parents')),
avatar=user_info.get('avatar'))
return result
@@ -112,32 +109,8 @@ class ACLManager(object):
id2perms=id2perms
)
def create_resources_type(self, payload):
payload['app_id'] = self.validate_app().id
rt = ResourceTypeCRUD.add(**payload)
return rt.to_dict()
def update_resources_type(self, _id, payload):
rt = ResourceTypeCRUD.update(_id, **payload)
return rt.to_dict()
def create_resource(self, payload):
payload['app_id'] = self.validate_app().id
resource = ResourceCRUD.add(**payload)
return resource.to_dict()
def get_resource_by_type(self, q, u, rt_id, page=1, page_size=999999):
numfound, res = ResourceCRUD.search(q, u, self.validate_app().id, rt_id, page, page_size)
return res
def grant_resource(self, rid, resource_id, perms):
PermissionCRUD.grant(rid, perms, resource_id=resource_id, group_id=None)
@staticmethod
def create_app(payload):
rt = AppCRUD.add(**payload)
return rt.to_dict()

View File

@@ -1,5 +1,5 @@
# -*- coding:utf-8 -*-
from api.extensions import cache
from api.models.common_setting import CompanyInfo
@@ -11,34 +11,14 @@ class CompanyInfoCRUD(object):
@staticmethod
def create(**kwargs):
res = CompanyInfo.create(**kwargs)
CompanyInfoCache.refresh(res.info)
return res
return CompanyInfo.create(**kwargs)
@staticmethod
def update(_id, **kwargs):
kwargs.pop('id', None)
existed = CompanyInfo.get_by_id(_id)
if not existed:
existed = CompanyInfoCRUD.create(**kwargs)
return CompanyInfoCRUD.create(**kwargs)
else:
existed = existed.update(**kwargs)
CompanyInfoCache.refresh(existed.info)
return existed
class CompanyInfoCache(object):
key = 'CompanyInfoCache::'
@classmethod
def get(cls):
info = cache.get(cls.key)
if not info:
res = CompanyInfo.get_by(first=True) or {}
info = res.get('info', {})
cache.set(cls.key, info)
return info
@classmethod
def refresh(cls, info):
cache.set(cls.key, info)
return existed

View File

@@ -12,10 +12,3 @@ class OperatorType(BaseEnum):
LESS_THAN = 6
IS_EMPTY = 7
IS_NOT_EMPTY = 8
BotNameMap = {
'wechatApp': 'wechatBot',
'feishuApp': 'feishuBot',
'dingdingApp': 'dingdingBot',
}

View File

@@ -1,6 +1,6 @@
# -*- coding:utf-8 -*-
from flask import abort, current_app
from flask import abort
from treelib import Tree
from wtforms import Form
from wtforms import IntegerField
@@ -9,7 +9,6 @@ from wtforms import validators
from api.extensions import db
from api.lib.common_setting.resp_format import ErrFormat
from api.lib.common_setting.acl import ACLManager
from api.lib.perm.acl.role import RoleCRUD
from api.models.common_setting import Department, Employee
@@ -153,10 +152,6 @@ class DepartmentForm(Form):
class DepartmentCRUD(object):
@staticmethod
def get_department_by_id(d_id, to_dict=True):
return Department.get_by(first=True, department_id=d_id, to_dict=to_dict)
@staticmethod
def add(**kwargs):
DepartmentCRUD.check_department_name_unique(kwargs['department_name'])
@@ -191,11 +186,10 @@ class DepartmentCRUD(object):
filter(lambda d: d['department_id'] == department_parent_id, allow_p_d_id_list))
if len(target) == 0:
try:
dep = Department.get_by(
d = Department.get_by(
first=True, to_dict=False, department_id=department_parent_id)
name = dep.department_name if dep else ErrFormat.department_id_not_found.format(department_parent_id)
name = d.department_name if d else ErrFormat.department_id_not_found.format(department_parent_id)
except Exception as e:
current_app.logger.error(str(e))
name = ErrFormat.department_id_not_found.format(department_parent_id)
abort(400, ErrFormat.cannot_to_be_parent_department.format(name))
@@ -259,7 +253,7 @@ class DepartmentCRUD(object):
try:
RoleCRUD.delete_role(existed.acl_rid)
except Exception as e:
current_app.logger.error(str(e))
pass
return existed.soft_delete()
@@ -274,7 +268,7 @@ class DepartmentCRUD(object):
try:
tree.remove_subtree(department_id)
except Exception as e:
current_app.logger.error(str(e))
pass
[allow_d_id_list.append({'department_id': int(n.identifier), 'department_name': n.tag}) for n in
tree.all_nodes()]
@@ -396,125 +390,6 @@ class DepartmentCRUD(object):
[id_list.append(int(n.identifier))
for n in tmp_tree.all_nodes()]
except Exception as e:
current_app.logger.error(str(e))
pass
return id_list
class EditDepartmentInACL(object):
@staticmethod
def add_department_to_acl(department_id, op_uid):
db_department = DepartmentCRUD.get_department_by_id(department_id, to_dict=False)
if not db_department:
return
from api.models.acl import Role
role = Role.get_by(first=True, name=db_department.department_name, app_id=None)
acl = ACLManager('acl', str(op_uid))
if role is None:
payload = {
'app_id': 'acl',
'name': db_department.department_name,
}
role = acl.create_role(payload)
acl_rid = role.get('id') if role else 0
db_department.update(
acl_rid=acl_rid
)
info = f"add_department_to_acl, acl_rid: {acl_rid}"
current_app.logger.info(info)
return info
@staticmethod
def delete_department_from_acl(department_rids, op_uid):
acl = ACLManager('acl', str(op_uid))
result = []
for rid in department_rids:
try:
acl.delete_role(rid)
except Exception as e:
result.append(f"delete_department_in_acl, rid: {rid}, error: {e}")
continue
return result
@staticmethod
def edit_department_name_in_acl(d_rid: int, d_name: str, op_uid: int):
acl = ACLManager('acl', str(op_uid))
payload = {
'name': d_name
}
try:
acl.edit_role(d_rid, payload)
except Exception as e:
return f"edit_department_name_in_acl, rid: {d_rid}, error: {e}"
return f"edit_department_name_in_acl, rid: {d_rid}, success"
@staticmethod
def edit_employee_department_in_acl(e_list: list, new_d_id: int, op_uid: int):
result = []
new_department = DepartmentCRUD.get_department_by_id(new_d_id, False)
if not new_department:
result.append(f"{new_d_id} new_department is None")
return result
from api.models.acl import Role
new_role = Role.get_by(first=True, name=new_department.department_name, app_id=None)
new_d_rid_in_acl = new_role.get('id') if new_role else 0
if new_d_rid_in_acl == 0:
return
if new_d_rid_in_acl != new_department.acl_rid:
new_department.update(
acl_rid=new_d_rid_in_acl
)
new_department_acl_rid = new_department.acl_rid if new_d_rid_in_acl == new_department.acl_rid else \
new_d_rid_in_acl
acl = ACLManager('acl', str(op_uid))
for employee in e_list:
old_department = DepartmentCRUD.get_department_by_id(employee.get('department_id'), False)
if not old_department:
continue
employee_acl_rid = employee.get('e_acl_rid')
if employee_acl_rid == 0:
result.append(f"employee_acl_rid == 0")
continue
old_role = Role.get_by(first=True, name=old_department.department_name, app_id=None)
old_d_rid_in_acl = old_role.get('id') if old_role else 0
if old_d_rid_in_acl == 0:
return
if old_d_rid_in_acl != old_department.acl_rid:
old_department.update(
acl_rid=old_d_rid_in_acl
)
d_acl_rid = old_department.acl_rid if old_d_rid_in_acl == old_department.acl_rid else old_d_rid_in_acl
payload = {
'app_id': 'acl',
'parent_id': d_acl_rid,
}
try:
acl.remove_user_from_role(employee_acl_rid, payload)
except Exception as e:
result.append(
f"remove_user_from_role employee_acl_rid: {employee_acl_rid}, parent_id: {d_acl_rid}, err: {e}")
payload = {
'app_id': 'acl',
'child_ids': [employee_acl_rid],
}
try:
acl.add_user_to_role(new_department_acl_rid, payload)
except Exception as e:
result.append(
f"add_user_to_role employee_acl_rid: {employee_acl_rid}, parent_id: {d_acl_rid}, err: {e}")
return result

View File

@@ -1,9 +1,8 @@
# -*- coding:utf-8 -*-
import copy
import traceback
from datetime import datetime
import requests
from flask import abort
from flask_login import current_user
from sqlalchemy import or_, literal_column, func, not_, and_
@@ -121,19 +120,6 @@ class EmployeeCRUD(object):
employee = CreateEmployee().create_single(**data)
return employee.to_dict()
@staticmethod
def add_employee_from_acl_created(**kwargs):
try:
kwargs['acl_uid'] = kwargs.pop('uid')
kwargs['acl_rid'] = kwargs.pop('rid')
kwargs['department_id'] = 0
Employee.create(
**kwargs
)
except Exception as e:
abort(400, str(e))
@staticmethod
def add(**kwargs):
try:
@@ -178,7 +164,7 @@ class EmployeeCRUD(object):
def edit_employee_by_uid(_uid, **kwargs):
existed = EmployeeCRUD.get_employee_by_uid(_uid)
try:
edit_acl_user(_uid, **kwargs)
user = edit_acl_user(_uid, **kwargs)
for column in employee_pop_columns:
if kwargs.get(column):
@@ -190,9 +176,9 @@ class EmployeeCRUD(object):
@staticmethod
def change_password_by_uid(_uid, password):
EmployeeCRUD.get_employee_by_uid(_uid)
existed = EmployeeCRUD.get_employee_by_uid(_uid)
try:
edit_acl_user(_uid, password=password)
user = edit_acl_user(_uid, password=password)
except Exception as e:
return abort(400, str(e))
@@ -359,11 +345,9 @@ class EmployeeCRUD(object):
if value and column == "last_login":
try:
return datetime.strptime(value, "%Y-%m-%d %H:%M:%S")
value = datetime.strptime(value, "%Y-%m-%d %H:%M:%S")
except Exception as e:
err = f"{ErrFormat.datetime_format_error.format(column)}: {str(e)}"
abort(400, err)
return value
abort(400, ErrFormat.datetime_format_error.format(column))
@staticmethod
def get_attr_by_column(column):
@@ -384,7 +368,7 @@ class EmployeeCRUD(object):
relation = condition.get("relation", None)
value = condition.get("value", None)
value = EmployeeCRUD.check_condition(column, operator, value, relation)
EmployeeCRUD.check_condition(column, operator, value, relation)
a, o = EmployeeCRUD.get_expr_by_condition(
column, operator, value, relation)
and_list += a
@@ -490,202 +474,6 @@ class EmployeeCRUD(object):
return [r.to_dict() for r in results]
@staticmethod
def remove_bind_notice_by_uid(_platform, _uid):
existed = EmployeeCRUD.get_employee_by_uid(_uid)
employee_data = existed.to_dict()
notice_info = employee_data.get('notice_info', {})
notice_info = copy.deepcopy(notice_info) if notice_info else {}
notice_info[_platform] = ''
existed.update(
notice_info=notice_info
)
return ErrFormat.notice_remove_bind_success
@staticmethod
def bind_notice_by_uid(_platform, _uid):
existed = EmployeeCRUD.get_employee_by_uid(_uid)
mobile = existed.mobile
if not mobile or len(mobile) == 0:
abort(400, ErrFormat.notice_bind_err_with_empty_mobile)
from api.lib.common_setting.notice_config import NoticeConfigCRUD
messenger = NoticeConfigCRUD.get_messenger_url()
if not messenger or len(messenger) == 0:
abort(400, ErrFormat.notice_please_config_messenger_first)
url = f"{messenger}/v1/uid/getbyphone"
try:
payload = dict(
phone=mobile,
sender=_platform
)
res = requests.post(url, json=payload)
result = res.json()
if res.status_code != 200:
raise Exception(result.get('msg', ''))
target_id = result.get('uid', '')
employee_data = existed.to_dict()
notice_info = employee_data.get('notice_info', {})
notice_info = copy.deepcopy(notice_info) if notice_info else {}
notice_info[_platform] = '' if not target_id else target_id
existed.update(
notice_info=notice_info
)
return ErrFormat.notice_bind_success
except Exception as e:
return abort(400, ErrFormat.notice_bind_failed.format(str(e)))
@staticmethod
def get_employee_notice_by_ids(employee_ids):
criterion = [
Employee.employee_id.in_(employee_ids),
Employee.deleted == 0,
]
direct_columns = ['email', 'mobile']
employees = Employee.query.filter(
*criterion
).all()
results = []
for employee in employees:
d = employee.to_dict()
tmp = dict(
employee_id=employee.employee_id,
)
for column in direct_columns:
tmp[column] = d.get(column, '')
notice_info = d.get('notice_info', {})
tmp.update(**notice_info)
results.append(tmp)
return results
@staticmethod
def import_employee(employee_list):
res = CreateEmployee().batch_create(employee_list)
return res
@staticmethod
def batch_edit_employee_department(employee_id_list, column_value):
err_list = []
employee_list = []
for _id in employee_id_list:
try:
existed = EmployeeCRUD.get_employee_by_id(_id)
employee = dict(
e_acl_rid=existed.acl_rid,
department_id=existed.department_id
)
employee_list.append(employee)
existed.update(department_id=column_value)
except Exception as e:
err_list.append({
'employee_id': _id,
'err': str(e),
})
from api.lib.common_setting.department import EditDepartmentInACL
EditDepartmentInACL.edit_employee_department_in_acl(
employee_list, column_value, current_user.uid
)
return err_list
@staticmethod
def batch_edit_password_or_block_column(column_name, employee_id_list, column_value, is_acl=False):
if column_name == 'block':
err_list = []
success_list = []
for _id in employee_id_list:
try:
employee = EmployeeCRUD.edit_employee_block_column(
_id, is_acl, **{column_name: column_value})
success_list.append(employee)
except Exception as e:
err_list.append({
'employee_id': _id,
'err': str(e),
})
return err_list
else:
return EmployeeCRUD.batch_edit_column(column_name, employee_id_list, column_value, is_acl)
@staticmethod
def batch_edit_column(column_name, employee_id_list, column_value, is_acl=False):
err_list = []
for _id in employee_id_list:
try:
EmployeeCRUD.edit_employee_single_column(
_id, is_acl, **{column_name: column_value})
except Exception as e:
err_list.append({
'employee_id': _id,
'err': str(e),
})
return err_list
@staticmethod
def edit_employee_single_column(_id, is_acl=False, **kwargs):
existed = EmployeeCRUD.get_employee_by_id(_id)
if 'direct_supervisor_id' in kwargs.keys():
if kwargs['direct_supervisor_id'] == existed.direct_supervisor_id:
raise Exception(ErrFormat.direct_supervisor_is_not_self)
if is_acl:
return edit_acl_user(existed.acl_uid, **kwargs)
try:
for column in employee_pop_columns:
if kwargs.get(column):
kwargs.pop(column)
return existed.update(**kwargs)
except Exception as e:
return abort(400, str(e))
@staticmethod
def edit_employee_block_column(_id, is_acl=False, **kwargs):
existed = EmployeeCRUD.get_employee_by_id(_id)
value = get_block_value(kwargs.get('block'))
if value is True:
check_department_director_id_or_direct_supervisor_id(_id)
value = 1
else:
value = 0
if is_acl:
kwargs['block'] = value
edit_acl_user(existed.acl_uid, **kwargs)
existed.update(block=value)
data = existed.to_dict()
return data
@staticmethod
def batch_employee(column_name, column_value, employee_id_list):
if column_value is None:
abort(400, ErrFormat.value_is_required)
if column_name in ['password', 'block']:
return EmployeeCRUD.batch_edit_password_or_block_column(column_name, employee_id_list, column_value, True)
elif column_name in ['department_id']:
return EmployeeCRUD.batch_edit_employee_department(employee_id_list, column_value)
elif column_name in [
'direct_supervisor_id', 'position_name'
]:
return EmployeeCRUD.batch_edit_column(column_name, employee_id_list, column_value, False)
else:
abort(400, ErrFormat.column_name_not_support)
def get_user_map(key='uid', acl=None):
"""
@@ -762,8 +550,7 @@ class CreateEmployee(object):
**kwargs
)
@staticmethod
def get_department_by_name(d_name):
def get_department_by_name(self, d_name):
return Department.get_by(first=True, department_name=d_name)
def get_end_department_id(self, department_name_list, department_name_map):

View File

@@ -1,165 +0,0 @@
import requests
from api.lib.common_setting.const import BotNameMap
from api.lib.common_setting.resp_format import ErrFormat
from api.models.common_setting import CompanyInfo, NoticeConfig
from wtforms import Form
from wtforms import StringField
from wtforms import validators
from flask import abort, current_app
class NoticeConfigCRUD(object):
@staticmethod
def add_notice_config(**kwargs):
platform = kwargs.get('platform')
NoticeConfigCRUD.check_platform(platform)
info = kwargs.get('info', {})
if 'name' not in info:
info['name'] = platform
kwargs['info'] = info
try:
NoticeConfigCRUD.update_messenger_config(**info)
res = NoticeConfig.create(
**kwargs
)
return res
except Exception as e:
return abort(400, str(e))
@staticmethod
def check_platform(platform):
NoticeConfig.get_by(first=True, to_dict=False, platform=platform) and \
abort(400, ErrFormat.notice_platform_existed.format(platform))
@staticmethod
def edit_notice_config(_id, **kwargs):
existed = NoticeConfigCRUD.get_notice_config_by_id(_id)
try:
info = kwargs.get('info', {})
if 'name' not in info:
info['name'] = existed.platform
kwargs['info'] = info
NoticeConfigCRUD.update_messenger_config(**info)
res = existed.update(**kwargs)
return res
except Exception as e:
return abort(400, str(e))
@staticmethod
def get_messenger_url():
from api.lib.common_setting.company_info import CompanyInfoCache
com_info = CompanyInfoCache.get()
if not com_info:
return
messenger = com_info.get('messenger', '')
if len(messenger) == 0:
return
if messenger[-1] == '/':
messenger = messenger[:-1]
return messenger
@staticmethod
def update_messenger_config(**kwargs):
try:
messenger = NoticeConfigCRUD.get_messenger_url()
if not messenger or len(messenger) == 0:
raise Exception(ErrFormat.notice_please_config_messenger_first)
url = f"{messenger}/v1/senders"
name = kwargs.get('name')
bot_list = kwargs.pop('bot', None)
for k, v in kwargs.items():
if isinstance(v, bool):
kwargs[k] = 'true' if v else 'false'
else:
kwargs[k] = str(v)
payload = {name: [kwargs]}
current_app.logger.info(f"update_messenger_config: {url}, {payload}")
res = requests.put(url, json=payload, timeout=2)
current_app.logger.info(f"update_messenger_config: {res.status_code}, {res.text}")
if not bot_list or len(bot_list) == 0:
return
bot_name = BotNameMap.get(name)
payload = {bot_name: bot_list}
current_app.logger.info(f"update_messenger_config: {url}, {payload}")
bot_res = requests.put(url, json=payload, timeout=2)
current_app.logger.info(f"update_messenger_config: {bot_res.status_code}, {bot_res.text}")
except Exception as e:
return abort(400, str(e))
@staticmethod
def get_notice_config_by_id(_id):
return NoticeConfig.get_by(first=True, to_dict=False, id=_id) or \
abort(400,
ErrFormat.notice_not_existed.format(_id))
@staticmethod
def get_all():
return NoticeConfig.get_by(to_dict=True)
@staticmethod
def test_send_email(receive_address, **kwargs):
messenger = NoticeConfigCRUD.get_messenger_url()
if not messenger or len(messenger) == 0:
abort(400, ErrFormat.notice_please_config_messenger_first)
url = f"{messenger}/v1/message"
recipient_email = receive_address
subject = 'Test Email'
body = 'This is a test email'
payload = {
"sender": 'email',
"msgtype": "text/plain",
"title": subject,
"content": body,
"tos": [recipient_email],
}
current_app.logger.info(f"test_send_email: {url}, {payload}")
response = requests.post(url, json=payload)
if response.status_code != 200:
abort(400, response.text)
return 1
@staticmethod
def get_app_bot():
result = []
for notice_app in NoticeConfig.get_by(to_dict=False):
if notice_app.platform in ['email']:
continue
info = notice_app.info
name = info.get('name', '')
if name not in BotNameMap:
continue
result.append(dict(
name=info.get('name', ''),
label=info.get('label', ''),
bot=info.get('bot', []),
))
return result
class NoticeConfigForm(Form):
platform = StringField(validators=[
validators.DataRequired(message="平台 不能为空"),
validators.Length(max=255),
])
info = StringField(validators=[
validators.DataRequired(message="信息 不能为空"),
validators.Length(max=255),
])
class NoticeConfigUpdateForm(Form):
info = StringField(validators=[
validators.DataRequired(message="信息 不能为空"),
validators.Length(max=255),
])

View File

@@ -53,13 +53,5 @@ class ErrFormat(CommonErrFormat):
username_is_required = "username不能为空"
email_is_required = "邮箱不能为空"
email_format_error = "邮箱格式错误"
email_send_timeout = "邮件发送超时"
common_data_not_found = "ID {} 找不到记录"
notice_platform_existed = "{} 已存在"
notice_not_existed = "{} 配置项不存在"
notice_please_config_messenger_first = "请先配置 messenger"
notice_bind_err_with_empty_mobile = "绑定失败,手机号为空"
notice_bind_failed = "绑定失败: {}"
notice_bind_success = "绑定成功"
notice_remove_bind_success = "解绑成功"

View File

@@ -10,18 +10,14 @@ from api.lib.exception import CommitException
class FormatMixin(object):
def to_dict(self):
res = dict()
for k in getattr(self, "__mapper__").c.keys():
if k in {'password', '_password', 'secret', '_secret'}:
continue
res = dict([(k, getattr(self, k) if not isinstance(
getattr(self, k), (datetime.datetime, datetime.date, datetime.time)) else str(
getattr(self, k))) for k in getattr(self, "__mapper__").c.keys()])
# FIXME: getattr(cls, "__table__").columns k.name
if k.startswith('_'):
k = k[1:]
if not isinstance(getattr(self, k), (datetime.datetime, datetime.date, datetime.time)):
res[k] = getattr(self, k)
else:
res[k] = str(getattr(self, k))
res.pop('password', None)
res.pop('_password', None)
res.pop('secret', None)
return res

View File

@@ -4,14 +4,8 @@
from functools import wraps
from flask import abort
from flask import current_app
from flask import request
from sqlalchemy.exc import InvalidRequestError
from sqlalchemy.exc import OperationalError
from sqlalchemy.exc import PendingRollbackError
from sqlalchemy.exc import StatementError
from api.extensions import db
from api.lib.resp_format import CommonErrFormat
@@ -76,43 +70,3 @@ def args_validate(model_cls, exclude_args=None):
return wrapper
return decorate
def reconnect_db(func):
@wraps(func)
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except (StatementError, OperationalError, InvalidRequestError) as e:
error_msg = str(e)
if 'Lost connection' in error_msg or 'reconnect until invalid transaction' in error_msg or \
'can be emitted within this transaction' in error_msg:
current_app.logger.info('[reconnect_db] lost connect rollback then retry')
db.session.rollback()
return func(*args, **kwargs)
else:
raise e
except Exception as e:
raise e
return wrapper
def _flush_db():
try:
db.session.commit()
except (StatementError, OperationalError, InvalidRequestError, PendingRollbackError):
db.session.rollback()
def flush_db(func):
@wraps(func)
def wrapper(*args, **kwargs):
_flush_db()
return func(*args, **kwargs)
return wrapper
def run_flush_db():
_flush_db()

View File

@@ -1,72 +0,0 @@
# -*- coding:utf-8 -*-
import json
import requests
import six
from flask import current_app
from jinja2 import Template
from markdownify import markdownify as md
from api.lib.common_setting.notice_config import NoticeConfigCRUD
from api.lib.mail import send_mail
def _request_messenger(subject, body, tos, sender, payload):
params = dict(sender=sender, title=subject,
tos=[to[sender] for to in tos if to.get(sender)])
if not params['tos']:
raise Exception("no receivers")
flat_tos = []
for i in params['tos']:
if i.strip():
to = Template(i).render(payload)
if isinstance(to, list):
flat_tos.extend(to)
elif isinstance(to, six.string_types):
flat_tos.append(to)
params['tos'] = flat_tos
if sender == "email":
params['msgtype'] = 'text/html'
params['content'] = body
else:
params['msgtype'] = 'markdown'
try:
content = md("{}\n{}".format(subject or '', body or ''))
except Exception as e:
current_app.logger.warning("html2markdown failed: {}".format(e))
content = "{}\n{}".format(subject or '', body or '')
params['content'] = json.dumps(dict(content=content))
url = current_app.config.get('MESSENGER_URL') or NoticeConfigCRUD.get_messenger_url()
if not url:
raise Exception("no messenger url")
if not url.endswith("message"):
url = "{}/v1/message".format(url)
resp = requests.post(url, json=params)
if resp.status_code != 200:
raise Exception(resp.text)
return resp.text
def notify_send(subject, body, methods, tos, payload=None):
payload = payload or {}
payload = {k: v or '' for k, v in payload.items()}
subject = Template(subject).render(payload)
body = Template(body).render(payload)
res = ''
for method in methods:
if method == "email" and not current_app.config.get('USE_MESSENGER', True):
send_mail(None, [Template(to.get('email')).render(payload) for to in tos], subject, body)
res += (_request_messenger(subject, body, tos, method, payload) + "\n")
return res

View File

@@ -4,7 +4,7 @@
import msgpack
from api.extensions import cache
from api.lib.decorator import flush_db
from api.extensions import db
from api.lib.utils import Lock
from api.models.acl import App
from api.models.acl import Permission
@@ -221,9 +221,9 @@ class RoleRelationCache(object):
return msgpack.loads(r_g, raw=False)
@classmethod
@flush_db
def rebuild(cls, rid, app_id):
cls.clean(rid, app_id)
db.session.remove()
cls.get_parent_ids(rid, app_id)
cls.get_child_ids(rid, app_id)
@@ -235,9 +235,9 @@ class RoleRelationCache(object):
cls.get_resources2(rid, app_id)
@classmethod
@flush_db
def rebuild2(cls, rid, app_id):
cache.delete(cls.PREFIX_RESOURCES2.format(rid, app_id))
db.session.remove()
cls.get_resources2(rid, app_id)
@classmethod

View File

@@ -260,8 +260,7 @@ class ResourceCRUD(object):
numfound = query.count()
res = [i.to_dict() for i in query.offset((page - 1) * page_size).limit(page_size)]
for i in res:
user = UserCache.get(i['uid']) if i['uid'] else ''
i['user'] = user and user.nickname
i['user'] = UserCache.get(i['uid']).nickname if i['uid'] else ''
return numfound, res
@@ -276,6 +275,7 @@ class ResourceCRUD(object):
from api.tasks.acl import apply_trigger
triggers = TriggerCRUD.match_triggers(app_id, r.name, r.resource_type_id, uid)
current_app.logger.info(triggers)
for trigger in triggers:
# auto trigger should be no uid
apply_trigger.apply_async(args=(trigger.id,),

View File

@@ -10,7 +10,9 @@ from sqlalchemy import or_
from api.extensions import db
from api.lib.perm.acl.app import AppCRUD
from api.lib.perm.acl.audit import AuditCRUD, AuditOperateType, AuditScope
from api.lib.perm.acl.audit import AuditCRUD
from api.lib.perm.acl.audit import AuditOperateType
from api.lib.perm.acl.audit import AuditScope
from api.lib.perm.acl.cache import AppCache
from api.lib.perm.acl.cache import HasResourceRoleCache
from api.lib.perm.acl.cache import RoleCache
@@ -69,16 +71,16 @@ class RoleRelationCRUD(object):
@staticmethod
def get_parent_ids(rid, app_id):
if app_id is not None:
return [i.parent_id for i in RoleRelation.get_by(child_id=rid, app_id=app_id, to_dict=False)] + \
[i.parent_id for i in RoleRelation.get_by(child_id=rid, app_id=None, to_dict=False)]
return ([i.parent_id for i in RoleRelation.get_by(child_id=rid, app_id=app_id, to_dict=False)] +
[i.parent_id for i in RoleRelation.get_by(child_id=rid, app_id=None, to_dict=False)])
else:
return [i.parent_id for i in RoleRelation.get_by(child_id=rid, app_id=app_id, to_dict=False)]
@staticmethod
def get_child_ids(rid, app_id):
if app_id is not None:
return [i.child_id for i in RoleRelation.get_by(parent_id=rid, app_id=app_id, to_dict=False)] + \
[i.child_id for i in RoleRelation.get_by(parent_id=rid, app_id=None, to_dict=False)]
return ([i.child_id for i in RoleRelation.get_by(parent_id=rid, app_id=app_id, to_dict=False)] +
[i.child_id for i in RoleRelation.get_by(parent_id=rid, app_id=None, to_dict=False)])
else:
return [i.child_id for i in RoleRelation.get_by(parent_id=rid, app_id=app_id, to_dict=False)]
@@ -213,6 +215,7 @@ class RoleCRUD(object):
@staticmethod
def search(q, app_id, page=1, page_size=None, user_role=True, is_all=False, user_only=False):
if user_only: # only user role
query = db.session.query(Role).filter(Role.deleted.is_(False)).filter(Role.uid.isnot(None))
@@ -270,13 +273,6 @@ class RoleCRUD(object):
RoleCache.clean(rid)
role = role.update(**kwargs)
if origin['uid'] and kwargs.get('name') and kwargs.get('name') != origin['name']:
from api.models.acl import User
user = User.get_by(uid=origin['uid'], first=True, to_dict=False)
if user:
user.update(username=kwargs['name'])
AuditCRUD.add_role_log(role.app_id, AuditOperateType.update,
AuditScope.role, role.id, origin, role.to_dict(), {},
)
@@ -295,11 +291,12 @@ class RoleCRUD(object):
from api.lib.perm.acl.acl import is_admin
role = Role.get_by_id(rid) or abort(404, ErrFormat.role_not_found.format("rid={}".format(rid)))
if not role.app_id and not is_admin():
return abort(403, ErrFormat.admin_required)
not force and role.uid and abort(400, ErrFormat.user_role_delete_invalid)
if not role.app_id and not is_admin():
return abort(403, ErrFormat.admin_required)
origin = role.to_dict()
child_ids = []
@@ -308,18 +305,20 @@ class RoleCRUD(object):
for i in RoleRelation.get_by(parent_id=rid, to_dict=False):
child_ids.append(i.child_id)
i.soft_delete()
i.soft_delete(commit=False)
for i in RoleRelation.get_by(child_id=rid, to_dict=False):
parent_ids.append(i.parent_id)
i.soft_delete()
i.soft_delete(commit=False)
role_permissions = []
for i in RolePermission.get_by(rid=rid, to_dict=False):
role_permissions.append(i.to_dict())
i.soft_delete()
i.soft_delete(commit=False)
role.soft_delete()
role.soft_delete(commit=False)
db.session.commit()
role_rebuild.apply_async(args=(recursive_child_ids, role.app_id), queue=ACL_QUEUE)

View File

@@ -58,14 +58,10 @@ class UserCRUD(object):
kwargs['employee_id'] = '{0:04d}'.format(biggest_employee_id + 1)
user = User.create(**kwargs)
role = RoleCRUD.add_role(user.username, uid=user.uid)
RoleCRUD.add_role(user.username, uid=user.uid)
AuditCRUD.add_role_log(None, AuditOperateType.create,
AuditScope.user, user.uid, {}, user.to_dict(), {}, {}
)
from api.lib.common_setting.employee import EmployeeCRUD
payload = {column: getattr(user, column) for column in ['uid', 'username', 'nickname', 'email', 'block']}
payload['rid'] = role.id
EmployeeCRUD.add_employee_from_acl_created(**payload)
return user

View File

@@ -1 +0,0 @@
# -*- coding:utf-8 -*-

View File

@@ -1,429 +0,0 @@
import os
import secrets
import sys
from base64 import b64decode, b64encode
from Cryptodome.Protocol.SecretSharing import Shamir
from colorama import Back
from colorama import Fore
from colorama import Style
from colorama import init as colorama_init
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives import padding
from cryptography.hazmat.primitives.ciphers import Cipher
from cryptography.hazmat.primitives.ciphers import algorithms
from cryptography.hazmat.primitives.ciphers import modes
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from flask import current_app
global_iv_length = 16
global_key_shares = 5 # Number of generated key shares
global_key_threshold = 3 # Minimum number of shares required to rebuild the key
backend_root_key_name = "root_key"
backend_encrypt_key_name = "encrypt_key"
backend_root_key_salt_name = "root_key_salt"
backend_encrypt_key_salt_name = "encrypt_key_salt"
backend_seal_key = "seal_status"
success = "success"
seal_status = True
def string_to_bytes(value):
if isinstance(value, bytes):
return value
if sys.version_info.major == 2:
byte_string = value
else:
byte_string = value.encode("utf-8")
return byte_string
class Backend:
def __init__(self, backend=None):
self.backend = backend
def get(self, key):
return self.backend.get(key)
def add(self, key, value):
return self.backend.add(key, value)
def update(self, key, value):
return self.backend.update(key, value)
class KeyManage:
def __init__(self, trigger=None, backend=None):
self.trigger = trigger
self.backend = backend
if backend:
self.backend = Backend(backend)
def init_app(self, app, backend=None):
if (sys.argv[0].endswith("gunicorn") or
(len(sys.argv) > 1 and sys.argv[1] in ("run", "cmdb-password-data-migrate"))):
self.trigger = app.config.get("INNER_TRIGGER_TOKEN")
if not self.trigger:
return
self.backend = backend
resp = self.auto_unseal()
self.print_response(resp)
def hash_root_key(self, value):
algorithm = hashes.SHA256()
salt = self.backend.get(backend_root_key_salt_name)
if not salt:
salt = secrets.token_hex(16)
msg, ok = self.backend.add(backend_root_key_salt_name, salt)
if not ok:
return msg, ok
kdf = PBKDF2HMAC(
algorithm=algorithm,
length=32,
salt=string_to_bytes(salt),
iterations=100000,
)
key = kdf.derive(string_to_bytes(value))
return b64encode(key).decode('utf-8'), True
def generate_encrypt_key(self, key):
algorithm = hashes.SHA256()
salt = self.backend.get(backend_encrypt_key_salt_name)
if not salt:
salt = secrets.token_hex(32)
kdf = PBKDF2HMAC(
algorithm=algorithm,
length=32,
salt=string_to_bytes(salt),
iterations=100000,
backend=default_backend()
)
key = kdf.derive(string_to_bytes(key))
msg, ok = self.backend.add(backend_encrypt_key_salt_name, salt)
if ok:
return b64encode(key).decode('utf-8'), ok
else:
return msg, ok
@classmethod
def generate_keys(cls, secret):
shares = Shamir.split(global_key_threshold, global_key_shares, secret, False)
new_shares = []
for share in shares:
t = [i for i in share[1]] + [ord(i) for i in "{:0>2}".format(share[0])]
new_shares.append(b64encode(bytes(t)))
return new_shares
def is_valid_root_key(self, root_key):
root_key_hash, ok = self.hash_root_key(root_key)
if not ok:
return root_key_hash, ok
backend_root_key_hash = self.backend.get(backend_root_key_name)
if not backend_root_key_hash:
return "should init firstly", False
elif backend_root_key_hash != root_key_hash:
return "invalid root key", False
else:
return "", True
def auth_root_secret(self, root_key):
msg, ok = self.is_valid_root_key(root_key)
if not ok:
return {
"message": msg,
"status": "failed"
}
encrypt_key_aes = self.backend.get(backend_encrypt_key_name)
if not encrypt_key_aes:
return {
"message": "encrypt key is empty",
"status": "failed"
}
secrets_encrypt_key, ok = InnerCrypt.aes_decrypt(string_to_bytes(root_key), encrypt_key_aes)
if ok:
msg, ok = self.backend.update(backend_seal_key, "open")
if ok:
current_app.config["secrets_encrypt_key"] = secrets_encrypt_key
current_app.config["secrets_root_key"] = root_key
current_app.config["secrets_shares"] = []
return {"message": success, "status": success}
return {"message": msg, "status": "failed"}
else:
return {
"message": secrets_encrypt_key,
"status": "failed"
}
def unseal(self, key):
if not self.is_seal():
return {
"message": "current status is unseal, skip",
"status": "skip"
}
try:
t = [i for i in b64decode(key)]
v = (int("".join([chr(i) for i in t[-2:]])), bytes(t[:-2]))
shares = current_app.config.get("secrets_shares", [])
if v not in shares:
shares.append(v)
current_app.config["secrets_shares"] = shares
if len(shares) >= global_key_threshold:
recovered_secret = Shamir.combine(shares[:global_key_threshold], False)
return self.auth_root_secret(b64encode(recovered_secret))
else:
return {
"message": "waiting for inputting other unseal key {0}/{1}".format(len(shares),
global_key_threshold),
"status": "waiting"
}
except Exception as e:
return {
"message": "invalid token: " + str(e),
"status": "failed"
}
def generate_unseal_keys(self):
info = self.backend.get(backend_root_key_name)
if info:
return "already exist", [], False
secret = AESGCM.generate_key(128)
shares = self.generate_keys(secret)
return b64encode(secret), shares, True
def init(self):
"""
init the master key, unseal key and store in backend
:return:
"""
root_key = self.backend.get(backend_root_key_name)
if root_key:
return {"message": "already init, skip", "status": "skip"}, False
else:
root_key, shares, status = self.generate_unseal_keys()
if not status:
return {"message": root_key, "status": "failed"}, False
# hash root key and store in backend
root_key_hash, ok = self.hash_root_key(root_key)
if not ok:
return {"message": root_key_hash, "status": "failed"}, False
msg, ok = self.backend.add(backend_root_key_name, root_key_hash)
if not ok:
return {"message": msg, "status": "failed"}, False
# generate encrypt key from root_key and store in backend
encrypt_key, ok = self.generate_encrypt_key(root_key)
if not ok:
return {"message": encrypt_key, "status": "failed"}
encrypt_key_aes, status = InnerCrypt.aes_encrypt(root_key, encrypt_key)
if not status:
return {"message": encrypt_key_aes, "status": "failed"}
msg, ok = self.backend.add(backend_encrypt_key_name, encrypt_key_aes)
if not ok:
return {"message": msg, "status": "failed"}, False
msg, ok = self.backend.add(backend_seal_key, "open")
if not ok:
return {"message": msg, "status": "failed"}, False
current_app.config["secrets_root_key"] = root_key
current_app.config["secrets_encrypt_key"] = encrypt_key
self.print_token(shares, root_token=root_key)
return {"message": "OK",
"details": {
"root_token": root_key,
"seal_tokens": shares,
}}, True
def auto_unseal(self):
if not self.trigger:
return {
"message": "trigger config is empty, skip",
"status": "skip"
}
if self.trigger.startswith("http"):
return {
"message": "todo in next step, skip",
"status": "skip"
}
# TODO
elif len(self.trigger.strip()) == 24:
res = self.auth_root_secret(self.trigger.encode())
if res.get("status") == success:
return {
"message": success,
"status": success
}
else:
return {
"message": res.get("message"),
"status": "failed"
}
else:
return {
"message": "trigger config is invalid, skip",
"status": "skip"
}
def seal(self, root_key):
root_key = root_key.encode()
msg, ok = self.is_valid_root_key(root_key)
if not ok:
return {
"message": msg,
"status": "failed"
}
else:
msg, ok = self.backend.update(backend_seal_key, "block")
if not ok:
return {
"message": msg,
"status": "failed",
}
current_app.config["secrets_root_key"] = ''
current_app.config["secrets_encrypt_key"] = ''
return {
"message": success,
"status": success
}
def is_seal(self):
"""
If there is no initialization or the root key is inconsistent, it is considered to be in a sealed state.
:return:
"""
secrets_root_key = current_app.config.get("secrets_root_key")
msg, ok = self.is_valid_root_key(secrets_root_key)
if not ok:
return true
status = self.backend.get(backend_seal_key)
return status == "block"
@classmethod
def print_token(cls, shares, root_token):
"""
data: {"message": "OK",
"details": {
"root_token": root_key,
"seal_tokens": shares,
}}
"""
colorama_init()
print(Style.BRIGHT, "Please be sure to store the Unseal Key in a secure location and avoid losing it."
" The Unseal Key is required to unseal the system every time when it restarts."
" Successful unsealing is necessary to enable the password feature." + Style.RESET_ALL)
for i, v in enumerate(shares):
print(
"unseal token " + str(i + 1) + ": " + Fore.RED + Back.BLACK + v.decode("utf-8") + Style.RESET_ALL)
print()
print(Fore.GREEN + "root token: " + root_token.decode("utf-8") + Style.RESET_ALL)
@classmethod
def print_response(cls, data):
status = data.get("status", "")
message = data.get("message", "")
status_colors = {
"skip": Style.BRIGHT,
"failed": Fore.RED,
"waiting": Fore.YELLOW,
}
print(status_colors.get(status, Fore.GREEN), message, Style.RESET_ALL)
class InnerCrypt:
def __init__(self):
secrets_encrypt_key = current_app.config.get("secrets_encrypt_key", "")
self.encrypt_key = b64decode(secrets_encrypt_key.encode("utf-8"))
def encrypt(self, plaintext):
"""
encrypt method contain aes currently
"""
return self.aes_encrypt(self.encrypt_key, plaintext)
def decrypt(self, ciphertext):
"""
decrypt method contain aes currently
"""
return self.aes_decrypt(self.encrypt_key, ciphertext)
@classmethod
def aes_encrypt(cls, key, plaintext):
if isinstance(plaintext, str):
plaintext = string_to_bytes(plaintext)
iv = os.urandom(global_iv_length)
try:
cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend())
encryptor = cipher.encryptor()
v_padder = padding.PKCS7(algorithms.AES.block_size).padder()
padded_plaintext = v_padder.update(plaintext) + v_padder.finalize()
ciphertext = encryptor.update(padded_plaintext) + encryptor.finalize()
return b64encode(iv + ciphertext).decode("utf-8"), True
except Exception as e:
return str(e), False
@classmethod
def aes_decrypt(cls, key, ciphertext):
try:
s = b64decode(ciphertext.encode("utf-8"))
iv = s[:global_iv_length]
ciphertext = s[global_iv_length:]
cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend())
decrypter = cipher.decryptor()
decrypted_padded_plaintext = decrypter.update(ciphertext) + decrypter.finalize()
unpadder = padding.PKCS7(algorithms.AES.block_size).unpadder()
plaintext = unpadder.update(decrypted_padded_plaintext) + unpadder.finalize()
return plaintext.decode('utf-8'), True
except Exception as e:
return str(e), False
if __name__ == "__main__":
km = KeyManage()
# info, shares, status = km.generate_unseal_keys()
# print(info, shares, status)
# print("..................")
# for i in shares:
# print(b64encode(i[1]).decode())
res1, ok1 = km.init()
if not ok1:
print(res1)
# for j in res["details"]["seal_tokens"]:
# r = km.unseal(j)
# if r["status"] != "waiting":
# if r["status"] != "success":
# print("r........", r)
# else:
# print(r)
# break
t_plaintext = b"Hello, World!" # The plaintext to encrypt
c = InnerCrypt()
t_ciphertext, status1 = c.encrypt(t_plaintext)
print("Ciphertext:", t_ciphertext)
decrypted_plaintext, status2 = c.decrypt(t_ciphertext)
print("Decrypted plaintext:", decrypted_plaintext)

View File

@@ -1,35 +0,0 @@
from api.models.cmdb import InnerKV
class InnerKVManger(object):
def __init__(self):
pass
@classmethod
def add(cls, key, value):
data = {"key": key, "value": value}
res = InnerKV.create(**data)
if res.key == key:
return "success", True
return "add failed", False
@classmethod
def get(cls, key):
res = InnerKV.get_by(first=True, to_dict=False, key=key)
if not res:
return None
return res.value
@classmethod
def update(cls, key, value):
res = InnerKV.get_by(first=True, to_dict=False, key=key)
if not res:
return cls.add(key, value)
t = res.update(value=value)
if t.key == key:
return "success", True
return "update failed", True

View File

@@ -1,141 +0,0 @@
from base64 import b64decode
from base64 import b64encode
import hvac
class VaultClient:
def __init__(self, base_url, token, mount_path='cmdb'):
self.client = hvac.Client(url=base_url, token=token)
self.mount_path = mount_path
def create_app_role(self, role_name, policies):
resp = self.client.create_approle(role_name, policies=policies)
return resp == 200
def delete_app_role(self, role_name):
resp = self.client.delete_approle(role_name)
return resp == 204
def update_app_role_policies(self, role_name, policies):
resp = self.client.update_approle_role(role_name, policies=policies)
return resp == 204
def get_app_role(self, role_name):
resp = self.client.get_approle(role_name)
resp.json()
if resp.status_code == 200:
return resp.json
else:
return {}
def enable_secrets_engine(self):
resp = self.client.sys.enable_secrets_engine('kv', path=self.mount_path)
resp_01 = self.client.sys.enable_secrets_engine('transit')
if resp.status_code == 200 and resp_01.status_code == 200:
return resp.json
else:
return {}
def encrypt(self, plaintext):
response = self.client.secrets.transit.encrypt_data(name='transit-key', plaintext=plaintext)
ciphertext = response['data']['ciphertext']
return ciphertext
# decrypt data
def decrypt(self, ciphertext):
response = self.client.secrets.transit.decrypt_data(name='transit-key', ciphertext=ciphertext)
plaintext = response['data']['plaintext']
return plaintext
def write(self, path, data, encrypt=None):
if encrypt:
for k, v in data.items():
data[k] = self.encrypt(self.encode_base64(v))
response = self.client.secrets.kv.v2.create_or_update_secret(
path=path,
secret=data,
mount_point=self.mount_path
)
return response
# read data
def read(self, path, decrypt=True):
try:
response = self.client.secrets.kv.v2.read_secret_version(
path=path, raise_on_deleted_version=False, mount_point=self.mount_path
)
except Exception as e:
return str(e), False
data = response['data']['data']
if decrypt:
try:
for k, v in data.items():
data[k] = self.decode_base64(self.decrypt(v))
except:
return data, True
return data, True
# update data
def update(self, path, data, overwrite=True, encrypt=True):
if encrypt:
for k, v in data.items():
data[k] = self.encrypt(self.encode_base64(v))
if overwrite:
response = self.client.secrets.kv.v2.create_or_update_secret(
path=path,
secret=data,
mount_point=self.mount_path
)
else:
response = self.client.secrets.kv.v2.patch(path=path, secret=data, mount_point=self.mount_path)
return response
# delete data
def delete(self, path):
response = self.client.secrets.kv.v2.delete_metadata_and_all_versions(
path=path,
mount_point=self.mount_path
)
return response
# Base64 encode
@classmethod
def encode_base64(cls, data):
encoded_bytes = b64encode(data.encode())
encoded_string = encoded_bytes.decode()
return encoded_string
# Base64 decode
@classmethod
def decode_base64(cls, encoded_string):
decoded_bytes = b64decode(encoded_string)
decoded_string = decoded_bytes.decode()
return decoded_string
if __name__ == "__main__":
_base_url = "http://localhost:8200"
_token = "your token"
_path = "test001"
# Example
sdk = VaultClient(_base_url, _token)
# sdk.enable_secrets_engine()
_data = {"key1": "value1", "key2": "value2", "key3": "value3"}
_data = sdk.update(_path, _data, overwrite=True, encrypt=True)
print(_data)
_data = sdk.read(_path, decrypt=True)
print(_data)

View File

@@ -12,9 +12,6 @@ from Crypto.Cipher import AES
from elasticsearch import Elasticsearch
from flask import current_app
from api.lib.secrets.inner import InnerCrypt
from api.lib.secrets.inner import KeyManage
class BaseEnum(object):
_ALL_ = set() # type: Set[str]
@@ -289,33 +286,3 @@ class AESCrypto(object):
text_decrypted = cipher.decrypt(encode_bytes)
return cls.unpad(text_decrypted).decode('utf8')
class Crypto(AESCrypto):
@classmethod
def encrypt(cls, data):
from api.lib.secrets.secrets import InnerKVManger
if not KeyManage(backend=InnerKVManger()).is_seal():
res, status = InnerCrypt().encrypt(data)
if status:
return res
return AESCrypto().encrypt(data)
@classmethod
def decrypt(cls, data):
from api.lib.secrets.secrets import InnerKVManger
if not KeyManage(backend=InnerKVManger()).is_seal():
try:
res, status = InnerCrypt().decrypt(data)
if status:
return res
except:
pass
try:
return AESCrypto().decrypt(data)
except:
return data

View File

@@ -1,109 +0,0 @@
# -*- coding:utf-8 -*-
import json
from functools import partial
import requests
from jinja2 import Template
from requests.auth import HTTPBasicAuth
from requests_oauthlib import OAuth2Session
class BearerAuth(requests.auth.AuthBase):
def __init__(self, token):
self.token = token
def __call__(self, r):
r.headers["authorization"] = "Bearer {}".format(self.token)
return r
def _wrap_auth(**kwargs):
auth_type = (kwargs.get('type') or "").lower()
if auth_type == "basicauth":
return HTTPBasicAuth(kwargs.get('username'), kwargs.get('password'))
elif auth_type == "bearer":
return BearerAuth(kwargs.get('token'))
elif auth_type == 'oauth2.0':
client_id = kwargs.get('client_id')
client_secret = kwargs.get('client_secret')
authorization_base_url = kwargs.get('authorization_base_url')
token_url = kwargs.get('token_url')
redirect_url = kwargs.get('redirect_url')
scope = kwargs.get('scope')
oauth2_session = OAuth2Session(client_id, scope=scope or None)
oauth2_session.authorization_url(authorization_base_url)
oauth2_session.fetch_token(token_url, client_secret=client_secret, authorization_response=redirect_url)
return oauth2_session
elif auth_type == "apikey":
return HTTPBasicAuth(kwargs.get('key'), kwargs.get('value'))
def webhook_request(webhook, payload):
"""
:param webhook:
{
"url": "https://veops.cn"
"method": "GET|POST|PUT|DELETE"
"body": {},
"headers": {
"Content-Type": "Application/json"
},
"parameters": {
"key": "value"
},
"authorization": {
"type": "BasicAuth|Bearer|OAuth2.0|APIKey",
"password": "mmmm", # BasicAuth
"username": "bbb", # BasicAuth
"token": "xxx", # Bearer
"key": "xxx", # APIKey
"value": "xxx", # APIKey
"client_id": "xxx", # OAuth2.0
"client_secret": "xxx", # OAuth2.0
"authorization_base_url": "xxx", # OAuth2.0
"token_url": "xxx", # OAuth2.0
"redirect_url": "xxx", # OAuth2.0
"scope": "xxx" # OAuth2.0
}
}
:param payload:
:return:
"""
assert webhook.get('url') is not None
payload = {k: v or '' for k, v in payload.items()}
url = Template(webhook['url']).render(payload)
params = webhook.get('parameters') or None
if isinstance(params, dict):
params = json.loads(Template(json.dumps(params)).render(payload))
headers = json.loads(Template(json.dumps(webhook.get('headers') or {})).render(payload))
data = Template(json.dumps(webhook.get('body', ''))).render(payload)
auth = _wrap_auth(**webhook.get('authorization', {}))
if (webhook.get('authorization', {}).get("type") or '').lower() == 'oauth2.0':
request = getattr(auth, webhook.get('method', 'GET').lower())
else:
request = partial(requests.request, webhook.get('method', 'GET'))
return request(
url,
params=params,
headers=headers or None,
data=data,
auth=auth
)

View File

@@ -5,8 +5,7 @@ import copy
import hashlib
from datetime import datetime
from ldap3 import Server, Connection, ALL
from ldap3.core.exceptions import LDAPBindError, LDAPCertificateError
import ldap
from flask import current_app
from flask_sqlalchemy import BaseQuery
@@ -58,7 +57,9 @@ class UserQuery(BaseQuery):
return user, authenticated
def authenticate_with_ldap(self, username, password):
server = Server(current_app.config.get('LDAP_SERVER'), get_info=ALL)
ldap_conn = ldap.initialize(current_app.config.get('LDAP_SERVER'))
ldap_conn.protocol_version = 3
ldap_conn.set_option(ldap.OPT_REFERRALS, 0)
if '@' in username:
email = username
who = current_app.config.get('LDAP_USER_DN').format(username.split('@')[0])
@@ -69,14 +70,11 @@ class UserQuery(BaseQuery):
username = username.split('@')[0]
user = self.get_by_username(username)
try:
if not password:
raise LDAPCertificateError
conn = Connection(server, user=who, password=password)
conn.bind()
if conn.result['result'] != 0:
raise LDAPBindError
conn.unbind()
if not password:
raise ldap.INVALID_CREDENTIALS
ldap_conn.simple_bind_s(who, password)
if not user:
from api.lib.perm.acl.user import UserCRUD
@@ -86,7 +84,7 @@ class UserQuery(BaseQuery):
op_record.apply_async(args=(None, username, OperateType.LOGIN, ["ACL"]), queue=ACL_QUEUE)
return user, True
except LDAPBindError:
except ldap.INVALID_CREDENTIALS:
return user, False
def search(self, key):

View File

@@ -12,9 +12,7 @@ from api.lib.cmdb.const import CITypeOperateType
from api.lib.cmdb.const import ConstraintEnum
from api.lib.cmdb.const import OperateType
from api.lib.cmdb.const import ValueTypeEnum
from api.lib.database import Model
from api.lib.database import Model2
from api.lib.utils import Crypto
from api.lib.database import Model, Model2
# template
@@ -91,37 +89,12 @@ class Attribute(Model):
compute_expr = db.Column(db.Text)
compute_script = db.Column(db.Text)
_choice_web_hook = db.Column('choice_web_hook', db.JSON)
choice_other = db.Column(db.JSON)
choice_web_hook = db.Column(db.JSON)
uid = db.Column(db.Integer, index=True)
option = db.Column(db.JSON)
def _get_webhook(self):
if self._choice_web_hook:
if self._choice_web_hook.get('headers') and "Cookie" in self._choice_web_hook['headers']:
self._choice_web_hook['headers']['Cookie'] = Crypto.decrypt(self._choice_web_hook['headers']['Cookie'])
if self._choice_web_hook.get('authorization'):
for k, v in self._choice_web_hook['authorization'].items():
self._choice_web_hook['authorization'][k] = Crypto.decrypt(v)
return self._choice_web_hook
def _set_webhook(self, data):
if data:
if data.get('headers') and "Cookie" in data['headers']:
data['headers']['Cookie'] = Crypto.encrypt(data['headers']['Cookie'])
if data.get('authorization'):
for k, v in data['authorization'].items():
data['authorization'][k] = Crypto.encrypt(v)
self._choice_web_hook = data
choice_web_hook = db.synonym("_choice_web_hook", descriptor=property(_get_webhook, _set_webhook))
class CITypeAttribute(Model):
__tablename__ = "c_ci_type_attributes"
@@ -152,45 +125,16 @@ class CITypeAttributeGroupItem(Model):
class CITypeTrigger(Model):
# __tablename__ = "c_ci_type_triggers"
__tablename__ = "c_c_t_t"
type_id = db.Column(db.Integer, db.ForeignKey('c_ci_types.id'), nullable=False)
attr_id = db.Column(db.Integer, db.ForeignKey("c_attributes.id"))
_option = db.Column('notify', db.JSON)
def _get_option(self):
if self._option and self._option.get('webhooks'):
if self._option['webhooks'].get('authorization'):
for k, v in self._option['webhooks']['authorization'].items():
self._option['webhooks']['authorization'][k] = Crypto.decrypt(v)
return self._option
def _set_option(self, data):
if data and data.get('webhooks'):
if data['webhooks'].get('authorization'):
for k, v in data['webhooks']['authorization'].items():
data['webhooks']['authorization'][k] = Crypto.encrypt(v)
self._option = data
option = db.synonym("_option", descriptor=property(_get_option, _set_option))
class CITriggerHistory(Model):
__tablename__ = "c_ci_trigger_histories"
operate_type = db.Column(db.Enum(*OperateType.all(), name="operate_type"))
record_id = db.Column(db.Integer, db.ForeignKey("c_records.id"))
ci_id = db.Column(db.Integer, index=True, nullable=False)
trigger_id = db.Column(db.Integer, db.ForeignKey("c_c_t_t.id"))
trigger_name = db.Column(db.String(64))
is_ok = db.Column(db.Boolean, default=False)
notify = db.Column(db.Text)
webhook = db.Column(db.Text)
attr_id = db.Column(db.Integer, db.ForeignKey("c_attributes.id"), nullable=False)
notify = db.Column(db.JSON) # {subject: x, body: x, wx_to: [], mail_to: [], before_days: 0, notify_at: 08:00}
class CITypeUniqueConstraint(Model):
# __tablename__ = "c_ci_type_unique_constraints"
__tablename__ = "c_c_t_u_c"
type_id = db.Column(db.Integer, db.ForeignKey('c_ci_types.id'), nullable=False)
@@ -419,6 +363,7 @@ class CITypeHistory(Model):
# preference
class PreferenceShowAttributes(Model):
# __tablename__ = "c_preference_show_attributes"
__tablename__ = "c_psa"
uid = db.Column(db.Integer, index=True, nullable=False)
@@ -432,6 +377,7 @@ class PreferenceShowAttributes(Model):
class PreferenceTreeView(Model):
# __tablename__ = "c_preference_tree_views"
__tablename__ = "c_ptv"
uid = db.Column(db.Integer, index=True, nullable=False)
@@ -440,6 +386,7 @@ class PreferenceTreeView(Model):
class PreferenceRelationView(Model):
# __tablename__ = "c_preference_relation_views"
__tablename__ = "c_prv"
uid = db.Column(db.Integer, index=True, nullable=False)
@@ -548,10 +495,3 @@ class CIFilterPerms(Model):
attr_filter = db.Column(db.Text)
rid = db.Column(db.Integer, index=True)
class InnerKV(Model):
__tablename__ = "c_kv"
key = db.Column(db.String(128), index=True)
value = db.Column(db.Text)

View File

@@ -47,8 +47,6 @@ class Employee(ModelWithoutPK):
last_login = db.Column(db.TIMESTAMP, nullable=True)
block = db.Column(db.Integer, default=0)
notice_info = db.Column(db.JSON, default={})
_department = db.relationship(
'Department', backref='common_employee.department_id',
lazy='joined'
@@ -89,10 +87,3 @@ class CommonData(Model):
data_type = db.Column(db.VARCHAR(255), default='')
data = db.Column(db.JSON)
class NoticeConfig(Model):
__tablename__ = "common_notice_config"
platform = db.Column(db.VARCHAR(255), nullable=False)
info = db.Column(db.JSON)

View File

@@ -46,4 +46,5 @@ def register_resources(resource_path, rest_api):
resource_cls.url_prefix = ("",)
if isinstance(resource_cls.url_prefix, six.string_types):
resource_cls.url_prefix = (resource_cls.url_prefix,)
rest_api.add_resource(resource_cls, *resource_cls.url_prefix)

View File

@@ -9,8 +9,7 @@ from werkzeug.exceptions import BadRequest
from werkzeug.exceptions import NotFound
from api.extensions import celery
from api.lib.decorator import flush_db
from api.lib.decorator import reconnect_db
from api.extensions import db
from api.lib.perm.acl.audit import AuditCRUD
from api.lib.perm.acl.audit import AuditOperateSource
from api.lib.perm.acl.audit import AuditOperateType
@@ -29,7 +28,6 @@ from api.models.acl import Trigger
name="acl.role_rebuild",
queue=ACL_QUEUE,
once={"graceful": True, "unlock_before_run": True})
@reconnect_db
def role_rebuild(rids, app_id):
rids = rids if isinstance(rids, list) else [rids]
for rid in rids:
@@ -39,7 +37,6 @@ def role_rebuild(rids, app_id):
@celery.task(name="acl.update_resource_to_build_role", queue=ACL_QUEUE)
@reconnect_db
def update_resource_to_build_role(resource_id, app_id, group_id=None):
rids = [i.id for i in Role.get_by(__func_isnot__key_uid=None, fl='id', to_dict=False)]
rids += [i.id for i in Role.get_by(app_id=app_id, fl='id', to_dict=False)]
@@ -55,9 +52,9 @@ def update_resource_to_build_role(resource_id, app_id, group_id=None):
@celery.task(name="acl.apply_trigger", queue=ACL_QUEUE)
@flush_db
@reconnect_db
def apply_trigger(_id, resource_id=None, operator_uid=None):
db.session.remove()
from api.lib.perm.acl.permission import PermissionCRUD
trigger = Trigger.get_by_id(_id)
@@ -121,9 +118,9 @@ def apply_trigger(_id, resource_id=None, operator_uid=None):
@celery.task(name="acl.cancel_trigger", queue=ACL_QUEUE)
@flush_db
@reconnect_db
def cancel_trigger(_id, resource_id=None, operator_uid=None):
db.session.remove()
from api.lib.perm.acl.permission import PermissionCRUD
trigger = Trigger.get_by_id(_id)
@@ -189,7 +186,6 @@ def cancel_trigger(_id, resource_id=None, operator_uid=None):
@celery.task(name="acl.op_record", queue=ACL_QUEUE)
@reconnect_db
def op_record(app, rolename, operate_type, obj):
if isinstance(app, int):
app = AppCache.get(app)

View File

@@ -4,6 +4,8 @@
import json
import time
import jinja2
import requests
from flask import current_app
from flask_login import login_user
@@ -16,8 +18,7 @@ from api.lib.cmdb.cache import CITypeAttributesCache
from api.lib.cmdb.const import CMDB_QUEUE
from api.lib.cmdb.const import REDIS_PREFIX_CI
from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION
from api.lib.decorator import flush_db
from api.lib.decorator import reconnect_db
from api.lib.mail import send_mail
from api.lib.perm.acl.cache import UserCache
from api.lib.utils import Lock
from api.lib.utils import handle_arg_list
@@ -27,12 +28,9 @@ from api.models.cmdb import CITypeAttribute
@celery.task(name="cmdb.ci_cache", queue=CMDB_QUEUE)
@flush_db
@reconnect_db
def ci_cache(ci_id, operate_type, record_id):
from api.lib.cmdb.ci import CITriggerManager
def ci_cache(ci_id):
time.sleep(0.01)
db.session.remove()
m = api.lib.cmdb.ci.CIManager()
ci_dict = m.get_ci_by_id_from_db(ci_id, need_children=False, use_master=False)
@@ -44,18 +42,11 @@ def ci_cache(ci_id, operate_type, record_id):
current_app.logger.info("{0} flush..........".format(ci_id))
if operate_type:
current_app.test_request_context().push()
login_user(UserCache.get('worker'))
CITriggerManager.fire(operate_type, ci_dict, record_id)
@celery.task(name="cmdb.batch_ci_cache", queue=CMDB_QUEUE)
@flush_db
@reconnect_db
def batch_ci_cache(ci_ids, ): # only for attribute change index
def batch_ci_cache(ci_ids):
time.sleep(1)
db.session.remove()
for ci_id in ci_ids:
m = api.lib.cmdb.ci.CIManager()
@@ -70,7 +61,6 @@ def batch_ci_cache(ci_ids, ): # only for attribute change index
@celery.task(name="cmdb.ci_delete", queue=CMDB_QUEUE)
@reconnect_db
def ci_delete(ci_id):
current_app.logger.info(ci_id)
@@ -82,22 +72,10 @@ def ci_delete(ci_id):
current_app.logger.info("{0} delete..........".format(ci_id))
@celery.task(name="cmdb.ci_delete_trigger", queue=CMDB_QUEUE)
@reconnect_db
def ci_delete_trigger(trigger, operate_type, ci_dict):
current_app.logger.info('delete ci {} trigger'.format(ci_dict['_id']))
from api.lib.cmdb.ci import CITriggerManager
current_app.test_request_context().push()
login_user(UserCache.get('worker'))
CITriggerManager.fire_by_trigger(trigger, operate_type, ci_dict)
@celery.task(name="cmdb.ci_relation_cache", queue=CMDB_QUEUE)
@flush_db
@reconnect_db
def ci_relation_cache(parent_id, child_id):
db.session.remove()
with Lock("CIRelation_{}".format(parent_id)):
children = rd.get([parent_id], REDIS_PREFIX_CI_RELATION)[0]
children = json.loads(children) if children is not None else {}
@@ -112,8 +90,6 @@ def ci_relation_cache(parent_id, child_id):
@celery.task(name="cmdb.ci_relation_add", queue=CMDB_QUEUE)
@flush_db
@reconnect_db
def ci_relation_add(parent_dict, child_id, uid):
"""
:param parent_dict: key is '$parent_model.attr_name'
@@ -129,6 +105,8 @@ def ci_relation_add(parent_dict, child_id, uid):
current_app.test_request_context().push()
login_user(UserCache.get(uid))
db.session.remove()
for parent in parent_dict:
parent_ci_type_name, _attr_name = parent.strip()[1:].split('.', 1)
attr_name = CITypeAttributeManager.get_attr_name(parent_ci_type_name, _attr_name)
@@ -153,14 +131,10 @@ def ci_relation_add(parent_dict, child_id, uid):
except Exception as e:
current_app.logger.warning(e)
finally:
try:
db.session.commit()
except:
pass
db.session.remove()
@celery.task(name="cmdb.ci_relation_delete", queue=CMDB_QUEUE)
@reconnect_db
def ci_relation_delete(parent_id, child_id):
with Lock("CIRelation_{}".format(parent_id)):
children = rd.get([parent_id], REDIS_PREFIX_CI_RELATION)[0]
@@ -175,19 +149,15 @@ def ci_relation_delete(parent_id, child_id):
@celery.task(name="cmdb.ci_type_attribute_order_rebuild", queue=CMDB_QUEUE)
@flush_db
@reconnect_db
def ci_type_attribute_order_rebuild(type_id, uid):
def ci_type_attribute_order_rebuild(type_id):
current_app.logger.info('rebuild attribute order')
db.session.remove()
from api.lib.cmdb.ci_type import CITypeAttributeGroupManager
attrs = CITypeAttributesCache.get(type_id)
id2attr = {attr.attr_id: attr for attr in attrs}
current_app.test_request_context().push()
login_user(UserCache.get(uid))
res = CITypeAttributeGroupManager.get_by_type_id(type_id, True)
order = 0
for group in res:
@@ -198,17 +168,56 @@ def ci_type_attribute_order_rebuild(type_id, uid):
order += 1
@celery.task(name='cmdb.trigger_notify', queue=CMDB_QUEUE)
def trigger_notify(notify, ci_id):
from api.lib.perm.acl.cache import UserCache
def _wrap_mail(mail_to):
if "@" not in mail_to:
user = UserCache.get(mail_to)
if user:
return user.email
return mail_to
db.session.remove()
m = api.lib.cmdb.ci.CIManager()
ci_dict = m.get_ci_by_id_from_db(ci_id, need_children=False, use_master=False)
subject = jinja2.Template(notify.get('subject') or "").render(ci_dict)
body = jinja2.Template(notify.get('body') or "").render(ci_dict)
if notify.get('wx_to'):
to_user = jinja2.Template('|'.join(notify['wx_to'])).render(ci_dict)
url = current_app.config.get("WX_URI")
data = {"to_user": to_user, "content": subject}
try:
requests.post(url, data=data)
except Exception as e:
current_app.logger.error(str(e))
if notify.get('mail_to'):
try:
if len(subject) > 700:
subject = subject[:600] + "..." + subject[-100:]
send_mail("", [_wrap_mail(jinja2.Template(i).render(ci_dict))
for i in notify['mail_to'] if i], subject, body)
except Exception as e:
current_app.logger.error("Send mail failed: {0}".format(str(e)))
@celery.task(name="cmdb.calc_computed_attribute", queue=CMDB_QUEUE)
@flush_db
@reconnect_db
def calc_computed_attribute(attr_id, uid):
from api.lib.cmdb.ci import CIManager
db.session.remove()
current_app.test_request_context().push()
login_user(UserCache.get(uid))
cim = CIManager()
for i in CITypeAttribute.get_by(attr_id=attr_id, to_dict=False):
cis = CI.get_by(type_id=i.type_id, to_dict=False)
for ci in cis:
cim.update(ci.id, {})
CIManager.update(ci.id, {})

View File

@@ -84,10 +84,11 @@ class CIView(APIView):
ci_dict = self._wrap_ci_dict()
manager = CIManager()
current_app.logger.debug(ci_dict)
ci_id = manager.add(ci_type,
exist_policy=exist_policy or ExistPolicy.REJECT,
_no_attribute_policy=_no_attribute_policy,
_is_admin=request.values.pop('__is_admin', None) or False,
_is_admin=request.values.pop('__is_admin', False),
**ci_dict)
return self.jsonify(ci_id=ci_id)
@@ -95,6 +96,7 @@ class CIView(APIView):
@has_perm_for_ci("ci_id", ResourceTypeEnum.CI, PermEnum.UPDATE, CIManager.get_type)
def put(self, ci_id=None):
args = request.values
current_app.logger.info(args)
ci_type = args.get("ci_type")
_no_attribute_policy = args.get("no_attribute_policy", ExistPolicy.IGNORE)
@@ -102,14 +104,14 @@ class CIView(APIView):
manager = CIManager()
if ci_id is not None:
manager.update(ci_id,
_is_admin=request.values.pop('__is_admin', None) or False,
_is_admin=request.values.pop('__is_admin', False),
**ci_dict)
else:
request.values.pop('exist_policy', None)
ci_id = manager.add(ci_type,
exist_policy=ExistPolicy.REPLACE,
_no_attribute_policy=_no_attribute_policy,
_is_admin=request.values.pop('__is_admin', None) or False,
_is_admin=request.values.pop('__is_admin', False),
**ci_dict)
return self.jsonify(ci_id=ci_id)
@@ -183,8 +185,8 @@ class CIUnique(APIView):
@has_perm_from_args("ci_id", ResourceTypeEnum.CI, PermEnum.UPDATE, CIManager.get_type_name)
def put(self, ci_id):
params = request.values
unique_name = list(params.keys())[0]
unique_value = list(params.values())[0]
unique_name = params.keys()[0]
unique_value = params.values()[0]
CIManager.update_unique_value(ci_id, unique_name, unique_value)
@@ -226,11 +228,11 @@ class CIFlushView(APIView):
from api.tasks.cmdb import ci_cache
from api.lib.cmdb.const import CMDB_QUEUE
if ci_id is not None:
ci_cache.apply_async(args=(ci_id, None, None), queue=CMDB_QUEUE)
ci_cache.apply_async([ci_id], queue=CMDB_QUEUE)
else:
cis = CI.get_by(to_dict=False)
for ci in cis:
ci_cache.apply_async(args=(ci.id, None, None), queue=CMDB_QUEUE)
ci_cache.apply_async([ci.id], queue=CMDB_QUEUE)
return self.jsonify(code=200)
@@ -240,13 +242,3 @@ class CIAutoDiscoveryStatisticsView(APIView):
def get(self):
return self.jsonify(CIManager.get_ad_statistics())
class CIPasswordView(APIView):
url_prefix = "/ci/<int:ci_id>/attributes/<int:attr_id>/password"
def get(self, ci_id, attr_id):
return self.jsonify(ci_id=ci_id, attr_id=attr_id, value=CIManager.load_password(ci_id, attr_id))
def post(self, ci_id, attr_id):
return self.get(ci_id, attr_id)

View File

@@ -419,22 +419,22 @@ class CITypeTriggerView(APIView):
return self.jsonify(CITypeTriggerManager.get(type_id))
@has_perm_from_args("type_id", ResourceTypeEnum.CI, PermEnum.CONFIG, CITypeManager.get_name_by_id)
@args_required("option")
@args_required("attr_id")
@args_required("notify")
def post(self, type_id):
attr_id = request.values.get('attr_id') or None
option = request.values.get('option')
attr_id = request.values.get('attr_id')
notify = request.values.get('notify')
return self.jsonify(CITypeTriggerManager().add(type_id, attr_id, option))
return self.jsonify(CITypeTriggerManager().add(type_id, attr_id, notify))
@has_perm_from_args("type_id", ResourceTypeEnum.CI, PermEnum.CONFIG, CITypeManager.get_name_by_id)
@args_required("option")
@args_required("notify")
def put(self, type_id, _id):
assert type_id is not None
option = request.values.get('option')
attr_id = request.values.get('attr_id')
notify = request.values.get('notify')
return self.jsonify(CITypeTriggerManager().update(_id, attr_id, option))
return self.jsonify(CITypeTriggerManager().update(_id, notify))
@has_perm_from_args("type_id", ResourceTypeEnum.CI, PermEnum.CONFIG, CITypeManager.get_name_by_id)
def delete(self, type_id, _id):
@@ -506,3 +506,4 @@ class CITypeFilterPermissionView(APIView):
@auth_with_app_token
def get(self, type_id):
return self.jsonify(CIFilterPermsCRUD().get(type_id))

View File

@@ -5,18 +5,15 @@ import datetime
from flask import abort
from flask import request
from flask import session
from api.lib.cmdb.ci import CIManager
from api.lib.cmdb.const import PermEnum
from api.lib.cmdb.const import ResourceTypeEnum
from api.lib.cmdb.const import RoleEnum
from api.lib.cmdb.history import AttributeHistoryManger
from api.lib.cmdb.history import CITriggerHistoryManager
from api.lib.cmdb.history import CITypeHistoryManager
from api.lib.cmdb.resp_format import ErrFormat
from api.lib.perm.acl.acl import has_perm_from_args
from api.lib.perm.acl.acl import is_app_admin
from api.lib.perm.acl.acl import role_required
from api.lib.utils import get_page
from api.lib.utils import get_page_size
@@ -79,39 +76,6 @@ class CIHistoryView(APIView):
return self.jsonify(result)
class CITriggerHistoryView(APIView):
url_prefix = ("/history/ci_triggers/<int:ci_id>", "/history/ci_triggers")
@has_perm_from_args("ci_id", ResourceTypeEnum.CI, PermEnum.READ, CIManager.get_type_name)
def get(self, ci_id=None):
if ci_id is not None:
result = CITriggerHistoryManager.get_by_ci_id(ci_id)
return self.jsonify(result)
if RoleEnum.CONFIG not in session.get("acl", {}).get("parentRoles", []) and not is_app_admin("cmdb"):
return abort(403, ErrFormat.role_required.format(RoleEnum.CONFIG))
type_id = request.values.get("type_id")
trigger_id = request.values.get("trigger_id")
operate_type = request.values.get("operate_type")
page = get_page(request.values.get('page', 1))
page_size = get_page_size(request.values.get('page_size', 1))
numfound, result = CITriggerHistoryManager.get(page,
page_size,
type_id=type_id,
trigger_id=trigger_id,
operate_type=operate_type)
return self.jsonify(page=page,
page_size=page_size,
numfound=numfound,
total=len(result),
result=result)
class CITypeHistoryView(APIView):
url_prefix = "/history/ci_types"

View File

@@ -1,37 +0,0 @@
from flask import request
from api.lib.perm.auth import auth_abandoned
from api.lib.secrets.inner import KeyManage
from api.lib.secrets.secrets import InnerKVManger
from api.resource import APIView
class InnerSecretUnSealView(APIView):
url_prefix = "/secrets/unseal"
@auth_abandoned
def post(self):
unseal_key = request.headers.get("Unseal-Token")
res = KeyManage(backend=InnerKVManger()).unseal(unseal_key)
return self.jsonify(**res)
class InnerSecretSealView(APIView):
url_prefix = "/secrets/seal"
@auth_abandoned
def post(self):
unseal_key = request.headers.get("Inner-Token")
res = KeyManage(backend=InnerKVManger()).seal(unseal_key)
return self.jsonify(**res)
class InnerSecretAutoSealView(APIView):
url_prefix = "/secrets/auto_seal"
@auth_abandoned
def post(self):
root_key = request.headers.get("Inner-Token")
res = KeyManage(trigger=root_key,
backend=InnerKVManger()).auto_unseal()
return self.jsonify(**res)

View File

@@ -24,12 +24,12 @@ class DataView(APIView):
class DataViewWithId(APIView):
url_prefix = (f'{prefix}/<string:data_type>/<int:_id>',)
def put(self, _id):
def put(self, data_type, _id):
params = request.json
res = CommonDataCRUD.update_data(_id, **params)
return self.jsonify(res.to_dict())
def delete(self, _id):
def delete(self, data_type, _id):
CommonDataCRUD.delete(_id)
return self.jsonify({})

View File

@@ -1,7 +1,9 @@
# -*- coding:utf-8 -*-
from flask import abort
from flask import request
from api.lib.common_setting.company_info import CompanyInfoCRUD
from api.lib.common_setting.resp_format import ErrFormat
from api.resource import APIView
prefix = '/company'

View File

@@ -1,5 +1,7 @@
# -*- coding:utf-8 -*-
from flask import abort
import os
from flask import abort, current_app, send_from_directory
from flask import request
from werkzeug.datastructures import MultiDict
@@ -143,26 +145,3 @@ class EmployeePositionView(APIView):
result = EmployeeCRUD.get_all_position()
return self.jsonify(result)
class GetEmployeeNoticeByIds(APIView):
url_prefix = (f'{prefix}/get_notice_by_ids',)
def post(self):
employee_ids = request.json.get('employee_ids', [])
if not employee_ids:
result = []
else:
result = EmployeeCRUD.get_employee_notice_by_ids(employee_ids)
return self.jsonify(result)
class EmployeeBindNoticeWithACLID(APIView):
url_prefix = (f'{prefix}/by_uid/bind_notice/<string:platform>/<int:_uid>',)
def put(self, platform, _uid):
data = EmployeeCRUD.bind_notice_by_uid(platform, _uid)
return self.jsonify(info=data)
def delete(self, platform, _uid):
data = EmployeeCRUD.remove_bind_notice_by_uid(platform, _uid)
return self.jsonify(info=data)

View File

@@ -11,7 +11,7 @@ from api.resource import APIView
prefix = '/file'
ALLOWED_EXTENSIONS = {
'txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif', 'xls', 'xlsx', 'doc', 'docx', 'ppt', 'pptx', 'csv', 'svg'
'txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif', 'xls', 'xlsx', 'doc', 'docx', 'ppt', 'pptx', 'csv'
}

View File

@@ -1,79 +0,0 @@
from flask import request, abort, current_app
from werkzeug.datastructures import MultiDict
from api.lib.perm.auth import auth_with_app_token
from api.models.common_setting import NoticeConfig
from api.resource import APIView
from api.lib.common_setting.notice_config import NoticeConfigForm, NoticeConfigUpdateForm, NoticeConfigCRUD
from api.lib.decorator import args_required
from api.lib.common_setting.resp_format import ErrFormat
prefix = '/notice_config'
class NoticeConfigView(APIView):
url_prefix = (f'{prefix}',)
@args_required('platform')
@auth_with_app_token
def get(self):
platform = request.args.get('platform')
res = NoticeConfig.get_by(first=True, to_dict=True, platform=platform) or {}
return self.jsonify(res)
def post(self):
form = NoticeConfigForm(MultiDict(request.json))
if not form.validate():
abort(400, ','.join(['{}: {}'.format(filed, ','.join(msg)) for filed, msg in form.errors.items()]))
data = NoticeConfigCRUD.add_notice_config(**form.data)
return self.jsonify(data.to_dict())
class NoticeConfigUpdateView(APIView):
url_prefix = (f'{prefix}/<int:_id>',)
def put(self, _id):
form = NoticeConfigUpdateForm(MultiDict(request.json))
if not form.validate():
abort(400, ','.join(['{}: {}'.format(filed, ','.join(msg)) for filed, msg in form.errors.items()]))
data = NoticeConfigCRUD.edit_notice_config(_id, **form.data)
return self.jsonify(data.to_dict())
class CheckEmailServer(APIView):
url_prefix = (f'{prefix}/send_test_email',)
def post(self):
receive_address = request.args.get('receive_address')
info = request.values.get('info', {})
try:
result = NoticeConfigCRUD.test_send_email(receive_address, **info)
return self.jsonify(result=result)
except Exception as e:
current_app.logger.error('test_send_email err:')
current_app.logger.error(e)
if 'Timed Out' in str(e):
abort(400, ErrFormat.email_send_timeout)
abort(400, f"{str(e)}")
class NoticeConfigGetView(APIView):
method_decorators = []
url_prefix = (f'{prefix}/all',)
@auth_with_app_token
def get(self):
res = NoticeConfigCRUD.get_all()
return self.jsonify(res)
class NoticeAppBotView(APIView):
url_prefix = (f'{prefix}/app_bot',)
def get(self):
res = NoticeConfigCRUD.get_app_bot()
return self.jsonify(res)

View File

@@ -1 +0,0 @@
Generic single-database configuration.

View File

@@ -1,45 +0,0 @@
# A generic, single database configuration.
[alembic]
# template used to generate migration files
# file_template = %%(rev)s_%%(slug)s
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S

View File

@@ -1,110 +0,0 @@
from __future__ import with_statement
import logging
from logging.config import fileConfig
from sqlalchemy import engine_from_config
from sqlalchemy import pool
from alembic import context
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
fileConfig(config.config_file_name)
logger = logging.getLogger('alembic.env')
# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
from flask import current_app
config.set_main_option(
'sqlalchemy.url', current_app.config.get(
'SQLALCHEMY_DATABASE_URI').replace('%', '%%'))
target_metadata = current_app.extensions['migrate'].db.metadata
# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
# 添加要屏蔽的table列表
exclude_tables = ["c_cfp"]
def run_migrations_offline():
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url, target_metadata=target_metadata, literal_binds=True,
include_name=include_name
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online():
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
# this callback is used to prevent an auto-migration from being generated
# when there are no changes to the schema
# reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
def process_revision_directives(context, revision, directives):
if getattr(config.cmd_opts, 'autogenerate', False):
script = directives[0]
if script.upgrade_ops.is_empty():
directives[:] = []
logger.info('No changes in schema detected.')
connectable = engine_from_config(
config.get_section(config.config_ini_section),
prefix='sqlalchemy.',
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
context.configure(
connection=connection,
target_metadata=target_metadata,
process_revision_directives=process_revision_directives,
include_name=include_name,
**current_app.extensions['migrate'].configure_args
)
with context.begin_transaction():
context.run_migrations()
def include_name(name, type_, parent_names):
if type_ == "table":
return name not in exclude_tables
elif parent_names.get("table_name") in exclude_tables:
return False
return True
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()

View File

@@ -1,24 +0,0 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
branch_labels = ${repr(branch_labels)}
depends_on = ${repr(depends_on)}
def upgrade():
${upgrades if upgrades else "pass"}
def downgrade():
${downgrades if downgrades else "pass"}

View File

@@ -1,360 +0,0 @@
"""empty message
Revision ID: 6a4df2623057
Revises:
Create Date: 2023-10-13 15:17:00.066858
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = '6a4df2623057'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('common_data',
sa.Column('deleted_at', sa.DateTime(), nullable=True),
sa.Column('deleted', sa.Boolean(), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('data_type', sa.VARCHAR(length=255), nullable=True),
sa.Column('data', sa.JSON(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_common_data_deleted'), 'common_data', ['deleted'], unique=False)
op.create_table('common_notice_config',
sa.Column('deleted_at', sa.DateTime(), nullable=True),
sa.Column('deleted', sa.Boolean(), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('platform', sa.VARCHAR(length=255), nullable=False),
sa.Column('info', sa.JSON(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_common_notice_config_deleted'), 'common_notice_config', ['deleted'], unique=False)
op.add_column('c_attributes', sa.Column('choice_other', sa.JSON(), nullable=True))
op.drop_index('idx_c_attributes_uid', table_name='c_attributes')
op.create_index(op.f('ix_c_attributes_uid'), 'c_attributes', ['uid'], unique=False)
op.drop_index('ix_c_custom_dashboard_deleted', table_name='c_c_d')
op.create_index(op.f('ix_c_c_d_deleted'), 'c_c_d', ['deleted'], unique=False)
op.drop_index('ix_c_ci_type_triggers_deleted', table_name='c_c_t_t')
op.create_index(op.f('ix_c_c_t_t_deleted'), 'c_c_t_t', ['deleted'], unique=False)
op.drop_index('ix_c_ci_type_unique_constraints_deleted', table_name='c_c_t_u_c')
op.create_index(op.f('ix_c_c_t_u_c_deleted'), 'c_c_t_u_c', ['deleted'], unique=False)
op.drop_index('c_ci_types_uid', table_name='c_ci_types')
op.create_index(op.f('ix_c_ci_types_uid'), 'c_ci_types', ['uid'], unique=False)
op.alter_column('c_prv', 'uid',
existing_type=mysql.INTEGER(),
nullable=False)
op.drop_index('ix_c_preference_relation_views_deleted', table_name='c_prv')
op.drop_index('ix_c_preference_relation_views_name', table_name='c_prv')
op.create_index(op.f('ix_c_prv_deleted'), 'c_prv', ['deleted'], unique=False)
op.create_index(op.f('ix_c_prv_name'), 'c_prv', ['name'], unique=False)
op.create_index(op.f('ix_c_prv_uid'), 'c_prv', ['uid'], unique=False)
op.drop_index('ix_c_preference_show_attributes_deleted', table_name='c_psa')
op.drop_index('ix_c_preference_show_attributes_uid', table_name='c_psa')
op.create_index(op.f('ix_c_psa_deleted'), 'c_psa', ['deleted'], unique=False)
op.create_index(op.f('ix_c_psa_uid'), 'c_psa', ['uid'], unique=False)
op.drop_index('ix_c_preference_tree_views_deleted', table_name='c_ptv')
op.drop_index('ix_c_preference_tree_views_uid', table_name='c_ptv')
op.create_index(op.f('ix_c_ptv_deleted'), 'c_ptv', ['deleted'], unique=False)
op.create_index(op.f('ix_c_ptv_uid'), 'c_ptv', ['uid'], unique=False)
op.alter_column('common_department', 'department_name',
existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=255),
comment=None,
existing_comment='部门名称',
existing_nullable=True)
op.alter_column('common_department', 'department_director_id',
existing_type=mysql.INTEGER(),
comment=None,
existing_comment='部门负责人ID',
existing_nullable=True)
op.alter_column('common_department', 'department_parent_id',
existing_type=mysql.INTEGER(),
comment=None,
existing_comment='上级部门ID',
existing_nullable=True)
op.alter_column('common_department', 'sort_value',
existing_type=mysql.INTEGER(),
comment=None,
existing_comment='排序值',
existing_nullable=True)
op.alter_column('common_department', 'acl_rid',
existing_type=mysql.INTEGER(),
comment=None,
existing_comment='ACL中rid',
existing_nullable=True)
op.alter_column('common_employee', 'email',
existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=255),
comment=None,
existing_comment='邮箱',
existing_nullable=True)
op.alter_column('common_employee', 'username',
existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=255),
comment=None,
existing_comment='用户名',
existing_nullable=True)
op.alter_column('common_employee', 'nickname',
existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=255),
comment=None,
existing_comment='姓名',
existing_nullable=True)
op.alter_column('common_employee', 'sex',
existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=64),
comment=None,
existing_comment='性别',
existing_nullable=True)
op.alter_column('common_employee', 'position_name',
existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=255),
comment=None,
existing_comment='职位名称',
existing_nullable=True)
op.alter_column('common_employee', 'mobile',
existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=255),
comment=None,
existing_comment='电话号码',
existing_nullable=True)
op.alter_column('common_employee', 'avatar',
existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=255),
comment=None,
existing_comment='头像',
existing_nullable=True)
op.alter_column('common_employee', 'direct_supervisor_id',
existing_type=mysql.INTEGER(),
comment=None,
existing_comment='直接上级ID',
existing_nullable=True)
op.alter_column('common_employee', 'department_id',
existing_type=mysql.INTEGER(),
comment=None,
existing_comment='部门ID',
existing_nullable=True)
op.alter_column('common_employee', 'acl_uid',
existing_type=mysql.INTEGER(),
comment=None,
existing_comment='ACL中uid',
existing_nullable=True)
op.alter_column('common_employee', 'acl_rid',
existing_type=mysql.INTEGER(),
comment=None,
existing_comment='ACL中rid',
existing_nullable=True)
op.alter_column('common_employee', 'acl_virtual_rid',
existing_type=mysql.INTEGER(),
comment=None,
existing_comment='ACL中虚拟角色rid',
existing_nullable=True)
op.alter_column('common_employee', 'last_login',
existing_type=mysql.TIMESTAMP(),
comment=None,
existing_comment='上次登录时间',
existing_nullable=True)
op.alter_column('common_employee', 'block',
existing_type=mysql.INTEGER(),
comment=None,
existing_comment='锁定状态',
existing_nullable=True)
op.alter_column('common_employee_info', 'info',
existing_type=mysql.JSON(),
comment=None,
existing_comment='员工信息',
existing_nullable=True)
op.alter_column('common_employee_info', 'employee_id',
existing_type=mysql.INTEGER(),
comment=None,
existing_comment='员工ID',
existing_nullable=True)
op.alter_column('common_internal_message', 'title',
existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=255),
comment=None,
existing_comment='标题',
existing_nullable=True)
op.alter_column('common_internal_message', 'content',
existing_type=mysql.TEXT(charset='utf8mb3', collation='utf8mb3_unicode_ci'),
comment=None,
existing_comment='内容',
existing_nullable=True)
op.alter_column('common_internal_message', 'path',
existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=255),
comment=None,
existing_comment='跳转路径',
existing_nullable=True)
op.alter_column('common_internal_message', 'is_read',
existing_type=mysql.TINYINT(display_width=1),
comment=None,
existing_comment='是否已读',
existing_nullable=True)
op.alter_column('common_internal_message', 'app_name',
existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=128),
comment=None,
existing_comment='应用名称',
existing_nullable=False)
op.alter_column('common_internal_message', 'category',
existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=128),
comment=None,
existing_comment='分类',
existing_nullable=False)
op.alter_column('common_internal_message', 'message_data',
existing_type=mysql.JSON(),
comment=None,
existing_comment='数据',
existing_nullable=True)
op.drop_column('users', 'apps')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('users', sa.Column('apps', mysql.JSON(), nullable=True))
op.alter_column('common_internal_message', 'message_data',
existing_type=mysql.JSON(),
comment='数据',
existing_nullable=True)
op.alter_column('common_internal_message', 'category',
existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=128),
comment='分类',
existing_nullable=False)
op.alter_column('common_internal_message', 'app_name',
existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=128),
comment='应用名称',
existing_nullable=False)
op.alter_column('common_internal_message', 'is_read',
existing_type=mysql.TINYINT(display_width=1),
comment='是否已读',
existing_nullable=True)
op.alter_column('common_internal_message', 'path',
existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=255),
comment='跳转路径',
existing_nullable=True)
op.alter_column('common_internal_message', 'content',
existing_type=mysql.TEXT(charset='utf8mb3', collation='utf8mb3_unicode_ci'),
comment='内容',
existing_nullable=True)
op.alter_column('common_internal_message', 'title',
existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=255),
comment='标题',
existing_nullable=True)
op.alter_column('common_employee_info', 'employee_id',
existing_type=mysql.INTEGER(),
comment='员工ID',
existing_nullable=True)
op.alter_column('common_employee_info', 'info',
existing_type=mysql.JSON(),
comment='员工信息',
existing_nullable=True)
op.alter_column('common_employee', 'block',
existing_type=mysql.INTEGER(),
comment='锁定状态',
existing_nullable=True)
op.alter_column('common_employee', 'last_login',
existing_type=mysql.TIMESTAMP(),
comment='上次登录时间',
existing_nullable=True)
op.alter_column('common_employee', 'acl_virtual_rid',
existing_type=mysql.INTEGER(),
comment='ACL中虚拟角色rid',
existing_nullable=True)
op.alter_column('common_employee', 'acl_rid',
existing_type=mysql.INTEGER(),
comment='ACL中rid',
existing_nullable=True)
op.alter_column('common_employee', 'acl_uid',
existing_type=mysql.INTEGER(),
comment='ACL中uid',
existing_nullable=True)
op.alter_column('common_employee', 'department_id',
existing_type=mysql.INTEGER(),
comment='部门ID',
existing_nullable=True)
op.alter_column('common_employee', 'direct_supervisor_id',
existing_type=mysql.INTEGER(),
comment='直接上级ID',
existing_nullable=True)
op.alter_column('common_employee', 'avatar',
existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=255),
comment='头像',
existing_nullable=True)
op.alter_column('common_employee', 'mobile',
existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=255),
comment='电话号码',
existing_nullable=True)
op.alter_column('common_employee', 'position_name',
existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=255),
comment='职位名称',
existing_nullable=True)
op.alter_column('common_employee', 'sex',
existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=64),
comment='性别',
existing_nullable=True)
op.alter_column('common_employee', 'nickname',
existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=255),
comment='姓名',
existing_nullable=True)
op.alter_column('common_employee', 'username',
existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=255),
comment='用户名',
existing_nullable=True)
op.alter_column('common_employee', 'email',
existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=255),
comment='邮箱',
existing_nullable=True)
op.alter_column('common_department', 'acl_rid',
existing_type=mysql.INTEGER(),
comment='ACL中rid',
existing_nullable=True)
op.alter_column('common_department', 'sort_value',
existing_type=mysql.INTEGER(),
comment='排序值',
existing_nullable=True)
op.alter_column('common_department', 'department_parent_id',
existing_type=mysql.INTEGER(),
comment='上级部门ID',
existing_nullable=True)
op.alter_column('common_department', 'department_director_id',
existing_type=mysql.INTEGER(),
comment='部门负责人ID',
existing_nullable=True)
op.alter_column('common_department', 'department_name',
existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=255),
comment='部门名称',
existing_nullable=True)
op.drop_index(op.f('ix_c_ptv_uid'), table_name='c_ptv')
op.drop_index(op.f('ix_c_ptv_deleted'), table_name='c_ptv')
op.create_index('ix_c_preference_tree_views_uid', 'c_ptv', ['uid'], unique=False)
op.create_index('ix_c_preference_tree_views_deleted', 'c_ptv', ['deleted'], unique=False)
op.drop_index(op.f('ix_c_psa_uid'), table_name='c_psa')
op.drop_index(op.f('ix_c_psa_deleted'), table_name='c_psa')
op.create_index('ix_c_preference_show_attributes_uid', 'c_psa', ['uid'], unique=False)
op.create_index('ix_c_preference_show_attributes_deleted', 'c_psa', ['deleted'], unique=False)
op.drop_index(op.f('ix_c_prv_uid'), table_name='c_prv')
op.drop_index(op.f('ix_c_prv_name'), table_name='c_prv')
op.drop_index(op.f('ix_c_prv_deleted'), table_name='c_prv')
op.create_index('ix_c_preference_relation_views_name', 'c_prv', ['name'], unique=False)
op.create_index('ix_c_preference_relation_views_deleted', 'c_prv', ['deleted'], unique=False)
op.alter_column('c_prv', 'uid',
existing_type=mysql.INTEGER(),
nullable=True)
op.drop_index(op.f('ix_c_ci_types_uid'), table_name='c_ci_types')
op.create_index('c_ci_types_uid', 'c_ci_types', ['uid'], unique=False)
op.drop_index(op.f('ix_c_c_t_u_c_deleted'), table_name='c_c_t_u_c')
op.create_index('ix_c_ci_type_unique_constraints_deleted', 'c_c_t_u_c', ['deleted'], unique=False)
op.drop_index(op.f('ix_c_c_t_t_deleted'), table_name='c_c_t_t')
op.create_index('ix_c_ci_type_triggers_deleted', 'c_c_t_t', ['deleted'], unique=False)
op.drop_index(op.f('ix_c_c_d_deleted'), table_name='c_c_d')
op.create_index('ix_c_custom_dashboard_deleted', 'c_c_d', ['deleted'], unique=False)
op.drop_index(op.f('ix_c_attributes_uid'), table_name='c_attributes')
op.create_index('idx_c_attributes_uid', 'c_attributes', ['uid'], unique=False)
op.drop_column('c_attributes', 'choice_other')
op.drop_index(op.f('ix_common_notice_config_deleted'), table_name='common_notice_config')
op.drop_table('common_notice_config')
op.drop_index(op.f('ix_common_data_deleted'), table_name='common_data')
op.drop_table('common_data')
# ### end Alembic commands ###

View File

@@ -1,7 +1,7 @@
-i https://mirrors.aliyun.com/pypi/simple
alembic==1.7.7
bs4==0.0.1
celery>=5.3.1
celery==5.3.1
celery-once==3.0.1
click==8.1.3
elasticsearch==7.17.9
@@ -12,42 +12,35 @@ Flask==2.3.2
Flask-Bcrypt==1.0.1
Flask-Caching==2.0.2
Flask-Cors==4.0.0
Flask-Login>=0.6.2
Flask-Login==0.6.2
Flask-Migrate==2.5.2
Flask-RESTful==0.3.10
Flask-SQLAlchemy==2.5.0
future==0.18.3
gunicorn==21.0.1
hvac==2.0.0
itsdangerous==2.1.2
Jinja2==3.1.2
jinja2schema==0.1.4
jsonschema==4.18.0
kombu>=5.3.1
kombu==5.3.1
Mako==1.2.4
MarkupSafe==2.1.3
marshmallow==2.20.2
more-itertools==5.0.0
msgpack-python==0.5.6
Pillow>=10.0.1
cryptography>=41.0.2
Pillow==9.3.0
pycryptodome==3.12.0
PyJWT==2.4.0
PyMySQL==1.1.0
ldap3==2.9.1
python-ldap==3.4.0
PyYAML==6.0
redis==4.6.0
requests==2.31.0
requests_oauthlib==1.3.1
markdownify==0.11.6
six==1.16.0
six==1.12.0
SQLAlchemy==1.4.49
supervisor==4.0.3
timeout-decorator==0.5.0
toposort==1.10
treelib==1.6.1
Werkzeug>=2.3.6
WTForms==3.0.0
shamir~=17.12.0
hvac~=2.0.0
pycryptodomex>=3.19.0
colorama>=0.4.6
Werkzeug==2.3.6
WTForms==3.0.0

View File

@@ -94,12 +94,3 @@ ES_HOST = '127.0.0.1'
USE_ES = False
BOOL_TRUE = ['true', 'TRUE', 'True', True, '1', 1, "Yes", "YES", "yes", 'Y', 'y']
# # messenger
USE_MESSENGER = True
# # secrets
SECRETS_ENGINE = 'inner' # 'inner' or 'vault'
VAULT_URL = ''
VAULT_TOKEN = ''
INNER_TRIGGER_TOKEN = ''

View File

@@ -17,8 +17,6 @@
"@babel/plugin-syntax-import-meta": "^7.10.4",
"@riophae/vue-treeselect": "^0.4.0",
"@vue/composition-api": "^1.7.1",
"@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^1.0.0",
"ant-design-vue": "^1.6.5",
"axios": "0.18.0",
"babel-eslint": "^8.2.2",
@@ -39,7 +37,6 @@
"moment": "^2.24.0",
"nprogress": "^0.2.0",
"relation-graph": "^1.1.0",
"snabbdom": "^3.5.1",
"sortablejs": "1.9.0",
"viser-vue": "^2.4.8",
"vue": "2.6.11",
@@ -63,15 +60,14 @@
},
"devDependencies": {
"@ant-design/colors": "^3.2.2",
"@babel/core": "^7.23.2",
"@babel/polyfill": "^7.2.5",
"@babel/preset-env": "^7.23.2",
"@vue/cli-plugin-babel": "4.5.17",
"@vue/cli-plugin-eslint": "^4.0.5",
"@vue/cli-plugin-unit-jest": "^4.0.5",
"@vue/cli-service": "^4.0.5",
"@vue/eslint-config-standard": "^4.0.0",
"@vue/test-utils": "^1.0.0-beta.30",
"babel-core": "7.0.0-bridge.0",
"babel-jest": "^23.6.0",
"babel-plugin-import": "^1.11.0",
"babel-plugin-transform-remove-console": "^6.9.4",

View File

@@ -54,90 +54,6 @@
<div class="content unicode" style="display: block;">
<ul class="icon_lists dib-box">
<li class="dib">
<span class="icon iconfont">&#xe894;</span>
<div class="name">icon-xianxing-password</div>
<div class="code-name">&amp;#xe894;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe895;</span>
<div class="name">icon-xianxing-link</div>
<div class="code-name">&amp;#xe895;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe892;</span>
<div class="name">itsm-oneclick download</div>
<div class="code-name">&amp;#xe892;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe893;</span>
<div class="name">itsm-package download</div>
<div class="code-name">&amp;#xe893;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe891;</span>
<div class="name">weixin</div>
<div class="code-name">&amp;#xe891;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe88f;</span>
<div class="name">itsm-again</div>
<div class="code-name">&amp;#xe88f;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe890;</span>
<div class="name">itsm-next</div>
<div class="code-name">&amp;#xe890;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe88e;</span>
<div class="name">wechatApp</div>
<div class="code-name">&amp;#xe88e;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe88b;</span>
<div class="name">robot</div>
<div class="code-name">&amp;#xe88b;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe88c;</span>
<div class="name">feishuApp</div>
<div class="code-name">&amp;#xe88c;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe88d;</span>
<div class="name">dingdingApp</div>
<div class="code-name">&amp;#xe88d;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe88a;</span>
<div class="name">email</div>
<div class="code-name">&amp;#xe88a;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe887;</span>
<div class="name">setting-feishu</div>
<div class="code-name">&amp;#xe887;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe888;</span>
<div class="name">setting-feishu-selected</div>
<div class="code-name">&amp;#xe888;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe886;</span>
<div class="name">cmdb-histogram</div>
@@ -2184,12 +2100,6 @@
<div class="code-name">&amp;#xe738;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe889;</span>
<div class="name">ops-setting-notice-email-selected</div>
<div class="code-name">&amp;#xe889;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe72f;</span>
<div class="name">ops-setting-notice</div>
@@ -4044,9 +3954,9 @@
<pre><code class="language-css"
>@font-face {
font-family: 'iconfont';
src: url('iconfont.woff2?t=1698273699449') format('woff2'),
url('iconfont.woff?t=1698273699449') format('woff'),
url('iconfont.ttf?t=1698273699449') format('truetype');
src: url('iconfont.woff2?t=1694508259411') format('woff2'),
url('iconfont.woff?t=1694508259411') format('woff'),
url('iconfont.ttf?t=1694508259411') format('truetype');
}
</code></pre>
<h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3>
@@ -4072,132 +3982,6 @@
<div class="content font-class">
<ul class="icon_lists dib-box">
<li class="dib">
<span class="icon iconfont icon-xianxing-password"></span>
<div class="name">
icon-xianxing-password
</div>
<div class="code-name">.icon-xianxing-password
</div>
</li>
<li class="dib">
<span class="icon iconfont icon-xianxing-link"></span>
<div class="name">
icon-xianxing-link
</div>
<div class="code-name">.icon-xianxing-link
</div>
</li>
<li class="dib">
<span class="icon iconfont a-itsm-oneclickdownload"></span>
<div class="name">
itsm-oneclick download
</div>
<div class="code-name">.a-itsm-oneclickdownload
</div>
</li>
<li class="dib">
<span class="icon iconfont a-itsm-packagedownload"></span>
<div class="name">
itsm-package download
</div>
<div class="code-name">.a-itsm-packagedownload
</div>
</li>
<li class="dib">
<span class="icon iconfont a-Frame4"></span>
<div class="name">
weixin
</div>
<div class="code-name">.a-Frame4
</div>
</li>
<li class="dib">
<span class="icon iconfont itsm-again"></span>
<div class="name">
itsm-again
</div>
<div class="code-name">.itsm-again
</div>
</li>
<li class="dib">
<span class="icon iconfont itsm-next"></span>
<div class="name">
itsm-next
</div>
<div class="code-name">.itsm-next
</div>
</li>
<li class="dib">
<span class="icon iconfont wechatApp"></span>
<div class="name">
wechatApp
</div>
<div class="code-name">.wechatApp
</div>
</li>
<li class="dib">
<span class="icon iconfont robot"></span>
<div class="name">
robot
</div>
<div class="code-name">.robot
</div>
</li>
<li class="dib">
<span class="icon iconfont feishuApp"></span>
<div class="name">
feishuApp
</div>
<div class="code-name">.feishuApp
</div>
</li>
<li class="dib">
<span class="icon iconfont dingdingApp"></span>
<div class="name">
dingdingApp
</div>
<div class="code-name">.dingdingApp
</div>
</li>
<li class="dib">
<span class="icon iconfont email"></span>
<div class="name">
email
</div>
<div class="code-name">.email
</div>
</li>
<li class="dib">
<span class="icon iconfont ops-setting-notice-feishu"></span>
<div class="name">
setting-feishu
</div>
<div class="code-name">.ops-setting-notice-feishu
</div>
</li>
<li class="dib">
<span class="icon iconfont ops-setting-notice-feishu-selected"></span>
<div class="name">
setting-feishu-selected
</div>
<div class="code-name">.ops-setting-notice-feishu-selected
</div>
</li>
<li class="dib">
<span class="icon iconfont cmdb-bar"></span>
<div class="name">
@@ -7267,15 +7051,6 @@
</div>
</li>
<li class="dib">
<span class="icon iconfont ops-setting-notice-email-selected-copy"></span>
<div class="name">
ops-setting-notice-email-selected
</div>
<div class="code-name">.ops-setting-notice-email-selected-copy
</div>
</li>
<li class="dib">
<span class="icon iconfont ops-setting-notice"></span>
<div class="name">
@@ -10057,118 +9832,6 @@
<div class="content symbol">
<ul class="icon_lists dib-box">
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#icon-xianxing-password"></use>
</svg>
<div class="name">icon-xianxing-password</div>
<div class="code-name">#icon-xianxing-password</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#icon-xianxing-link"></use>
</svg>
<div class="name">icon-xianxing-link</div>
<div class="code-name">#icon-xianxing-link</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#a-itsm-oneclickdownload"></use>
</svg>
<div class="name">itsm-oneclick download</div>
<div class="code-name">#a-itsm-oneclickdownload</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#a-itsm-packagedownload"></use>
</svg>
<div class="name">itsm-package download</div>
<div class="code-name">#a-itsm-packagedownload</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#a-Frame4"></use>
</svg>
<div class="name">weixin</div>
<div class="code-name">#a-Frame4</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#itsm-again"></use>
</svg>
<div class="name">itsm-again</div>
<div class="code-name">#itsm-again</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#itsm-next"></use>
</svg>
<div class="name">itsm-next</div>
<div class="code-name">#itsm-next</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#wechatApp"></use>
</svg>
<div class="name">wechatApp</div>
<div class="code-name">#wechatApp</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#robot"></use>
</svg>
<div class="name">robot</div>
<div class="code-name">#robot</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#feishuApp"></use>
</svg>
<div class="name">feishuApp</div>
<div class="code-name">#feishuApp</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#dingdingApp"></use>
</svg>
<div class="name">dingdingApp</div>
<div class="code-name">#dingdingApp</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#email"></use>
</svg>
<div class="name">email</div>
<div class="code-name">#email</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#ops-setting-notice-feishu"></use>
</svg>
<div class="name">setting-feishu</div>
<div class="code-name">#ops-setting-notice-feishu</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#ops-setting-notice-feishu-selected"></use>
</svg>
<div class="name">setting-feishu-selected</div>
<div class="code-name">#ops-setting-notice-feishu-selected</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#cmdb-bar"></use>
@@ -12897,14 +12560,6 @@
<div class="code-name">#ops-dot</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#ops-setting-notice-email-selected-copy"></use>
</svg>
<div class="name">ops-setting-notice-email-selected</div>
<div class="code-name">#ops-setting-notice-email-selected-copy</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#ops-setting-notice"></use>

View File

@@ -1,8 +1,8 @@
@font-face {
font-family: "iconfont"; /* Project id 3857903 */
src: url('iconfont.woff2?t=1698273699449') format('woff2'),
url('iconfont.woff?t=1698273699449') format('woff'),
url('iconfont.ttf?t=1698273699449') format('truetype');
src: url('iconfont.woff2?t=1694508259411') format('woff2'),
url('iconfont.woff?t=1694508259411') format('woff'),
url('iconfont.ttf?t=1694508259411') format('truetype');
}
.iconfont {
@@ -13,62 +13,6 @@
-moz-osx-font-smoothing: grayscale;
}
.icon-xianxing-password:before {
content: "\e894";
}
.icon-xianxing-link:before {
content: "\e895";
}
.a-itsm-oneclickdownload:before {
content: "\e892";
}
.a-itsm-packagedownload:before {
content: "\e893";
}
.a-Frame4:before {
content: "\e891";
}
.itsm-again:before {
content: "\e88f";
}
.itsm-next:before {
content: "\e890";
}
.wechatApp:before {
content: "\e88e";
}
.robot:before {
content: "\e88b";
}
.feishuApp:before {
content: "\e88c";
}
.dingdingApp:before {
content: "\e88d";
}
.email:before {
content: "\e88a";
}
.ops-setting-notice-feishu:before {
content: "\e887";
}
.ops-setting-notice-feishu-selected:before {
content: "\e888";
}
.cmdb-bar:before {
content: "\e886";
}
@@ -1433,10 +1377,6 @@
content: "\e738";
}
.ops-setting-notice-email-selected-copy:before {
content: "\e889";
}
.ops-setting-notice:before {
content: "\e72f";
}

File diff suppressed because one or more lines are too long

View File

@@ -5,104 +5,6 @@
"css_prefix_text": "",
"description": "",
"glyphs": [
{
"icon_id": "37830610",
"name": "icon-xianxing-password",
"font_class": "icon-xianxing-password",
"unicode": "e894",
"unicode_decimal": 59540
},
{
"icon_id": "37830609",
"name": "icon-xianxing-link",
"font_class": "icon-xianxing-link",
"unicode": "e895",
"unicode_decimal": 59541
},
{
"icon_id": "37822199",
"name": "itsm-oneclick download",
"font_class": "a-itsm-oneclickdownload",
"unicode": "e892",
"unicode_decimal": 59538
},
{
"icon_id": "37822198",
"name": "itsm-package download",
"font_class": "a-itsm-packagedownload",
"unicode": "e893",
"unicode_decimal": 59539
},
{
"icon_id": "37772067",
"name": "weixin",
"font_class": "a-Frame4",
"unicode": "e891",
"unicode_decimal": 59537
},
{
"icon_id": "37632784",
"name": "itsm-again",
"font_class": "itsm-again",
"unicode": "e88f",
"unicode_decimal": 59535
},
{
"icon_id": "37632783",
"name": "itsm-next",
"font_class": "itsm-next",
"unicode": "e890",
"unicode_decimal": 59536
},
{
"icon_id": "37590786",
"name": "wechatApp",
"font_class": "wechatApp",
"unicode": "e88e",
"unicode_decimal": 59534
},
{
"icon_id": "37590798",
"name": "robot",
"font_class": "robot",
"unicode": "e88b",
"unicode_decimal": 59531
},
{
"icon_id": "37590794",
"name": "feishuApp",
"font_class": "feishuApp",
"unicode": "e88c",
"unicode_decimal": 59532
},
{
"icon_id": "37590791",
"name": "dingdingApp",
"font_class": "dingdingApp",
"unicode": "e88d",
"unicode_decimal": 59533
},
{
"icon_id": "37590776",
"name": "email",
"font_class": "email",
"unicode": "e88a",
"unicode_decimal": 59530
},
{
"icon_id": "37537876",
"name": "setting-feishu",
"font_class": "ops-setting-notice-feishu",
"unicode": "e887",
"unicode_decimal": 59527
},
{
"icon_id": "37537859",
"name": "setting-feishu-selected",
"font_class": "ops-setting-notice-feishu-selected",
"unicode": "e888",
"unicode_decimal": 59528
},
{
"icon_id": "37334642",
"name": "cmdb-histogram",
@@ -2490,13 +2392,6 @@
"unicode": "e738",
"unicode_decimal": 59192
},
{
"icon_id": "37575490",
"name": "ops-setting-notice-email-selected",
"font_class": "ops-setting-notice-email-selected-copy",
"unicode": "e889",
"unicode_decimal": 59529
},
{
"icon_id": "34108346",
"name": "ops-setting-notice",

Binary file not shown.

View File

@@ -12,9 +12,6 @@ import zhCN from 'ant-design-vue/lib/locale-provider/zh_CN'
import { AppDeviceEnquire } from '@/utils/mixin'
import { debounce } from './utils/util'
import { h } from 'snabbdom'
import { DomEditor, Boot } from '@wangeditor/editor'
export default {
mixins: [AppDeviceEnquire],
provide() {
@@ -50,134 +47,6 @@ export default {
this.$store.dispatch('setWindowSize')
})
)
// 注册富文本自定义元素
const resume = {
type: 'attachment',
attachmentLabel: '',
attachmentValue: '',
children: [{ text: '' }], // void 元素必须有一个 children 其中只有一个空字符串重要
}
function withAttachment(editor) {
// JS 语法
const { isInline, isVoid } = editor
const newEditor = editor
newEditor.isInline = (elem) => {
const type = DomEditor.getNodeType(elem)
if (type === 'attachment') return true // 针对 type: attachment 设置为 inline
return isInline(elem)
}
newEditor.isVoid = (elem) => {
const type = DomEditor.getNodeType(elem)
if (type === 'attachment') return true // 针对 type: attachment 设置为 void
return isVoid(elem)
}
return newEditor // 返回 newEditor 重要
}
Boot.registerPlugin(withAttachment)
/**
* 渲染附件元素到编辑器
* @param elem 附件元素即上文的 myResume
* @param children 元素子节点void 元素可忽略
* @param editor 编辑器实例
* @returns vnode 节点通过 snabbdom.js h 函数生成
*/
function renderAttachment(elem, children, editor) {
// JS 语法
// 获取附件的数据参考上文 myResume 数据结构
const { attachmentLabel = '', attachmentValue = '' } = elem
// 附件元素 vnode
const attachVnode = h(
// HTML tag
'span',
// HTML 属性样式事件
{
props: { contentEditable: false }, // HTML 属性驼峰式写法
style: {
display: 'inline-block',
margin: '0 3px',
padding: '0 3px',
backgroundColor: '#e6f7ff',
border: '1px solid #91d5ff',
borderRadius: '2px',
color: '#1890ff',
}, // style 驼峰式写法
on: {
click() {
console.log('clicked', attachmentValue)
} /* 其他... */,
},
},
// 子节点
[attachmentLabel]
)
return attachVnode
}
const renderElemConf = {
type: 'attachment', // 新元素 type 重要
renderElem: renderAttachment,
}
Boot.registerRenderElem(renderElemConf)
/**
* 生成附件元素的 HTML
* @param elem 附件元素即上文的 myResume
* @param childrenHtml 子节点的 HTML 代码void 元素可忽略
* @returns 附件元素的 HTML 字符串
*/
function attachmentToHtml(elem, childrenHtml) {
// JS 语法
// 获取附件元素的数据
const { attachmentValue = '', attachmentLabel = '' } = elem
// 生成 HTML 代码
const html = `<span data-w-e-type="attachment" data-w-e-is-void data-w-e-is-inline data-attachmentValue="${attachmentValue}" data-attachmentLabel="${attachmentLabel}">${attachmentLabel}</span>`
return html
}
const elemToHtmlConf = {
type: 'attachment', // 新元素的 type 重要
elemToHtml: attachmentToHtml,
}
Boot.registerElemToHtml(elemToHtmlConf)
/**
* 解析 HTML 字符串生成附件元素
* @param domElem HTML 对应的 DOM Element
* @param children 子节点
* @param editor editor 实例
* @returns 附件元素如上文的 myResume
*/
function parseAttachmentHtml(domElem, children, editor) {
// JS 语法
// DOM element 中获取附件的信息
const attachmentValue = domElem.getAttribute('data-attachmentValue') || ''
const attachmentLabel = domElem.getAttribute('data-attachmentLabel') || ''
// 生成附件元素按照此前约定的数据结构
const myResume = {
type: 'attachment',
attachmentValue,
attachmentLabel,
children: [{ text: '' }], // void node 必须有 children 其中有一个空字符串重要
}
return myResume
}
const parseHtmlConf = {
selector: 'span[data-w-e-type="attachment"]', // CSS 选择器匹配特定的 HTML 标签
parseElemHtml: parseAttachmentHtml,
}
Boot.registerParseElemHtml(parseHtmlConf)
},
beforeDestroy() {
clearInterval(this.timer)

View File

@@ -1,134 +1,119 @@
import { axios } from '@/utils/request'
export function getEmployeeList(params) {
return axios({
url: '/common-setting/v1/employee',
method: 'get',
params: params,
})
}
// export function getEmployeeList(params, orderBy) {
// return axios({
// url: '/common-setting/v1/employee' + '/' + orderBy,
// method: 'get',
// params: params,
// })
// }
export function postEmployee(data) {
return axios({
url: '/common-setting/v1/employee',
method: 'post',
data: data,
})
}
export function getEmployeeCount(params) {
return axios({
url: '/common-setting/v1/employee/count',
method: 'get',
params: params,
})
}
export function deleteEmployee(_id) {
return axios({
url: `/common-setting/v1/employee/${_id}`,
method: 'delete',
})
}
export function putEmployee(_id, data) {
return axios({
url: `/common-setting/v1/employee/${_id}`,
method: 'put',
data: data,
})
}
export function batchEditEmployee(data) {
return axios({
url: '/common-setting/v1/employee/batch',
method: 'post',
data: data,
})
}
export function importEmployee(data) {
return axios({
url: '/common-setting/v1/employee/import',
method: 'post',
data
})
}
export function getEmployeeByUid(uid) {
return axios({
url: `/common-setting/v1/employee/by_uid/${uid}`,
method: 'get',
})
}
export function updateEmployeeByUid(uid, data) {
return axios({
url: `/common-setting/v1/employee/by_uid/${uid}`,
method: 'put',
data
})
}
export function updatePasswordByUid(uid, data) {
return axios({
url: `/common-setting/v1/employee/by_uid/change_password/${uid}`,
method: 'put',
data
})
}
export function bindPlatformByUid(platform, uid) {
return axios({
url: `/common-setting/v1/employee/by_uid/bind_notice/${platform}/${uid}`,
method: 'put',
})
}
export function unbindPlatformByUid(platform, uid) {
return axios({
url: `/common-setting/v1/employee/by_uid/bind_notice/${platform}/${uid}`,
method: 'delete',
})
}
export function getAllPosition() {
return axios({
url: `/common-setting/v1/employee/position`,
method: 'get',
})
}
export function getEmployeeByEmployeeId(employee_id) {
return axios({
url: `/common-setting/v1/employee/${employee_id}`,
method: 'get',
})
}
// 下载员工列表
export function downloadAllEmployee(params) {
return axios({
url: `/common-setting/v1/employee/export_all`,
method: 'get',
params,
responseType: 'blob'
})
}
export function getEmployeeListByFilter(data) {
return axios({
url: '/common-setting/v1/employee/filter',
method: 'post',
data
})
}
export function getNoticeByEmployeeIds(data) {
return axios({
url: '/common-setting/v1/employee/get_notice_by_ids',
method: 'post',
data
})
}
import { axios } from '@/utils/request'
export function getEmployeeList(params) {
return axios({
url: '/common-setting/v1/employee',
method: 'get',
params: params,
})
}
// export function getEmployeeList(params, orderBy) {
// return axios({
// url: '/common-setting/v1/employee' + '/' + orderBy,
// method: 'get',
// params: params,
// })
// }
export function postEmployee(data) {
return axios({
url: '/common-setting/v1/employee',
method: 'post',
data: data,
})
}
export function getEmployeeCount(params) {
return axios({
url: '/common-setting/v1/employee/count',
method: 'get',
params: params,
})
}
export function deleteEmployee(_id) {
return axios({
url: `/common-setting/v1/employee/${_id}`,
method: 'delete',
})
}
export function putEmployee(_id, data) {
return axios({
url: `/common-setting/v1/employee/${_id}`,
method: 'put',
data: data,
})
}
export function batchEditEmployee(data) {
return axios({
url: '/common-setting/v1/employee/batch',
method: 'post',
data: data,
})
}
export function importEmployee(data) {
return axios({
url: '/common-setting/v1/employee/import',
method: 'post',
data
})
}
export function getEmployeeByUid(uid) {
return axios({
url: `/common-setting/v1/employee/by_uid/${uid}`,
method: 'get',
})
}
export function updateEmployeeByUid(uid, data) {
return axios({
url: `/common-setting/v1/employee/by_uid/${uid}`,
method: 'put',
data
})
}
export function updatePasswordByUid(uid, data) {
return axios({
url: `/common-setting/v1/employee/by_uid/change_password/${uid}`,
method: 'put',
data
})
}
export function bindWxByUid(uid) {
return axios({
url: `/common-setting/v1/employee/by_uid/bind_work_wechat/${uid}`,
method: 'put',
})
}
export function getAllPosition() {
return axios({
url: `/common-setting/v1/employee/position`,
method: 'get',
})
}
export function getEmployeeByEmployeeId(employee_id) {
return axios({
url: `/common-setting/v1/employee/${employee_id}`,
method: 'get',
})
}
// 下载员工列表
export function downloadAllEmployee(params) {
return axios({
url: `/common-setting/v1/employee/export_all`,
method: 'get',
params,
responseType: 'blob'
})
}
export function getEmployeeListByFilter(data) {
return axios({
url: '/common-setting/v1/employee/filter',
method: 'post',
data
})
}

View File

@@ -1,40 +0,0 @@
import { axios } from '@/utils/request'
export function sendTestEmail(receive_address, data) {
return axios({
url: `/common-setting/v1/notice_config/send_test_email?receive_address=${receive_address}`,
method: 'post',
data
})
}
export const getNoticeConfigByPlatform = (platform) => {
return axios({
url: '/common-setting/v1/notice_config',
method: 'get',
params: { ...platform },
})
}
export const postNoticeConfigByPlatform = (data) => {
return axios({
url: '/common-setting/v1/notice_config',
method: 'post',
data
})
}
export const putNoticeConfigByPlatform = (id, info) => {
return axios({
url: `/common-setting/v1/notice_config/${id}`,
method: 'put',
data: info
})
}
export const getNoticeConfigAppBot = () => {
return axios({
url: `/common-setting/v1/notice_config/app_bot`,
method: 'get',
})
}

View File

@@ -1,293 +1,285 @@
<template>
<div>
<a-space :style="{ display: 'flex', marginBottom: '10px' }" v-for="(item, index) in ruleList" :key="item.id">
<div :style="{ width: '50px', height: '24px', position: 'relative' }">
<treeselect
v-if="index"
class="custom-treeselect"
:style="{ width: '50px', '--custom-height': '24px', position: 'absolute', top: '-17px', left: 0 }"
v-model="item.type"
:multiple="false"
:clearable="false"
searchable
:options="ruleTypeList"
:normalizer="
(node) => {
return {
id: node.value,
label: node.label,
children: node.children,
}
}
"
>
</treeselect>
</div>
<treeselect
class="custom-treeselect"
:style="{ width: '130px', '--custom-height': '24px' }"
v-model="item.property"
:multiple="false"
:clearable="false"
searchable
:options="canSearchPreferenceAttrList"
:normalizer="
(node) => {
return {
id: node.name,
label: node.alias || node.name,
children: node.children,
}
}
"
appendToBody
:zIndex="1050"
>
<div
:title="node.label"
slot="option-label"
slot-scope="{ node }"
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
>
<ValueTypeMapIcon :attr="node.raw" />
{{ node.label }}
</div>
<div
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
slot="value-label"
slot-scope="{ node }"
>
<ValueTypeMapIcon :attr="node.raw" /> {{ node.label }}
</div>
</treeselect>
<treeselect
class="custom-treeselect"
:style="{ width: '100px', '--custom-height': '24px' }"
v-model="item.exp"
:multiple="false"
:clearable="false"
searchable
:options="[...getExpListByProperty(item.property), ...advancedExpList]"
:normalizer="
(node) => {
return {
id: node.value,
label: node.label,
children: node.children,
}
}
"
@select="(value) => handleChangeExp(value, item, index)"
appendToBody
:zIndex="1050"
>
</treeselect>
<treeselect
class="custom-treeselect"
:style="{ width: '175px', '--custom-height': '24px' }"
v-model="item.value"
:multiple="false"
:clearable="false"
searchable
v-if="isChoiceByProperty(item.property) && (item.exp === 'is' || item.exp === '~is')"
:options="getChoiceValueByProperty(item.property)"
placeholder="请选择"
:normalizer="
(node) => {
return {
id: node[0],
label: node[0],
children: node.children,
}
}
"
appendToBody
:zIndex="1050"
>
<div
:title="node.label"
slot="option-label"
slot-scope="{ node }"
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
>
{{ node.label }}
</div>
</treeselect>
<a-input-group
size="small"
compact
v-else-if="item.exp === 'range' || item.exp === '~range'"
:style="{ width: '175px' }"
>
<a-input class="ops-input" size="small" v-model="item.min" :style="{ width: '78px' }" placeholder="最小值" />
~
<a-input class="ops-input" size="small" v-model="item.max" :style="{ width: '78px' }" placeholder="最大值" />
</a-input-group>
<a-input-group size="small" compact v-else-if="item.exp === 'compare'" :style="{ width: '175px' }">
<treeselect
class="custom-treeselect"
:style="{ width: '60px', '--custom-height': '24px' }"
v-model="item.compareType"
:multiple="false"
:clearable="false"
searchable
:options="compareTypeList"
:normalizer="
(node) => {
return {
id: node.value,
label: node.label,
children: node.children,
}
}
"
appendToBody
:zIndex="1050"
>
</treeselect>
<a-input class="ops-input" v-model="item.value" size="small" style="width: 113px" />
</a-input-group>
<a-input
v-else-if="item.exp !== 'value' && item.exp !== '~value'"
size="small"
v-model="item.value"
:placeholder="item.exp === 'in' || item.exp === '~in' ? '以 ; 分隔' : ''"
class="ops-input"
></a-input>
<div v-else :style="{ width: '175px' }"></div>
<a-tooltip title="复制">
<a class="operation" @click="handleCopyRule(item)"><ops-icon type="icon-xianxing-copy"/></a>
</a-tooltip>
<a-tooltip title="删除">
<a class="operation" @click="handleDeleteRule(item)"><ops-icon type="icon-xianxing-delete"/></a>
</a-tooltip>
</a-space>
<div class="table-filter-add">
<a @click="handleAddRule">+ 新增</a>
</div>
</div>
</template>
<script>
import _ from 'lodash'
import { v4 as uuidv4 } from 'uuid'
import { ruleTypeList, expList, advancedExpList, compareTypeList } from './constants'
import ValueTypeMapIcon from '../CMDBValueTypeMapIcon'
export default {
name: 'Expression',
components: { ValueTypeMapIcon },
model: {
prop: 'value',
event: 'change',
},
props: {
value: {
type: Array,
default: () => [],
},
canSearchPreferenceAttrList: {
type: Array,
required: true,
default: () => [],
},
},
data() {
return {
ruleTypeList,
expList,
advancedExpList,
compareTypeList,
}
},
computed: {
ruleList: {
get() {
return this.value
},
set(val) {
this.$emit('change', val)
return val
},
},
},
methods: {
getExpListByProperty(property) {
if (property) {
const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property)
if (_find && ['0', '1', '3', '4', '5'].includes(_find.value_type)) {
return [
{ value: 'is', label: '等于' },
{ value: '~is', label: '不等于' },
{ value: '~value', label: '为空' }, // 为空的定义有点绕
{ value: 'value', label: '不为空' },
]
}
return this.expList
}
return this.expList
},
isChoiceByProperty(property) {
const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property)
if (_find) {
return _find.is_choice
}
return false
},
handleAddRule() {
this.ruleList.push({
id: uuidv4(),
type: 'and',
property: this.canSearchPreferenceAttrList[0]?.name,
exp: 'is',
value: null,
})
this.$emit('change', this.ruleList)
},
handleCopyRule(item) {
this.ruleList.push({ ...item, id: uuidv4() })
this.$emit('change', this.ruleList)
},
handleDeleteRule(item) {
const idx = this.ruleList.findIndex((r) => r.id === item.id)
if (idx > -1) {
this.ruleList.splice(idx, 1)
}
this.$emit('change', this.ruleList)
},
getChoiceValueByProperty(property) {
const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property)
if (_find) {
return _find.choice_value
}
return []
},
handleChangeExp({ value }, item, index) {
const _ruleList = _.cloneDeep(this.ruleList)
if (value === 'range') {
_ruleList[index] = {
..._ruleList[index],
min: '',
max: '',
exp: value,
}
} else if (value === 'compare') {
_ruleList[index] = {
..._ruleList[index],
compareType: '1',
exp: value,
}
} else {
_ruleList[index] = {
..._ruleList[index],
exp: value,
}
}
this.ruleList = _ruleList
this.$emit('change', this.ruleList)
},
},
}
</script>
<style></style>
<template>
<div>
<a-space :style="{ display: 'flex', marginBottom: '10px' }" v-for="(item, index) in ruleList" :key="item.id">
<div :style="{ width: '50px', height: '24px', position: 'relative' }">
<treeselect
v-if="index"
class="custom-treeselect"
:style="{ width: '50px', '--custom-height': '24px', position: 'absolute', top: '-17px', left: 0 }"
v-model="item.type"
:multiple="false"
:clearable="false"
searchable
:options="ruleTypeList"
:normalizer="
(node) => {
return {
id: node.value,
label: node.label,
children: node.children,
}
}
"
>
</treeselect>
</div>
<treeselect
class="custom-treeselect"
:style="{ width: '130px', '--custom-height': '24px' }"
v-model="item.property"
:multiple="false"
:clearable="false"
searchable
:options="canSearchPreferenceAttrList"
:normalizer="
(node) => {
return {
id: node.name,
label: node.alias || node.name,
children: node.children,
}
}
"
>
<div
:title="node.label"
slot="option-label"
slot-scope="{ node }"
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
>
<ValueTypeMapIcon :attr="node.raw" />
{{ node.label }}
</div>
<div
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
slot="value-label"
slot-scope="{ node }"
>
<ValueTypeMapIcon :attr="node.raw" /> {{ node.label }}
</div>
</treeselect>
<treeselect
class="custom-treeselect"
:style="{ width: '100px', '--custom-height': '24px' }"
v-model="item.exp"
:multiple="false"
:clearable="false"
searchable
:options="[...getExpListByProperty(item.property), ...advancedExpList]"
:normalizer="
(node) => {
return {
id: node.value,
label: node.label,
children: node.children,
}
}
"
@select="(value) => handleChangeExp(value, item, index)"
>
</treeselect>
<treeselect
class="custom-treeselect"
:style="{ width: '175px', '--custom-height': '24px' }"
v-model="item.value"
:multiple="false"
:clearable="false"
searchable
v-if="isChoiceByProperty(item.property) && (item.exp === 'is' || item.exp === '~is')"
:options="getChoiceValueByProperty(item.property)"
placeholder="请选择"
:normalizer="
(node) => {
return {
id: node[0],
label: node[0],
children: node.children,
}
}
"
>
<div
:title="node.label"
slot="option-label"
slot-scope="{ node }"
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
>
{{ node.label }}
</div>
</treeselect>
<a-input-group
size="small"
compact
v-else-if="item.exp === 'range' || item.exp === '~range'"
:style="{ width: '175px' }"
>
<a-input class="ops-input" size="small" v-model="item.min" :style="{ width: '78px' }" placeholder="最小值" />
~
<a-input class="ops-input" size="small" v-model="item.max" :style="{ width: '78px' }" placeholder="最大值" />
</a-input-group>
<a-input-group size="small" compact v-else-if="item.exp === 'compare'" :style="{ width: '175px' }">
<treeselect
class="custom-treeselect"
:style="{ width: '60px', '--custom-height': '24px' }"
v-model="item.compareType"
:multiple="false"
:clearable="false"
searchable
:options="compareTypeList"
:normalizer="
(node) => {
return {
id: node.value,
label: node.label,
children: node.children,
}
}
"
>
</treeselect>
<a-input class="ops-input" v-model="item.value" size="small" style="width: 113px" />
</a-input-group>
<a-input
v-else-if="item.exp !== 'value' && item.exp !== '~value'"
size="small"
v-model="item.value"
:placeholder="item.exp === 'in' || item.exp === '~in' ? '以 ; 分隔' : ''"
class="ops-input"
></a-input>
<div v-else :style="{ width: '175px' }"></div>
<a-tooltip title="复制">
<a class="operation" @click="handleCopyRule(item)"><ops-icon type="icon-xianxing-copy"/></a>
</a-tooltip>
<a-tooltip title="删除">
<a class="operation" @click="handleDeleteRule(item)"><ops-icon type="icon-xianxing-delete"/></a>
</a-tooltip>
</a-space>
<div class="table-filter-add">
<a @click="handleAddRule">+ 新增</a>
</div>
</div>
</template>
<script>
import _ from 'lodash'
import { v4 as uuidv4 } from 'uuid'
import { ruleTypeList, expList, advancedExpList, compareTypeList } from './constants'
import ValueTypeMapIcon from '../CMDBValueTypeMapIcon'
export default {
name: 'Expression',
components: { ValueTypeMapIcon },
model: {
prop: 'value',
event: 'change',
},
props: {
value: {
type: Array,
default: () => [],
},
canSearchPreferenceAttrList: {
type: Array,
required: true,
default: () => [],
},
},
data() {
return {
ruleTypeList,
expList,
advancedExpList,
compareTypeList,
}
},
computed: {
ruleList: {
get() {
return this.value
},
set(val) {
this.$emit('change', val)
return val
},
},
},
methods: {
getExpListByProperty(property) {
if (property) {
const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property)
if (_find && ['0', '1', '3', '4', '5'].includes(_find.value_type)) {
return [
{ value: 'is', label: '等于' },
{ value: '~is', label: '不等于' },
{ value: '~value', label: '为空' }, // 为空的定义有点绕
{ value: 'value', label: '不为空' },
]
}
return this.expList
}
return this.expList
},
isChoiceByProperty(property) {
const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property)
if (_find) {
return _find.is_choice
}
return false
},
handleAddRule() {
this.ruleList.push({
id: uuidv4(),
type: 'and',
property: this.canSearchPreferenceAttrList[0]?.name,
exp: 'is',
value: null,
})
this.$emit('change', this.ruleList)
},
handleCopyRule(item) {
this.ruleList.push({ ...item, id: uuidv4() })
this.$emit('change', this.ruleList)
},
handleDeleteRule(item) {
const idx = this.ruleList.findIndex((r) => r.id === item.id)
if (idx > -1) {
this.ruleList.splice(idx, 1)
}
this.$emit('change', this.ruleList)
},
getChoiceValueByProperty(property) {
const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property)
if (_find) {
return _find.choice_value
}
return []
},
handleChangeExp({ value }, item, index) {
const _ruleList = _.cloneDeep(this.ruleList)
if (value === 'range') {
_ruleList[index] = {
..._ruleList[index],
min: '',
max: '',
exp: value,
}
} else if (value === 'compare') {
_ruleList[index] = {
..._ruleList[index],
compareType: '1',
exp: value,
}
} else {
_ruleList[index] = {
..._ruleList[index],
exp: value,
}
}
this.ruleList = _ruleList
this.$emit('change', this.ruleList)
},
},
}
</script>
<style></style>

View File

@@ -1,290 +1,282 @@
<template>
<div>
<a-popover
v-if="isDropdown"
v-model="visible"
trigger="click"
:placement="placement"
overlayClassName="table-filter"
@visibleChange="visibleChange"
>
<slot name="popover_item">
<a-button type="primary" ghost>条件过滤<a-icon type="filter"/></a-button>
</slot>
<template slot="content">
<Expression
v-model="ruleList"
:canSearchPreferenceAttrList="canSearchPreferenceAttrList.filter((attr) => !attr.is_password)"
/>
<a-divider :style="{ margin: '10px 0' }" />
<div style="width:534px">
<a-space :style="{ display: 'flex', justifyContent: 'flex-end' }">
<a-button type="primary" size="small" @click="handleSubmit">确定</a-button>
<a-button size="small" @click="handleClear">清空</a-button>
</a-space>
</div>
</template>
</a-popover>
<Expression
v-else
v-model="ruleList"
:canSearchPreferenceAttrList="canSearchPreferenceAttrList.filter((attr) => !attr.is_password)"
/>
</div>
</template>
<script>
import { v4 as uuidv4 } from 'uuid'
import Expression from './expression.vue'
import { advancedExpList, compareTypeList } from './constants'
export default {
name: 'FilterComp',
components: { Expression },
props: {
canSearchPreferenceAttrList: {
type: Array,
required: true,
default: () => [],
},
expression: {
type: String,
default: '',
},
regQ: {
type: String,
default: '(?<=q=).+(?=&)|(?<=q=).+$',
},
placement: {
type: String,
default: 'bottomRight',
},
isDropdown: {
type: Boolean,
default: true,
},
},
data() {
return {
advancedExpList,
compareTypeList,
visible: false,
ruleList: [],
filterExp: '',
}
},
methods: {
visibleChange(open, isInitOne = true) {
// isInitOne 初始化exp为空时ruleList是否默认给一条
// const regQ = /(?<=q=).+(?=&)|(?<=q=).+$/g
const exp = this.expression.match(new RegExp(this.regQ, 'g'))
? this.expression.match(new RegExp(this.regQ, 'g'))[0]
: null
if (open && exp) {
const expArray = exp.split(',').map((item) => {
let has_not = ''
const key = item.split(':')[0]
const val = item
.split(':')
.slice(1)
.join(':')
let type, property, exp, value, min, max, compareType
if (key.includes('-')) {
type = 'or'
if (key.includes('~')) {
property = key.substring(2)
has_not = '~'
} else {
property = key.substring(1)
}
} else {
type = 'and'
if (key.includes('~')) {
property = key.substring(1)
has_not = '~'
} else {
property = key
}
}
const in_reg = /(?<=\().+(?=\))/g
const range_reg = /(?<=\[).+(?=\])/g
const compare_reg = /(?<=>=|<=|>(?!=)|<(?!=)).+/
if (val === '*') {
exp = has_not + 'value'
value = ''
} else if (in_reg.test(val)) {
exp = has_not + 'in'
value = val.match(in_reg)[0]
} else if (range_reg.test(val)) {
exp = has_not + 'range'
value = val.match(range_reg)[0]
min = value.split('_TO_')[0]
max = value.split('_TO_')[1]
} else if (compare_reg.test(val)) {
exp = has_not + 'compare'
value = val.match(compare_reg)[0]
const _compareType = val.substring(0, val.match(compare_reg)['index'])
const idx = compareTypeList.findIndex((item) => item.label === _compareType)
compareType = compareTypeList[idx].value
} else if (!val.includes('*')) {
exp = has_not + 'is'
value = val
} else {
const resList = [
['contain', /(?<=\*).*(?=\*)/g],
['end_with', /(?<=\*).+/g],
['start_with', /.+(?=\*)/g],
]
for (let i = 0; i < 3; i++) {
const reg = resList[i]
if (reg[1].test(val)) {
exp = has_not + reg[0]
value = val.match(reg[1])[0]
break
}
}
}
return {
id: uuidv4(),
type,
property,
exp,
value,
min,
max,
compareType,
}
})
this.ruleList = [...expArray]
} else if (open) {
const _canSearchPreferenceAttrList = this.canSearchPreferenceAttrList.filter((attr) => !attr.is_password)
this.ruleList = isInitOne
? [
{
id: uuidv4(),
type: 'and',
property:
_canSearchPreferenceAttrList && _canSearchPreferenceAttrList.length
? _canSearchPreferenceAttrList[0].name
: undefined,
exp: 'is',
value: null,
},
]
: []
}
},
handleClear() {
this.ruleList = [
{
id: uuidv4(),
type: 'and',
property: this.canSearchPreferenceAttrList[0].name,
exp: 'is',
value: null,
},
]
this.filterExp = ''
this.visible = false
this.$emit('setExpFromFilter', this.filterExp)
},
handleSubmit() {
if (this.ruleList && this.ruleList.length) {
this.ruleList[0].type = 'and' // 增删后以防万一第一个不是and
this.filterExp = ''
const expList = this.ruleList.map((rule) => {
let singleRuleExp = ''
let _exp = rule.exp
if (rule.type === 'or') {
singleRuleExp += '-'
}
if (rule.exp.includes('~')) {
singleRuleExp += '~'
_exp = rule.exp.split('~')[1]
}
singleRuleExp += `${rule.property}:`
if (_exp === 'is') {
singleRuleExp += `${rule.value ?? ''}`
}
if (_exp === 'contain') {
singleRuleExp += `*${rule.value ?? ''}*`
}
if (_exp === 'start_with') {
singleRuleExp += `${rule.value ?? ''}*`
}
if (_exp === 'end_with') {
singleRuleExp += `*${rule.value ?? ''}`
}
if (_exp === 'value') {
singleRuleExp += `*`
}
if (_exp === 'in') {
singleRuleExp += `(${rule.value ?? ''})`
}
if (_exp === 'range') {
singleRuleExp += `[${rule.min}_TO_${rule.max}]`
}
if (_exp === 'compare') {
const idx = compareTypeList.findIndex((item) => item.value === rule.compareType)
singleRuleExp += `${compareTypeList[idx].label}${rule.value ?? ''}`
}
return singleRuleExp
})
this.filterExp = expList.join(',')
this.$emit('setExpFromFilter', this.filterExp)
} else {
this.$emit('setExpFromFilter', '')
}
this.visible = false
},
},
}
</script>
<style lang="less" scoped>
.table-filter {
.table-filter-add {
margin-top: 10px;
& > a {
padding: 2px 8px;
&:hover {
background-color: #f0faff;
border-radius: 5px;
}
}
}
.table-filter-extra-icon {
padding: 0px 2px;
&:hover {
display: inline-block;
border-radius: 5px;
background-color: #f0faff;
}
}
}
</style>
<style lang="less">
.table-filter-extra-operation {
.ant-popover-inner-content {
padding: 3px 4px;
.operation {
cursor: pointer;
width: 90px;
height: 30px;
line-height: 30px;
padding: 3px 4px;
border-radius: 5px;
transition: all 0.3s;
&:hover {
background-color: #f0faff;
}
> .anticon {
margin-right: 10px;
}
}
}
}
</style>
<template>
<div>
<a-popover
v-if="isDropdown"
v-model="visible"
trigger="click"
:placement="placement"
overlayClassName="table-filter"
@visibleChange="visibleChange"
>
<slot name="popover_item">
<a-button type="primary" ghost>条件过滤<a-icon type="filter"/></a-button>
</slot>
<template slot="content">
<Expression v-model="ruleList" :canSearchPreferenceAttrList="canSearchPreferenceAttrList" />
<a-divider :style="{ margin: '10px 0' }" />
<div style="width:534px">
<a-space :style="{ display: 'flex', justifyContent: 'flex-end' }">
<a-button type="primary" size="small" @click="handleSubmit">确定</a-button>
<a-button size="small" @click="handleClear">清空</a-button>
</a-space>
</div>
</template>
</a-popover>
<Expression v-else v-model="ruleList" :canSearchPreferenceAttrList="canSearchPreferenceAttrList" />
</div>
</template>
<script>
import { v4 as uuidv4 } from 'uuid'
import Expression from './expression.vue'
import { advancedExpList, compareTypeList } from './constants'
export default {
name: 'FilterComp',
components: { Expression },
props: {
canSearchPreferenceAttrList: {
type: Array,
required: true,
default: () => [],
},
expression: {
type: String,
default: '',
},
regQ: {
type: String,
default: '(?<=q=).+(?=&)|(?<=q=).+$',
},
placement: {
type: String,
default: 'bottomRight',
},
isDropdown: {
type: Boolean,
default: true,
},
},
data() {
return {
advancedExpList,
compareTypeList,
visible: false,
ruleList: [],
filterExp: '',
}
},
methods: {
visibleChange(open, isInitOne = true) {
// isInitOne 初始化exp为空时ruleList是否默认给一条
// const regQ = /(?<=q=).+(?=&)|(?<=q=).+$/g
const exp = this.expression.match(new RegExp(this.regQ, 'g'))
? this.expression.match(new RegExp(this.regQ, 'g'))[0]
: null
if (open && exp) {
const expArray = exp.split(',').map((item) => {
let has_not = ''
const key = item.split(':')[0]
const val = item
.split(':')
.slice(1)
.join(':')
let type, property, exp, value, min, max, compareType
if (key.includes('-')) {
type = 'or'
if (key.includes('~')) {
property = key.substring(2)
has_not = '~'
} else {
property = key.substring(1)
}
} else {
type = 'and'
if (key.includes('~')) {
property = key.substring(1)
has_not = '~'
} else {
property = key
}
}
const in_reg = /(?<=\().+(?=\))/g
const range_reg = /(?<=\[).+(?=\])/g
const compare_reg = /(?<=>=|<=|>(?!=)|<(?!=)).+/
if (val === '*') {
exp = has_not + 'value'
value = ''
} else if (in_reg.test(val)) {
exp = has_not + 'in'
value = val.match(in_reg)[0]
} else if (range_reg.test(val)) {
exp = has_not + 'range'
value = val.match(range_reg)[0]
min = value.split('_TO_')[0]
max = value.split('_TO_')[1]
} else if (compare_reg.test(val)) {
exp = has_not + 'compare'
value = val.match(compare_reg)[0]
const _compareType = val.substring(0, val.match(compare_reg)['index'])
const idx = compareTypeList.findIndex((item) => item.label === _compareType)
compareType = compareTypeList[idx].value
} else if (!val.includes('*')) {
exp = has_not + 'is'
value = val
} else {
const resList = [
['contain', /(?<=\*).*(?=\*)/g],
['end_with', /(?<=\*).+/g],
['start_with', /.+(?=\*)/g],
]
for (let i = 0; i < 3; i++) {
const reg = resList[i]
if (reg[1].test(val)) {
exp = has_not + reg[0]
value = val.match(reg[1])[0]
break
}
}
}
return {
id: uuidv4(),
type,
property,
exp,
value,
min,
max,
compareType,
}
})
this.ruleList = [...expArray]
} else if (open) {
this.ruleList = isInitOne
? [
{
id: uuidv4(),
type: 'and',
property:
this.canSearchPreferenceAttrList && this.canSearchPreferenceAttrList.length
? this.canSearchPreferenceAttrList[0].name
: undefined,
exp: 'is',
value: null,
},
]
: []
}
},
handleClear() {
this.ruleList = [
{
id: uuidv4(),
type: 'and',
property: this.canSearchPreferenceAttrList[0].name,
exp: 'is',
value: null,
},
]
this.filterExp = ''
this.visible = false
this.$emit('setExpFromFilter', this.filterExp)
},
handleSubmit() {
if (this.ruleList && this.ruleList.length) {
this.ruleList[0].type = 'and' // 增删后以防万一第一个不是and
this.filterExp = ''
const expList = this.ruleList.map((rule) => {
let singleRuleExp = ''
let _exp = rule.exp
if (rule.type === 'or') {
singleRuleExp += '-'
}
if (rule.exp.includes('~')) {
singleRuleExp += '~'
_exp = rule.exp.split('~')[1]
}
singleRuleExp += `${rule.property}:`
if (_exp === 'is') {
singleRuleExp += `${rule.value ?? ''}`
}
if (_exp === 'contain') {
singleRuleExp += `*${rule.value ?? ''}*`
}
if (_exp === 'start_with') {
singleRuleExp += `${rule.value ?? ''}*`
}
if (_exp === 'end_with') {
singleRuleExp += `*${rule.value ?? ''}`
}
if (_exp === 'value') {
singleRuleExp += `*`
}
if (_exp === 'in') {
singleRuleExp += `(${rule.value ?? ''})`
}
if (_exp === 'range') {
singleRuleExp += `[${rule.min}_TO_${rule.max}]`
}
if (_exp === 'compare') {
const idx = compareTypeList.findIndex((item) => item.value === rule.compareType)
singleRuleExp += `${compareTypeList[idx].label}${rule.value ?? ''}`
}
return singleRuleExp
})
this.filterExp = expList.join(',')
this.$emit('setExpFromFilter', this.filterExp)
} else {
this.$emit('setExpFromFilter', '')
}
this.visible = false
},
},
}
</script>
<style lang="less" scoped>
.table-filter {
.table-filter-add {
margin-top: 10px;
& > a {
padding: 2px 8px;
&:hover {
background-color: #f0faff;
border-radius: 5px;
}
}
}
.table-filter-extra-icon {
padding: 0px 2px;
&:hover {
display: inline-block;
border-radius: 5px;
background-color: #f0faff;
}
}
}
</style>
<style lang="less">
.table-filter-extra-operation {
.ant-popover-inner-content {
padding: 3px 4px;
.operation {
cursor: pointer;
width: 90px;
height: 30px;
line-height: 30px;
padding: 3px 4px;
border-radius: 5px;
transition: all 0.3s;
&:hover {
background-color: #f0faff;
}
> .anticon {
margin-right: 10px;
}
}
}
}
</style>

View File

@@ -1,45 +1,57 @@
<template>
<span>
<ops-icon :type="getPropertyIcon(attr)" />
</span>
</template>
<script>
export default {
name: 'ValueTypeIcon',
props: {
attr: {
type: Object,
default: () => {},
},
},
methods: {
getPropertyIcon(attr) {
switch (attr.value_type) {
case '0':
return 'icon-xianxing-shishu'
case '1':
return 'icon-xianxing-fudianshu'
case '2':
if (attr.is_password) {
return 'icon-xianxing-password'
}
if (attr.is_link) {
return 'icon-xianxing-link'
}
return 'icon-xianxing-wenben'
case '3':
return 'icon-xianxing-datetime'
case '4':
return 'icon-xianxing-date'
case '5':
return 'icon-xianxing-time'
case '6':
return 'icon-xianxing-json'
}
},
},
}
</script>
<style></style>
<template>
<span>
<ops-icon :type="getPropertyIcon(attr)" />
</span>
</template>
<script>
export default {
name: 'ValueTypeIcon',
props: {
attr: {
type: Object,
default: () => {},
},
},
methods: {
getPropertyStyle(attr) {
switch (attr.value_type) {
case '0':
return { color: '#cf1322', backgroundColor: '#fff1f0' }
case '1':
return { color: '#d4b106', backgroundColor: '#feffe6' }
case '2':
return { color: '#d46b08', backgroundColor: '#fff7e6' }
case '3':
return { color: '#531dab', backgroundColor: '#f9f0ff' }
case '4':
return { color: '#389e0d', backgroundColor: '#f6ffed' }
case '5':
return { color: '#08979c', backgroundColor: '#e6fffb' }
case '6':
return { color: '#c41d7f', backgroundColor: '#fff0f6' }
}
},
getPropertyIcon(attr) {
switch (attr.value_type) {
case '0':
return 'icon-xianxing-shishu'
case '1':
return 'icon-xianxing-fudianshu'
case '2':
return 'icon-xianxing-wenben'
case '3':
return 'icon-xianxing-datetime'
case '4':
return 'icon-xianxing-date'
case '5':
return 'icon-xianxing-time'
case '6':
return 'icon-xianxing-json'
}
},
},
}
</script>
<style></style>

View File

@@ -4,7 +4,7 @@ const appConfig = {
buildAclToModules: true, // 是否在各个应用下 内联权限管理
ssoLogoutURL: '/api/sso/logout',
showDocs: false,
useEncryption: false,
useEncryption: true,
}
export default appConfig

View File

@@ -68,7 +68,6 @@
ref="xTable"
row-id="id"
show-overflow
resizable
>
<!-- 1 -->
<vxe-table-column type="checkbox" fixed="left" :width="45"></vxe-table-column>
@@ -100,7 +99,7 @@
align="center"
show-overflow>
<template #default="{ row }">
<span v-show="isGroup">
<span v-show="row.isGroup">
<a @click="handleDisplayMember(row)">成员</a>
<a-divider type="vertical" />
<a @click="handleGroupEdit(row)">编辑</a>

View File

@@ -35,8 +35,8 @@ export default {
secret: '',
},
rules: {
key: [{ required: false, message: 'key is required' }],
secret: [{ required: false, message: 'secret is required' }],
key: [{ required: true, message: 'key is required' }],
secret: [{ required: true, message: 'secret is required' }],
},
visible: false,
}

View File

@@ -1,198 +1,194 @@
<template>
<div class="acl-users">
<div class="acl-users-header">
<a-button v-if="isAclAdmin" @click="handleCreate" type="primary">{{ btnName }}</a-button>
<a-input-search
class="ops-input"
allowClear
:style="{ display: 'inline', marginLeft: '10px' }"
placeholder="搜索 | 用户名、中文名"
v-model="searchName"
></a-input-search>
</div>
<a-spin :spinning="loading">
<vxe-grid
stripe
class="ops-stripe-table"
:columns="tableColumns"
:data="tableData"
show-overflow
highlight-hover-row
:height="`${windowHeight - 165}px`"
size="small"
>
<template #block_default="{row}">
<a-icon type="lock" v-if="row.block" />
</template>
<template #action_default="{row}">
<a-space>
<a :disabled="isAclAdmin ? false : true" @click="handleEdit(row)">
<a-icon type="edit" />
</a>
<a-tooltip title="权限汇总">
<a @click="handlePermCollect(row)"><a-icon type="solution"/></a>
</a-tooltip>
<a-popconfirm :title="`确认删除【${row.nickname || row.username}】?`" @confirm="deleteUser(row.uid)">
<a :style="{ color: 'red' }"><ops-icon type="icon-xianxing-delete"/></a>
</a-popconfirm>
</a-space>
</template>
</vxe-grid>
</a-spin>
<userForm ref="userForm" :handleOk="handleOk"> </userForm>
<perm-collect-form ref="permCollectForm"></perm-collect-form>
</div>
</template>
<script>
import { mapState } from 'vuex'
import userForm from './module/userForm'
import PermCollectForm from './module/permCollectForm'
import { deleteUserById, searchUser, getOnDutyUser } from '@/modules/acl/api/user'
export default {
name: 'Users',
components: {
userForm,
PermCollectForm,
},
data() {
return {
loading: false,
tableColumns: [
{
title: '用户名',
field: 'username',
sortable: true,
minWidth: '100px',
fixed: 'left',
},
{
title: '中文名',
field: 'nickname',
minWidth: '100px',
},
{
title: '加入时间',
field: 'date_joined',
minWidth: '160px',
align: 'center',
sortable: true,
},
{
title: '锁定',
field: 'block',
width: '150px',
align: 'center',
slots: {
default: 'block_default',
},
},
{
title: '操作',
field: 'action',
width: '150px',
fixed: 'right',
align: 'center',
slots: {
default: 'action_default',
},
},
],
onDutuUids: [],
btnName: '新增用户',
allUsers: [],
tableData: [],
searchName: '',
}
},
beforeCreate() {
this.form = this.$form.createForm(this)
},
async beforeMount() {
this.loading = true
await this.getOnDutyUser()
this.search()
},
computed: {
...mapState({
windowHeight: (state) => state.windowHeight,
}),
isAclAdmin: function() {
if (this.$store.state.user.roles.permissions.filter((item) => item === 'acl_admin').length > 0) {
return true
} else {
return false
}
},
},
watch: {
searchName: {
immediate: true,
handler(newVal, oldVal) {
if (newVal) {
this.tableData = this.allUsers.filter(
(item) =>
item.username.toLowerCase().includes(newVal.toLowerCase()) ||
item.nickname.toLowerCase().includes(newVal.toLowerCase())
)
} else {
this.tableData = this.allUsers
}
},
},
},
mounted() {},
inject: ['reload'],
methods: {
async getOnDutyUser() {
await getOnDutyUser().then((res) => {
this.onDutuUids = res.map((i) => i.uid)
})
},
search() {
searchUser({ page_size: 10000 }).then((res) => {
const ret = res.users.filter((u) => this.onDutuUids.includes(u.uid))
this.allUsers = ret
this.tableData = ret
this.loading = false
})
},
handlePermCollect(record) {
this.$refs['permCollectForm'].collect(record)
},
handleEdit(record) {
this.$refs.userForm.handleEdit(record)
},
async handleOk() {
this.searchName = ''
await this.getOnDutyUser()
this.search()
},
handleCreate() {
this.$refs.userForm.handleCreate()
},
deleteUser(uid) {
deleteUserById(uid).then((res) => {
this.$message.success(`删除成功`)
this.handleOk()
})
},
},
}
</script>
<style lang="less" scoped>
.acl-users {
border-radius: 15px;
background-color: #fff;
height: calc(100vh - 64px);
margin-bottom: -24px;
padding: 24px;
.acl-users-header {
display: inline-flex;
margin-bottom: 15px;
}
}
</style>
<template>
<div class="acl-users">
<div class="acl-users-header">
<a-button v-if="isAclAdmin" @click="handleCreate" type="primary">{{ btnName }}</a-button>
<a-input-search
class="ops-input"
allowClear
:style="{ display: 'inline', marginLeft: '10px' }"
placeholder="搜索 | 用户名、中文名"
v-model="searchName"
></a-input-search>
</div>
<a-spin :spinning="loading">
<vxe-grid
stripe
class="ops-stripe-table"
:columns="tableColumns"
:data="tableData"
show-overflow
highlight-hover-row
:height="`${windowHeight - 165}px`"
size="small"
>
<template #block_default="{row}">
<a-icon type="lock" v-if="row.block" />
</template>
<template #action_default="{row}">
<a-space>
<a :disabled="isAclAdmin ? false : true" @click="handleEdit(row)">
<a-icon type="edit" />
</a>
<a-tooltip title="权限汇总">
<a @click="handlePermCollect(row)"><a-icon type="solution"/></a>
</a-tooltip>
<a-popconfirm :title="`确认删除【${row.nickname || row.username}】?`" @confirm="deleteUser(row.uid)">
<a :style="{ color: 'red' }"><ops-icon type="icon-xianxing-delete"/></a>
</a-popconfirm>
</a-space>
</template>
</vxe-grid>
</a-spin>
<userForm ref="userForm" :handleOk="handleOk"> </userForm>
<perm-collect-form ref="permCollectForm"></perm-collect-form>
</div>
</template>
<script>
import { mapState } from 'vuex'
import userForm from './module/userForm'
import PermCollectForm from './module/permCollectForm'
import { deleteUserById, searchUser, getOnDutyUser } from '@/modules/acl/api/user'
export default {
name: 'Users',
components: {
userForm,
PermCollectForm,
},
data() {
return {
loading: false,
tableColumns: [
{
title: '用户名',
field: 'username',
sortable: true,
minWidth: '100px',
fixed: 'left',
},
{
title: '中文名',
field: 'nickname',
minWidth: '100px',
},
{
title: '加入时间',
field: 'date_joined',
minWidth: '160px',
align: 'center',
sortable: true,
},
{
title: '锁定',
field: 'block',
width: '150px',
align: 'center',
slots: {
default: 'block_default',
},
},
{
title: '操作',
field: 'action',
width: '150px',
fixed: 'right',
align: 'center',
slots: {
default: 'action_default',
},
},
],
onDutuUids: [],
btnName: '新增用户',
allUsers: [],
tableData: [],
searchName: '',
}
},
beforeCreate() {
this.form = this.$form.createForm(this)
},
async beforeMount() {
this.loading = true
await getOnDutyUser().then((res) => {
this.onDutuUids = res.map((i) => i.uid)
this.search()
})
},
computed: {
...mapState({
windowHeight: (state) => state.windowHeight,
}),
isAclAdmin: function() {
if (this.$store.state.user.roles.permissions.filter((item) => item === 'acl_admin').length > 0) {
return true
} else {
return false
}
},
},
watch: {
searchName: {
immediate: true,
handler(newVal, oldVal) {
if (newVal) {
this.tableData = this.allUsers.filter(
(item) =>
item.username.toLowerCase().includes(newVal.toLowerCase()) ||
item.nickname.toLowerCase().includes(newVal.toLowerCase())
)
} else {
this.tableData = this.allUsers
}
},
},
},
mounted() {},
inject: ['reload'],
methods: {
search() {
searchUser({ page_size: 10000 }).then((res) => {
const ret = res.users.filter((u) => this.onDutuUids.includes(u.uid))
this.allUsers = ret
this.tableData = ret
this.loading = false
})
},
handlePermCollect(record) {
this.$refs['permCollectForm'].collect(record)
},
handleEdit(record) {
this.$refs.userForm.handleEdit(record)
},
handleOk() {
this.searchName = ''
this.search()
},
handleCreate() {
this.$refs.userForm.handleCreate()
},
deleteUser(uid) {
deleteUserById(uid).then((res) => {
this.$message.success(`删除成功`)
this.handleOk()
})
},
},
}
</script>
<style lang="less" scoped>
.acl-users {
border-radius: 15px;
background-color: #fff;
height: calc(100vh - 64px);
margin-bottom: -24px;
padding: 24px;
.acl-users-header {
display: inline-flex;
margin-bottom: 15px;
}
}
</style>

View File

@@ -1,207 +1,207 @@
import { axios } from '@/utils/request'
/**
* 获取 所有的 ci_types
* @param parameter
* @returns {AxiosPromise}
*/
export function getCITypes(parameter) {
return axios({
url: '/v0.1/ci_types',
method: 'GET',
params: parameter
})
}
/**
* 获取 某个 ci_types
* @param CITypeName
* @param parameter
* @returns {AxiosPromise}
*/
export function getCIType(CITypeName, parameter) {
return axios({
url: `/v0.1/ci_types/${CITypeName}`,
method: 'GET',
params: parameter
})
}
/**
* 创建 ci_type
* @param data
* @returns {AxiosPromise}
*/
export function createCIType(data) {
return axios({
url: '/v0.1/ci_types',
method: 'POST',
data: data
})
}
/**
* 更新 ci_type
* @param CITypeId
* @param data
* @returns {AxiosPromise}
*/
export function updateCIType(CITypeId, data) {
return axios({
url: `/v0.1/ci_types/${CITypeId}`,
method: 'PUT',
data: data
})
}
/**
* 删除 ci_type
* @param CITypeId
* @returns {AxiosPromise}
*/
export function deleteCIType(CITypeId) {
return axios({
url: `/v0.1/ci_types/${CITypeId}`,
method: 'DELETE'
})
}
/**
* 获取 某个 ci_type 的分组
* @param CITypeId
* @param data
* @returns {AxiosPromise}
*/
export function getCITypeGroupById(CITypeId, data) {
return axios({
url: `/v0.1/ci_types/${CITypeId}/attribute_groups`,
method: 'GET',
params: data
})
}
/**
* 保存 某个 ci_type 的分组
* @param CITypeId
* @param data
* @returns {AxiosPromise}
*/
export function createCITypeGroupById(CITypeId, data) {
return axios({
url: `/v0.1/ci_types/${CITypeId}/attribute_groups`,
method: 'POST',
data: data
})
}
/**
* 修改 某个 ci_type 的分组
* @param groupId
* @param data
* @returns {AxiosPromise}
*/
export function updateCITypeGroupById(groupId, data) {
return axios({
url: `/v0.1/ci_types/attribute_groups/${groupId}`,
method: 'PUT',
data: data
})
}
/**
* 删除 某个 ci_type 的分组
* @param groupId
* @param data
* @returns {AxiosPromise}
*/
export function deleteCITypeGroupById(groupId, data) {
return axios({
url: `/v0.1/ci_types/attribute_groups/${groupId}`,
method: 'delete',
data: data
})
}
export function getUniqueConstraintList(type_id) {
return axios({
url: `/v0.1/ci_types/${type_id}/unique_constraint`,
method: 'get',
})
}
export function addUniqueConstraint(type_id, data) {
return axios({
url: `/v0.1/ci_types/${type_id}/unique_constraint`,
method: 'post',
data: data
})
}
export function updateUniqueConstraint(type_id, id, data) {
return axios({
url: `/v0.1/ci_types/${type_id}/unique_constraint/${id}`,
method: 'put',
data: data
})
}
export function deleteUniqueConstraint(type_id, id) {
return axios({
url: `/v0.1/ci_types/${type_id}/unique_constraint/${id}`,
method: 'delete',
})
}
export function getTriggerList(type_id) {
return axios({
url: `/v0.1/ci_types/${type_id}/triggers`,
method: 'get',
})
}
export function addTrigger(type_id, data) {
return axios({
url: `/v0.1/ci_types/${type_id}/triggers`,
method: 'post',
data: data
})
}
export function updateTrigger(type_id, id, data) {
return axios({
url: `/v0.1/ci_types/${type_id}/triggers/${id}`,
method: 'put',
data: data
})
}
export function deleteTrigger(type_id, id) {
return axios({
url: `/v0.1/ci_types/${type_id}/triggers/${id}`,
method: 'delete',
})
}
// CMDB的模型和实例的授权接口
export function grantCiType(type_id, rid, data) {
return axios({
url: `/v0.1/ci_types/${type_id}/roles/${rid}/grant`,
method: 'post',
data
})
}
// CMDB的模型和实例的删除授权接口
export function revokeCiType(type_id, rid, data) {
return axios({
url: `/v0.1/ci_types/${type_id}/roles/${rid}/revoke`,
method: 'post',
data
})
}
// CMDB的模型和实例的过滤的权限
export function ciTypeFilterPermissions(type_id) {
return axios({
url: `/v0.1/ci_types/${type_id}/filters/permissions`,
method: 'get',
})
}
import { axios } from '@/utils/request'
/**
* 获取 所有的 ci_types
* @param parameter
* @returns {AxiosPromise}
*/
export function getCITypes(parameter) {
return axios({
url: '/v0.1/ci_types',
method: 'GET',
params: parameter
})
}
/**
* 获取 某个 ci_types
* @param CITypeName
* @param parameter
* @returns {AxiosPromise}
*/
export function getCIType(CITypeName, parameter) {
return axios({
url: `/v0.1/ci_types/${CITypeName}`,
method: 'GET',
params: parameter
})
}
/**
* 创建 ci_type
* @param data
* @returns {AxiosPromise}
*/
export function createCIType(data) {
return axios({
url: '/v0.1/ci_types',
method: 'POST',
data: data
})
}
/**
* 更新 ci_type
* @param CITypeId
* @param data
* @returns {AxiosPromise}
*/
export function updateCIType(CITypeId, data) {
return axios({
url: `/v0.1/ci_types/${CITypeId}`,
method: 'PUT',
data: data
})
}
/**
* 删除 ci_type
* @param CITypeId
* @returns {AxiosPromise}
*/
export function deleteCIType(CITypeId) {
return axios({
url: `/v0.1/ci_types/${CITypeId}`,
method: 'DELETE'
})
}
/**
* 获取 某个 ci_type 的分组
* @param CITypeId
* @param data
* @returns {AxiosPromise}
*/
export function getCITypeGroupById(CITypeId, data) {
return axios({
url: `/v0.1/ci_types/${CITypeId}/attribute_groups`,
method: 'GET',
params: data
})
}
/**
* 保存 某个 ci_type 的分组
* @param CITypeId
* @param data
* @returns {AxiosPromise}
*/
export function createCITypeGroupById(CITypeId, data) {
return axios({
url: `/v0.1/ci_types/${CITypeId}/attribute_groups`,
method: 'POST',
data: data
})
}
/**
* 修改 某个 ci_type 的分组
* @param groupId
* @param data
* @returns {AxiosPromise}
*/
export function updateCITypeGroupById(groupId, data) {
return axios({
url: `/v0.1/ci_types/attribute_groups/${groupId}`,
method: 'PUT',
data: data
})
}
/**
* 删除 某个 ci_type 的分组
* @param groupId
* @param data
* @returns {AxiosPromise}
*/
export function deleteCITypeGroupById(groupId, data) {
return axios({
url: `/v0.1/ci_types/attribute_groups/${groupId}`,
method: 'delete',
data: data
})
}
export function getUniqueConstraintList(type_id) {
return axios({
url: `/v0.1/ci_types/${type_id}/unique_constraint`,
method: 'get',
})
}
export function addUniqueConstraint(type_id, data) {
return axios({
url: `/v0.1/ci_types/${type_id}/unique_constraint`,
method: 'post',
data: data
})
}
export function updateUniqueConstraint(type_id, id, data) {
return axios({
url: `/v0.1/ci_types/${type_id}/unique_constraint/${id}`,
method: 'put',
data: data
})
}
export function deleteUniqueConstraint(type_id, id) {
return axios({
url: `/v0.1/ci_types/${type_id}/unique_constraint/${id}`,
method: 'delete',
})
}
export function getTriggerList(type_id) {
return axios({
url: `/v0.1/ci_types/${type_id}/triggers`,
method: 'get',
})
}
export function addTrigger(type_id, data) {
return axios({
url: `/v0.1/ci_types/${type_id}/triggers`,
method: 'post',
data: data
})
}
export function updateTrigger(type_id, id, data) {
return axios({
url: `/v0.1/ci_types/${type_id}/triggers/${id}`,
method: 'put',
data: data
})
}
export function deleteTrigger(type_id, id) {
return axios({
url: `/v0.1/ci_types/${type_id}/triggers/${id}`,
method: 'delete',
})
}
// CMDB的模型和实例的授权接口
export function grantCiType(type_id, rid, data) {
return axios({
url: `/v0.1/ci_types/${type_id}/roles/${rid}/grant`,
method: 'post',
data
})
}
// CMDB的模型和实例的删除授权接口
export function revokeCiType(type_id, rid, data) {
return axios({
url: `/v0.1/ci_types/${type_id}/roles/${rid}/revoke`,
method: 'post',
data
})
}
// CMDB的模型和实例的过滤的权限
export function ciTypeFilterPermissions(type_id) {
return axios({
url: `/v0.1/ci_types/${type_id}/filters/permissions`,
method: 'get',
})
}

View File

@@ -1,177 +1,170 @@
import { axios } from '@/utils/request'
/**
* 获取 ci_type 的属性
* @param CITypeName
* @param parameter
* @returns {AxiosPromise}
*/
export function getCITypeAttributesByName(CITypeName, parameter) {
return axios({
url: `/v0.1/ci_types/${CITypeName}/attributes`,
method: 'get',
params: parameter
})
}
/**
* 获取 ci_type 的属性
* @param CITypeId
* @param parameter
* @returns {AxiosPromise}
*/
export function getCITypeAttributesById(CITypeId, parameter) {
return axios({
url: `/v0.1/ci_types/${CITypeId}/attributes`,
method: 'get',
params: parameter
})
}
/**
* 更新属性
* @param attrId
* @param data
* @returns {AxiosPromise}
*/
export function updateAttributeById(attrId, data) {
return axios({
url: `/v0.1/attributes/${attrId}`,
method: 'put',
data: data
})
}
/**
* 添加属性
* @param data
* @returns {AxiosPromise}
*/
export function createAttribute(data) {
return axios({
url: `/v0.1/attributes`,
method: 'post',
data: data
})
}
/**
* 搜索属性/ 获取所有的属性
* @param data
* @returns {AxiosPromise}
*/
export function searchAttributes(params) {
return axios({
url: `/v0.1/attributes/s`,
method: 'get',
params: params
})
}
export function getCITypeAttributesByTypeIds(params) {
return axios({
url: `/v0.1/ci_types/attributes`,
method: 'get',
params: params
})
}
export function getCITypeCommonAttributesByTypeIds(params) {
return axios({
url: `/v0.1/ci_types/common_attributes`,
method: 'get',
params: params
})
}
/**
* 删除属性
* @param attrId
* @returns {AxiosPromise}
*/
export function deleteAttributesById(attrId) {
return axios({
url: `/v0.1/attributes/${attrId}`,
method: 'delete'
})
}
/**
* 绑定ci_type 属性
* @param CITypeId
* @param data
* @returns {AxiosPromise}
*/
export function createCITypeAttributes(CITypeId, data) {
return axios({
url: `/v0.1/ci_types/${CITypeId}/attributes`,
method: 'post',
data: data
})
}
/**
* 更新ci_type 属性
* @param CITypeId
* @param data
* @returns {AxiosPromise}
*/
export function updateCITypeAttributesById(CITypeId, data) {
return axios({
url: `/v0.1/ci_types/${CITypeId}/attributes`,
method: 'put',
data: data
})
}
/**
* 删除ci_type 属性
* @param CITypeId
* @param data
* @returns {AxiosPromise}
*/
export function deleteCITypeAttributesById(CITypeId, data) {
return axios({
url: `/v0.1/ci_types/${CITypeId}/attributes`,
method: 'delete',
data: data
})
}
export function transferCITypeAttrIndex(CITypeId, data) {
return axios({
url: `/v0.1/ci_types/${CITypeId}/attributes/transfer`,
method: 'POST',
data: data
})
}
export function transferCITypeGroupIndex(CITypeId, data) {
return axios({
url: `/v0.1/ci_types/${CITypeId}/attribute_groups/transfer`,
method: 'POST',
data: data
})
}
export function canDefineComputed() {
return axios({
url: `/v0.1/ci_types/can_define_computed`,
method: 'HEAD',
})
}
export function calcComputedAttribute(attr_id) {
return axios({
url: `/v0.1/attributes/${attr_id}/calc_computed_attribute`,
method: 'PUT',
})
}
export function getAttrPassword(ci_id, attr_id) {
return axios({
url: `/v0.1/ci/${ci_id}/attributes/${attr_id}/password`,
method: 'Get',
})
}
import { axios } from '@/utils/request'
/**
* 获取 ci_type 的属性
* @param CITypeName
* @param parameter
* @returns {AxiosPromise}
*/
export function getCITypeAttributesByName(CITypeName, parameter) {
return axios({
url: `/v0.1/ci_types/${CITypeName}/attributes`,
method: 'get',
params: parameter
})
}
/**
* 获取 ci_type 的属性
* @param CITypeId
* @param parameter
* @returns {AxiosPromise}
*/
export function getCITypeAttributesById(CITypeId, parameter) {
return axios({
url: `/v0.1/ci_types/${CITypeId}/attributes`,
method: 'get',
params: parameter
})
}
/**
* 更新属性
* @param attrId
* @param data
* @returns {AxiosPromise}
*/
export function updateAttributeById(attrId, data) {
return axios({
url: `/v0.1/attributes/${attrId}`,
method: 'put',
data: data
})
}
/**
* 添加属性
* @param data
* @returns {AxiosPromise}
*/
export function createAttribute(data) {
return axios({
url: `/v0.1/attributes`,
method: 'post',
data: data
})
}
/**
* 搜索属性/ 获取所有的属性
* @param data
* @returns {AxiosPromise}
*/
export function searchAttributes(params) {
return axios({
url: `/v0.1/attributes/s`,
method: 'get',
params: params
})
}
export function getCITypeAttributesByTypeIds(params) {
return axios({
url: `/v0.1/ci_types/attributes`,
method: 'get',
params: params
})
}
export function getCITypeCommonAttributesByTypeIds(params) {
return axios({
url: `/v0.1/ci_types/common_attributes`,
method: 'get',
params: params
})
}
/**
* 删除属性
* @param attrId
* @returns {AxiosPromise}
*/
export function deleteAttributesById(attrId) {
return axios({
url: `/v0.1/attributes/${attrId}`,
method: 'delete'
})
}
/**
* 绑定ci_type 属性
* @param CITypeId
* @param data
* @returns {AxiosPromise}
*/
export function createCITypeAttributes(CITypeId, data) {
return axios({
url: `/v0.1/ci_types/${CITypeId}/attributes`,
method: 'post',
data: data
})
}
/**
* 更新ci_type 属性
* @param CITypeId
* @param data
* @returns {AxiosPromise}
*/
export function updateCITypeAttributesById(CITypeId, data) {
return axios({
url: `/v0.1/ci_types/${CITypeId}/attributes`,
method: 'put',
data: data
})
}
/**
* 删除ci_type 属性
* @param CITypeId
* @param data
* @returns {AxiosPromise}
*/
export function deleteCITypeAttributesById(CITypeId, data) {
return axios({
url: `/v0.1/ci_types/${CITypeId}/attributes`,
method: 'delete',
data: data
})
}
export function transferCITypeAttrIndex(CITypeId, data) {
return axios({
url: `/v0.1/ci_types/${CITypeId}/attributes/transfer`,
method: 'POST',
data: data
})
}
export function transferCITypeGroupIndex(CITypeId, data) {
return axios({
url: `/v0.1/ci_types/${CITypeId}/attribute_groups/transfer`,
method: 'POST',
data: data
})
}
export function canDefineComputed() {
return axios({
url: `/v0.1/ci_types/can_define_computed`,
method: 'HEAD',
})
}
export function calcComputedAttribute(attr_id) {
return axios({
url: `/v0.1/attributes/${attr_id}/calc_computed_attribute`,
method: 'PUT',
})
}

View File

@@ -1,56 +1,40 @@
import { axios } from '@/utils/request'
export function getCIHistory(ciId) {
return axios({
url: `/v0.1/history/ci/${ciId}`,
method: 'GET'
})
}
export function getCIHistoryTable(params) {
return axios({
url: `/v0.1/history/records/attribute`,
method: 'GET',
params: params
})
}
export function getRelationTable(params) {
return axios({
url: `/v0.1/history/records/relation`,
method: 'GET',
params: params
})
}
export function getCITypesTable(params) {
return axios({
url: `/v0.1/history/ci_types`,
method: 'GET',
params: params
})
}
export function getUsers(params) {
return axios({
url: `/v1/acl/users/employee`,
method: 'GET',
params: params
})
}
export function getCiTriggers(params) {
return axios({
url: `/v0.1/history/ci_triggers`,
method: 'GET',
params: params
})
}
export function getCiTriggersByCiId(ci_id, params) {
return axios({
url: `/v0.1/history/ci_triggers/${ci_id}`,
method: 'GET',
params
})
}
import { axios } from '@/utils/request'
export function getCIHistory (ciId) {
return axios({
url: `/v0.1/history/ci/${ciId}`,
method: 'GET'
})
}
export function getCIHistoryTable (params) {
return axios({
url: `/v0.1/history/records/attribute`,
method: 'GET',
params: params
})
}
export function getRelationTable (params) {
return axios({
url: `/v0.1/history/records/relation`,
method: 'GET',
params: params
})
}
export function getCITypesTable (params) {
return axios({
url: `/v0.1/history/ci_types`,
method: 'GET',
params: params
})
}
export function getUsers (params) {
return axios({
url: `/v1/acl/users/employee`,
method: 'GET',
params: params
})
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

View File

@@ -1,2 +0,0 @@
import NoticeContent from './index.vue'
export default NoticeContent

View File

@@ -1,199 +0,0 @@
<template>
<div class="notice-content">
<div class="notice-content-main">
<Toolbar
:editor="editor"
:defaultConfig="{
excludeKeys: [
'emotion',
'group-image',
'group-video',
'insertTable',
'codeBlock',
'blockquote',
'fullScreen',
],
}"
mode="default"
/>
<Editor class="notice-content-editor" :defaultConfig="editorConfig" mode="simple" @onCreated="onCreated" />
<div class="notice-content-sidebar">
<template v-if="needOld">
<div class="notice-content-sidebar-divider">变更前</div>
<div
@dblclick="dblclickSidebar(`old_${attr.name}`, attr.alias || attr.name)"
class="notice-content-sidebar-item"
v-for="attr in attrList"
:key="`old_${attr.id}`"
:title="attr.alias || attr.name"
>
{{ attr.alias || attr.name }}
</div>
<div class="notice-content-sidebar-divider">变更后</div>
</template>
<div
@dblclick="dblclickSidebar(attr.name, attr.alias || attr.name)"
class="notice-content-sidebar-item"
v-for="attr in attrList"
:key="attr.id"
:title="attr.alias || attr.name"
>
{{ attr.alias || attr.name }}
</div>
</div>
</div>
</div>
</template>
<script>
import _ from 'lodash'
import '@wangeditor/editor/dist/css/style.css'
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
export default {
name: 'NoticeContent',
components: { Editor, Toolbar },
props: {
attrList: {
type: Array,
default: () => [],
},
needOld: {
type: Boolean,
default: false,
},
},
data() {
return {
editor: null,
editorConfig: { placeholder: '请输入通知内容', readOnly: this.readOnly },
content: '',
defaultParams: [],
value2LabelMap: {},
}
},
beforeDestroy() {
const editor = this.editor
if (editor == null) return
editor.destroy() // 组件销毁时及时销毁编辑器
},
methods: {
onCreated(editor) {
this.editor = Object.seal(editor) // 一定要用 Object.seal() 否则会报错
},
getContent() {
const html = _.cloneDeep(this.editor.getHtml())
const _html = html.replace(
/<span data-w-e-type="attachment" (data-w-e-is-void|data-w-e-is-void="") (data-w-e-is-inline|data-w-e-is-inline="").*?<\/span>/gm,
(value) => {
const _match = value.match(/(?<=data-attachment(V|v)alue=").*?(?=")/)
return `{{${_match[0]}}}`
}
)
return { body_html: html, body: _html }
},
setContent(html) {
this.editor.setHtml(html)
},
dblclickSidebar(value, label) {
if (!this.readOnly) {
this.editor.restoreSelection()
const node = {
type: 'attachment',
attachmentValue: value,
attachmentLabel: `${label}`,
children: [{ text: '' }],
}
this.editor.insertNode(node)
}
},
},
}
</script>
<style lang="less" scoped>
@import '~@/style/static.less';
.notice-content {
width: 100%;
& &-main {
width: 100%;
display: flex;
flex-direction: column;
align-items: flex-end;
position: relative;
.notice-content-editor {
height: 300px;
width: 75%;
border: 1px solid #e4e7ed;
border-top: none;
overflow: hidden;
}
.notice-content-sidebar {
width: 25%;
position: absolute;
height: 300px;
bottom: 0;
left: 0;
border: 1px solid #e4e7ed;
border-top: none;
border-right: none;
overflow: auto;
.notice-content-sidebar-divider {
position: sticky;
top: 0;
margin: 0;
font-size: 12px;
color: #afafaf;
background-color: #fff;
line-height: 20px;
padding-left: 12px;
&::before,
&::after {
content: '';
position: absolute;
border-top: 1px solid #d1d1d1;
top: 50%;
transition: translateY(-50%);
}
&::before {
left: 3px;
width: 5px;
}
&::after {
right: 3px;
width: 78px;
}
}
.notice-content-sidebar-item:first-child {
margin-top: 10px;
}
.notice-content-sidebar-item {
line-height: 1.5;
padding: 4px 12px;
cursor: pointer;
user-select: none;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
&:hover {
background-color: #custom_colors[color_2];
color: #custom_colors[color_1];
}
}
}
}
}
</style>
<style lang="less">
@import '~@/style/static.less';
.notice-content {
.w-e-bar {
background-color: #custom_colors[color_2];
}
.w-e-text-placeholder {
line-height: 1.5;
}
}
</style>

View File

@@ -1,51 +1,42 @@
<template>
<div>
<span v-if="!isShow && !isTableLoading">{{ showPassword }}</span>
<span v-else>{{ password }}</span>
<a :style="{ marginLeft: '10px' }" @click="getPassword"><a-icon :type="isShow ? 'eye-invisible' : 'eye'"/></a>
</div>
</template>
<script>
import { mapState } from 'vuex'
import { getAttrPassword } from '../../api/CITypeAttr'
export default {
name: 'PasswordField',
props: {
ci_id: {
type: Number,
default: 0,
},
attr_id: {
type: Number,
default: 0,
},
},
data() {
return {
isShow: false,
password: '',
}
},
computed: {
showPassword() {
return '******'
},
...mapState('cmdbStore', ['isTableLoading']),
},
methods: {
getPassword() {
if (this.isShow) {
this.isShow = false
} else {
getAttrPassword(this.ci_id, this.attr_id).then((res) => {
this.password = res.value
this.isShow = true
})
}
},
},
}
</script>
<style></style>
<template>
<div>
<span v-if="!isShow && !isTableLoading">{{ showPassword }}</span>
<span v-else>{{ password }}</span>
<a
:style="{ marginLeft: '10px' }"
@click="
() => {
isShow = !isShow
}
"
><a-icon
:type="isShow ? 'eye-invisible' : 'eye'"
/></a>
</div>
</template>
<script>
import { mapState } from 'vuex'
export default {
name: 'PasswordField',
props: {
password: {
type: String,
default: '',
},
},
data() {
return {
isShow: false,
}
},
computed: {
showPassword() {
return '******'
},
...mapState('cmdbStore', ['isTableLoading']),
},
}
</script>
<style></style>

View File

@@ -1,144 +0,0 @@
<template>
<div class="authorization-wrapper">
<div class="authorization-header">
<a-space>
<span>Authorization Type</span>
<a-select size="small" v-model="authorizationType" style="width: 200px" :showSearch="true">
<a-select-option value="none">
None
</a-select-option>
<a-select-option value="BasicAuth">
Basic Auth
</a-select-option>
<a-select-option value="Bearer">
Bearer
</a-select-option>
<a-select-option value="APIKey">
APIKey
</a-select-option>
<a-select-option value="OAuth2.0">
OAuth2.0
</a-select-option>
</a-select>
</a-space>
</div>
<div style="margin-top:10px">
<table v-if="authorizationType === 'BasicAuth'">
<tr>
<td><a-input class="authorization-input" v-model="BasicAuth.username" placeholder="用户名" /></td>
</tr>
<tr>
<td><a-input class="authorization-input" v-model="BasicAuth.password" placeholder="密码" /></td>
</tr>
</table>
<table v-else-if="authorizationType === 'Bearer'">
<tr>
<td><a-input class="authorization-input" v-model="Bearer.token" placeholder="token" /></td>
</tr>
</table>
<table v-else-if="authorizationType === 'APIKey'">
<tr>
<td><a-input class="authorization-input" v-model="APIKey.key" placeholder="key" /></td>
</tr>
<tr>
<td><a-input class="authorization-input" v-model="APIKey.value" placeholder="value" /></td>
</tr>
</table>
<table v-else-if="authorizationType === 'OAuth2.0'">
<tr>
<td><a-input class="authorization-input" v-model="OAuth2.client_id" placeholder="client_id" /></td>
</tr>
<tr>
<td>
<a-input class="authorization-input" v-model="OAuth2.client_secret" placeholder="client_secret" />
</td>
</tr>
<tr>
<td>
<a-input
class="authorization-input"
v-model="OAuth2.authorization_base_url"
placeholder="authorization_base_url"
/>
</td>
</tr>
<tr>
<td>
<a-input class="authorization-input" v-model="OAuth2.token_url" placeholder="token_url" />
</td>
</tr>
<tr>
<td><a-input class="authorization-input" v-model="OAuth2.redirect_url" placeholder="redirect_url" /></td>
</tr>
<tr>
<td>
<a-input class="authorization-input" v-model="OAuth2.scope" placeholder="scope" />
</td>
</tr>
</table>
<a-empty
v-else
:image-style="{
height: '60px',
}"
>
<img slot="image" :src="require('@/assets/data_empty.png')" />
<span slot="description"> 暂无请求认证 </span>
</a-empty>
</div>
</div>
</template>
<script>
export default {
name: 'Authorization',
data() {
return {
authorizationType: 'none',
BasicAuth: {
username: '',
password: '',
},
Bearer: {
token: '',
},
APIKey: {
key: '',
value: '',
},
OAuth2: {
client_id: '',
client_secret: '',
authorization_base_url: '',
token_url: '',
redirect_url: '',
scope: '',
},
}
},
}
</script>
<style lang="less" scoped>
.authorization-wrapper {
table {
width: 100%;
border-collapse: collapse;
}
table,
td,
th {
border: 1px solid #f3f4f6;
}
.authorization-input {
border: none;
&:focus {
box-shadow: none;
}
}
}
</style>

Some files were not shown because too many files have changed in this diff Show More