mirror of
https://github.com/jumpserver/jumpserver.git
synced 2025-12-16 00:52:41 +00:00
Compare commits
104 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e6e8a0cbd | ||
|
|
493e61aa34 | ||
|
|
cdf3cf3e8f | ||
|
|
118564577e | ||
|
|
47f2df0a5b | ||
|
|
e4aafc236d | ||
|
|
b1c530bba8 | ||
|
|
95aa9781c3 | ||
|
|
9f6540afe3 | ||
|
|
832bb832ce | ||
|
|
501329a8db | ||
|
|
8913aacd1e | ||
|
|
e461fbdf50 | ||
|
|
3941539408 | ||
|
|
605db2d905 | ||
|
|
1ef3f24465 | ||
|
|
4090a0b123 | ||
|
|
a55e28fc87 | ||
|
|
82cf53181f | ||
|
|
78232aa900 | ||
|
|
d2c93aff66 | ||
|
|
516e2309c0 | ||
|
|
4688e46f97 | ||
|
|
1299f3da75 | ||
|
|
fe502cbe41 | ||
|
|
09bfac34f1 | ||
|
|
12a86d7244 | ||
|
|
269eea8802 | ||
|
|
72aa265dd7 | ||
|
|
e26716e1e1 | ||
|
|
80b9db417c | ||
|
|
d944b5f4ff | ||
|
|
1b84afee0c | ||
|
|
172b6edd28 | ||
|
|
e6f248bfa0 | ||
|
|
1f037b1933 | ||
|
|
ae9bbd2683 | ||
|
|
a0085c4eab | ||
|
|
ddb71c43c4 | ||
|
|
8227f44058 | ||
|
|
e81762d692 | ||
|
|
5b8fa1809c | ||
|
|
90ba6442dd | ||
|
|
a28334b6d8 | ||
|
|
692dd6c8c4 | ||
|
|
15992ad5b3 | ||
|
|
072c3155ca | ||
|
|
9cb5985947 | ||
|
|
0fd2f18240 | ||
|
|
9ca8ab218c | ||
|
|
fcd8356e90 | ||
|
|
64d093e677 | ||
|
|
11493b9f3d | ||
|
|
5a9c91d9dd | ||
|
|
25dcb9510c | ||
|
|
a38a1868ca | ||
|
|
bec9b97092 | ||
|
|
d5b9596e7d | ||
|
|
af85d551ad | ||
|
|
ab8c57894e | ||
|
|
0e0c9275bd | ||
|
|
21b4a8600c | ||
|
|
4cf5573c36 | ||
|
|
962ea67b84 | ||
|
|
31720c9dcc | ||
|
|
54fe4835f6 | ||
|
|
91649a3908 | ||
|
|
0a242c3e81 | ||
|
|
25d1b3334f | ||
|
|
ffde306a04 | ||
|
|
f1e29a91f7 | ||
|
|
ec2b3b4cda | ||
|
|
1a9d9e4145 | ||
|
|
a14f121fad | ||
|
|
a25da8d479 | ||
|
|
15fe7f810b | ||
|
|
f6a4253936 | ||
|
|
c3c5801d2e | ||
|
|
f0d564180c | ||
|
|
8ee7230ead | ||
|
|
90f03dda62 | ||
|
|
4e7a5d8d4f | ||
|
|
2ed0927b18 | ||
|
|
e98235ca27 | ||
|
|
1b052a8729 | ||
|
|
34b188bbe7 | ||
|
|
3e6cd1c1d3 | ||
|
|
f8e248f0af | ||
|
|
b331730422 | ||
|
|
de3865fa1d | ||
|
|
1bc913ab13 | ||
|
|
2f11a70341 | ||
|
|
c277aec561 | ||
|
|
2a53a20808 | ||
|
|
674ad40f67 | ||
|
|
78089e01a3 | ||
|
|
1b71350199 | ||
|
|
5d08438dad | ||
|
|
31ba0564e4 | ||
|
|
ea5b7cd921 | ||
|
|
3e541162e3 | ||
|
|
6e19384231 | ||
|
|
19903c80c3 | ||
|
|
0fca33d874 |
3
.github/ISSUE_TEMPLATE.md
vendored
3
.github/ISSUE_TEMPLATE.md
vendored
@@ -4,6 +4,9 @@
|
||||
##### 使用版本
|
||||
[请提供你使用的JumpServer版本 如 2.0.1 注: 1.4及以下版本不再提供支持]
|
||||
|
||||
##### 使用浏览器版本
|
||||
[请提供你使用的浏览器版本 如 Chrome 84.0.4147.105 ]
|
||||
|
||||
##### 问题复现步骤
|
||||
1. [步骤1]
|
||||
2. [步骤2]
|
||||
|
||||
12
.github/workflows/jms-generic-action-handler.yml
vendored
Normal file
12
.github/workflows/jms-generic-action-handler.yml
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
on: [push, pull_request, release]
|
||||
|
||||
name: JumpServer repos generic handler
|
||||
|
||||
jobs:
|
||||
generic_handler:
|
||||
name: Run generic handler
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: jumpserver/action-generic-handler@master
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.PRIVATE_TOKEN }}
|
||||
19
Dockerfile
19
Dockerfile
@@ -9,18 +9,23 @@ RUN cd utils && bash -ixeu build.sh
|
||||
|
||||
|
||||
FROM registry.fit2cloud.com/public/python:v3
|
||||
ARG PIP_MIRROR=https://pypi.douban.com/simple
|
||||
ENV PIP_MIRROR=$PIP_MIRROR
|
||||
ARG MYSQL_MIRROR=https://mirrors.tuna.tsinghua.edu.cn/mysql/yum/mysql57-community-el6/
|
||||
ENV MYSQL_MIRROR=$MYSQL_MIRROR
|
||||
|
||||
WORKDIR /opt/jumpserver
|
||||
COPY --from=stage-build /opt/jumpserver/release/jumpserver /opt/jumpserver
|
||||
|
||||
COPY ./requirements ./requirements
|
||||
RUN useradd jumpserver
|
||||
|
||||
RUN yum -y install epel-release && \
|
||||
echo -e "[mysql]\nname=mysql\nbaseurl=https://mirrors.tuna.tsinghua.edu.cn/mysql/yum/mysql57-community-el6/\ngpgcheck=0\nenabled=1" > /etc/yum.repos.d/mysql.repo
|
||||
|
||||
COPY . .
|
||||
echo -e "[mysql]\nname=mysql\nbaseurl=${MYSQL_MIRROR}\ngpgcheck=0\nenabled=1" > /etc/yum.repos.d/mysql.repo
|
||||
RUN yum -y install $(cat requirements/rpm_requirements.txt)
|
||||
RUN pip install --upgrade pip setuptools && pip install wheel && \
|
||||
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements/requirements.txt || pip install -r requirements/requirements.txt
|
||||
RUN pip install --upgrade pip setuptools==49.6.0 wheel -i ${PIP_MIRROR} && \
|
||||
pip config set global.index-url ${PIP_MIRROR}
|
||||
RUN pip install -r requirements/requirements.txt || pip install -r requirements/requirements.txt
|
||||
|
||||
COPY --from=stage-build /opt/jumpserver/release/jumpserver /opt/jumpserver
|
||||
RUN mkdir -p /root/.ssh/ && echo -e "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null" > /root/.ssh/config
|
||||
|
||||
RUN echo > config.yml
|
||||
|
||||
18
README.md
18
README.md
@@ -4,6 +4,10 @@
|
||||
[](https://www.djangoproject.com/)
|
||||
[](https://hub.docker.com/u/jumpserver)
|
||||
|
||||
|Developer Wanted|
|
||||
|------------------|
|
||||
|JumpServer 正在寻找开发者,一起为改变世界做些贡献吧,哪怕一点点,联系我 <ibuler@fit2cloud.com> |
|
||||
|
||||
JumpServer 是全球首款开源的堡垒机,使用 GNU GPL v2.0 开源协议,是符合 4A 规范的运维安全审计系统。
|
||||
|
||||
JumpServer 使用 Python / Django 为主进行开发,遵循 Web 2.0 规范,配备了业界领先的 Web Terminal 方案,交互界面美观、用户体验好。
|
||||
@@ -16,8 +20,8 @@ JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向
|
||||
|
||||
## 特色优势
|
||||
|
||||
- 开源: 零门槛,线上快速获取和安装, 修复版本视情况而定;
|
||||
, 修复版本视情况而定- 分布式: 轻松支持大规模并发访问;
|
||||
- 开源: 零门槛,线上快速获取和安装;
|
||||
- 分布式: 轻松支持大规模并发访问;
|
||||
- 无插件: 仅需浏览器,极致的 Web Terminal 使用体验;
|
||||
- 多云支持: 一套系统,同时管理不同云上面的资产;
|
||||
- 云端存储: 审计录像云端存储,永不丢失;
|
||||
@@ -202,6 +206,16 @@ v2.1.0 是 v2.0.0 之后的功能版本。
|
||||
- [完整文档](https://docs.jumpserver.org)
|
||||
- [演示视频](https://jumpserver.oss-cn-hangzhou.aliyuncs.com/jms-media/%E3%80%90%E6%BC%94%E7%A4%BA%E8%A7%86%E9%A2%91%E3%80%91Jumpserver%20%E5%A0%A1%E5%9E%92%E6%9C%BA%20V1.5.0%20%E6%BC%94%E7%A4%BA%E8%A7%86%E9%A2%91%20-%20final.mp4)
|
||||
|
||||
## 组件项目
|
||||
- [Lina](https://github.com/jumpserver/lina) JumpServer Web UI 项目
|
||||
- [Luna](https://github.com/jumpserver/luna) JumpServer Web Terminal 项目
|
||||
- [Koko](https://github.com/jumpserver/koko) JumpServer 字符协议 Connector 项目,替代原来 Python 版本的 [Coco](https://github.com/jumpserver/coco)
|
||||
- [Guacamole](https://github.com/jumpserver/docker-guacamole) JumpServer 图形协议 Connector 项目,依赖 [Apache Guacamole](https://guacamole.apache.org/)
|
||||
|
||||
## JumpServer 企业版
|
||||
- [申请企业版试用](https://jinshuju.net/f/kyOYpi)
|
||||
> 注:企业版支持离线安装,申请通过后会提供高速下载链接。
|
||||
|
||||
## 案例研究
|
||||
|
||||
- [JumpServer 堡垒机护航顺丰科技超大规模资产安全运维](https://blog.fit2cloud.com/?p=1147);
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
from .remote_app import *
|
||||
from .database_app import *
|
||||
from .k8s_app import *
|
||||
|
||||
20
apps/applications/api/k8s_app.py
Normal file
20
apps/applications/api/k8s_app.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# coding: utf-8
|
||||
#
|
||||
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
|
||||
from .. import models
|
||||
from .. import serializers
|
||||
from ..hands import IsOrgAdminOrAppUser
|
||||
|
||||
__all__ = [
|
||||
'K8sAppViewSet',
|
||||
]
|
||||
|
||||
|
||||
class K8sAppViewSet(OrgBulkModelViewSet):
|
||||
model = models.K8sApp
|
||||
filter_fields = ('name',)
|
||||
search_fields = filter_fields
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
serializer_class = serializers.K8sAppSerializer
|
||||
@@ -23,6 +23,7 @@ REMOTE_APP_TYPE_CHROME_FIELDS = [
|
||||
REMOTE_APP_TYPE_MYSQL_WORKBENCH_FIELDS = [
|
||||
{'name': 'mysql_workbench_ip'},
|
||||
{'name': 'mysql_workbench_name'},
|
||||
{'name': 'mysql_workbench_port'},
|
||||
{'name': 'mysql_workbench_username'},
|
||||
{'name': 'mysql_workbench_password', 'write_only': True}
|
||||
]
|
||||
|
||||
34
apps/applications/migrations/0005_k8sapp.py
Normal file
34
apps/applications/migrations/0005_k8sapp.py
Normal file
@@ -0,0 +1,34 @@
|
||||
# Generated by Django 2.2.13 on 2020-08-07 07:13
|
||||
|
||||
from django.db import migrations, models
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('applications', '0004_auto_20191218_1705'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='K8sApp',
|
||||
fields=[
|
||||
('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')),
|
||||
('updated_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Updated by')),
|
||||
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
|
||||
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
|
||||
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
|
||||
('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
|
||||
('name', models.CharField(max_length=128, verbose_name='Name')),
|
||||
('type', models.CharField(choices=[('k8s', 'Kubernetes')], default='k8s', max_length=128, verbose_name='Type')),
|
||||
('cluster', models.CharField(max_length=1024, verbose_name='Cluster')),
|
||||
('comment', models.TextField(blank=True, default='', max_length=128, verbose_name='Comment')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'KubernetesApp',
|
||||
'ordering': ('name',),
|
||||
'unique_together': {('org_id', 'name')},
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -1,2 +1,3 @@
|
||||
from .remote_app import *
|
||||
from .database_app import *
|
||||
from .k8s_app import *
|
||||
|
||||
27
apps/applications/models/k8s_app.py
Normal file
27
apps/applications/models/k8s_app.py
Normal file
@@ -0,0 +1,27 @@
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from common.db import models
|
||||
from orgs.mixins.models import OrgModelMixin
|
||||
|
||||
|
||||
class K8sApp(OrgModelMixin, models.JMSModel):
|
||||
class TYPE(models.ChoiceSet):
|
||||
K8S = 'k8s', _('Kubernetes')
|
||||
|
||||
name = models.CharField(max_length=128, verbose_name=_('Name'))
|
||||
type = models.CharField(
|
||||
default=TYPE.K8S, choices=TYPE.choices,
|
||||
max_length=128, verbose_name=_('Type')
|
||||
)
|
||||
cluster = models.CharField(max_length=1024, verbose_name=_('Cluster'))
|
||||
comment = models.TextField(
|
||||
max_length=128, default='', blank=True, verbose_name=_('Comment')
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class Meta:
|
||||
unique_together = [('org_id', 'name'), ]
|
||||
verbose_name = _('KubernetesApp')
|
||||
ordering = ('name', )
|
||||
@@ -1,2 +1,3 @@
|
||||
from .remote_app import *
|
||||
from .database_app import *
|
||||
from .k8s_app import *
|
||||
|
||||
22
apps/applications/serializers/k8s_app.py
Normal file
22
apps/applications/serializers/k8s_app.py
Normal file
@@ -0,0 +1,22 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||
from .. import models
|
||||
|
||||
__all__ = [
|
||||
'K8sAppSerializer',
|
||||
]
|
||||
|
||||
|
||||
class K8sAppSerializer(BulkOrgResourceModelSerializer):
|
||||
type_display = serializers.CharField(source='get_type_display', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.K8sApp
|
||||
fields = [
|
||||
'id', 'name', 'type', 'type_display', 'comment', 'created_by',
|
||||
'date_created', 'date_updated', 'cluster'
|
||||
]
|
||||
read_only_fields = [
|
||||
'id', 'created_by', 'date_created', 'date_updated',
|
||||
]
|
||||
@@ -12,6 +12,7 @@ app_name = 'applications'
|
||||
router = BulkRouter()
|
||||
router.register(r'remote-apps', api.RemoteAppViewSet, 'remote-app')
|
||||
router.register(r'database-apps', api.DatabaseAppViewSet, 'database-app')
|
||||
router.register(r'k8s-apps', api.K8sAppViewSet, 'k8s-app')
|
||||
|
||||
urlpatterns = [
|
||||
path('remote-apps/<uuid:pk>/connection-info/', api.RemoteAppConnectionInfoApi.as_view(), name='remote-app-connection-info'),
|
||||
|
||||
34
apps/assets/migrations/0053_auto_20200723_1232.py
Normal file
34
apps/assets/migrations/0053_auto_20200723_1232.py
Normal file
@@ -0,0 +1,34 @@
|
||||
# Generated by Django 2.2.10 on 2020-07-23 04:32
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0052_auto_20200715_1535'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='adminuser',
|
||||
name='username',
|
||||
field=models.CharField(blank=True, db_index=True, max_length=128, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='authbook',
|
||||
name='username',
|
||||
field=models.CharField(blank=True, db_index=True, max_length=128, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='gateway',
|
||||
name='username',
|
||||
field=models.CharField(blank=True, db_index=True, max_length=128, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='systemuser',
|
||||
name='username',
|
||||
field=models.CharField(blank=True, db_index=True, max_length=128, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username'),
|
||||
),
|
||||
]
|
||||
23
apps/assets/migrations/0054_auto_20200807_1032.py
Normal file
23
apps/assets/migrations/0054_auto_20200807_1032.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 2.2.13 on 2020-08-07 02:32
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0053_auto_20200723_1232'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='systemuser',
|
||||
name='token',
|
||||
field=models.TextField(default='', verbose_name='Token'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='systemuser',
|
||||
name='protocol',
|
||||
field=models.CharField(choices=[('ssh', 'ssh'), ('rdp', 'rdp'), ('telnet', 'telnet'), ('vnc', 'vnc'), ('mysql', 'mysql'), ('k8s', 'k8s')], default='ssh', max_length=16, verbose_name='Protocol'),
|
||||
),
|
||||
]
|
||||
23
apps/assets/migrations/0055_auto_20200811_1845.py
Normal file
23
apps/assets/migrations/0055_auto_20200811_1845.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 2.2.13 on 2020-08-11 10:45
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0054_auto_20200807_1032'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='systemuser',
|
||||
name='home',
|
||||
field=models.CharField(blank=True, default='', max_length=4096, verbose_name='Home'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='systemuser',
|
||||
name='system_groups',
|
||||
field=models.CharField(blank=True, default='', max_length=4096, verbose_name='System groups'),
|
||||
),
|
||||
]
|
||||
@@ -230,7 +230,7 @@ class AuthMixin:
|
||||
class BaseUser(OrgModelMixin, AuthMixin, ConnectivityMixin):
|
||||
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||
name = models.CharField(max_length=128, verbose_name=_('Name'))
|
||||
username = models.CharField(max_length=32, blank=True, verbose_name=_('Username'), validators=[alphanumeric], db_index=True)
|
||||
username = models.CharField(max_length=128, blank=True, verbose_name=_('Username'), validators=[alphanumeric], db_index=True)
|
||||
password = fields.EncryptCharField(max_length=256, blank=True, null=True, verbose_name=_('Password'))
|
||||
private_key = fields.EncryptTextField(blank=True, null=True, verbose_name=_('SSH private key'))
|
||||
public_key = fields.EncryptTextField(blank=True, null=True, verbose_name=_('SSH public key'))
|
||||
|
||||
@@ -9,6 +9,7 @@ from django.utils.translation import ugettext_lazy as _
|
||||
from django.core.validators import MinValueValidator, MaxValueValidator
|
||||
|
||||
from common.utils import signer
|
||||
from common.fields.model import JsonListCharField
|
||||
from .base import BaseUser
|
||||
from .asset import Asset
|
||||
|
||||
@@ -91,12 +92,14 @@ class SystemUser(BaseUser):
|
||||
PROTOCOL_TELNET = 'telnet'
|
||||
PROTOCOL_VNC = 'vnc'
|
||||
PROTOCOL_MYSQL = 'mysql'
|
||||
PROTOCOL_K8S = 'k8s'
|
||||
PROTOCOL_CHOICES = (
|
||||
(PROTOCOL_SSH, 'ssh'),
|
||||
(PROTOCOL_RDP, 'rdp'),
|
||||
(PROTOCOL_TELNET, 'telnet'),
|
||||
(PROTOCOL_VNC, 'vnc'),
|
||||
(PROTOCOL_MYSQL, 'mysql'),
|
||||
(PROTOCOL_K8S, 'k8s'),
|
||||
)
|
||||
|
||||
LOGIN_AUTO = 'auto'
|
||||
@@ -118,6 +121,9 @@ class SystemUser(BaseUser):
|
||||
login_mode = models.CharField(choices=LOGIN_MODE_CHOICES, default=LOGIN_AUTO, max_length=10, verbose_name=_('Login mode'))
|
||||
cmd_filters = models.ManyToManyField('CommandFilter', related_name='system_users', verbose_name=_("Command filter"), blank=True)
|
||||
sftp_root = models.CharField(default='tmp', max_length=128, verbose_name=_("SFTP Root"))
|
||||
token = models.TextField(default='', verbose_name=_('Token'))
|
||||
home = models.CharField(max_length=4096, default='', verbose_name=_('Home'), blank=True)
|
||||
system_groups = models.CharField(default='', max_length=4096, verbose_name=_('System groups'), blank=True)
|
||||
_prefer = 'system_user'
|
||||
|
||||
def __str__(self):
|
||||
|
||||
@@ -33,13 +33,15 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
||||
'login_mode', 'login_mode_display',
|
||||
'priority', 'username_same_with_user',
|
||||
'auto_push', 'cmd_filters', 'sudo', 'shell', 'comment',
|
||||
'auto_generate_key', 'sftp_root',
|
||||
'assets_amount', 'date_created', 'created_by'
|
||||
'auto_generate_key', 'sftp_root', 'token',
|
||||
'assets_amount', 'date_created', 'created_by',
|
||||
'home', 'system_groups'
|
||||
]
|
||||
extra_kwargs = {
|
||||
'password': {"write_only": True},
|
||||
'public_key': {"write_only": True},
|
||||
'private_key': {"write_only": True},
|
||||
'token': {"write_only": True},
|
||||
'nodes_amount': {'label': _('Node')},
|
||||
'assets_amount': {'label': _('Asset')},
|
||||
'login_mode_display': {'label': _('Login mode display')},
|
||||
@@ -143,17 +145,25 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
||||
|
||||
|
||||
class SystemUserListSerializer(SystemUserSerializer):
|
||||
|
||||
class Meta(SystemUserSerializer.Meta):
|
||||
fields = [
|
||||
'id', 'name', 'username', 'protocol',
|
||||
'password', 'public_key', 'private_key',
|
||||
'login_mode', 'login_mode_display',
|
||||
'priority', "username_same_with_user",
|
||||
'auto_push', 'sudo', 'shell', 'comment',
|
||||
"assets_amount",
|
||||
"assets_amount", 'home', 'system_groups',
|
||||
'auto_generate_key',
|
||||
'sftp_root',
|
||||
]
|
||||
|
||||
extra_kwargs = {
|
||||
'password': {"write_only": True},
|
||||
'public_key': {"write_only": True},
|
||||
'private_key': {"write_only": True},
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def setup_eager_loading(cls, queryset):
|
||||
""" Perform necessary eager loading of data. """
|
||||
@@ -169,7 +179,7 @@ class SystemUserWithAuthInfoSerializer(SystemUserSerializer):
|
||||
'login_mode', 'login_mode_display',
|
||||
'priority', 'username_same_with_user',
|
||||
'auto_push', 'sudo', 'shell', 'comment',
|
||||
'auto_generate_key', 'sftp_root',
|
||||
'auto_generate_key', 'sftp_root', 'token'
|
||||
]
|
||||
extra_kwargs = {
|
||||
'nodes_amount': {'label': _('Node')},
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
from itertools import groupby
|
||||
from celery import shared_task
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.db.models import Empty
|
||||
|
||||
from common.utils import encrypt_password, get_logger
|
||||
from orgs.utils import tmp_to_org, org_aware_func
|
||||
from orgs.utils import org_aware_func
|
||||
from . import const
|
||||
from .utils import clean_ansible_task_hosts, group_asset_by_platform
|
||||
|
||||
@@ -17,20 +18,42 @@ __all__ = [
|
||||
]
|
||||
|
||||
|
||||
def _split_by_comma(raw: str):
|
||||
try:
|
||||
return [i.strip() for i in raw.split(',')]
|
||||
except AttributeError:
|
||||
return []
|
||||
|
||||
|
||||
def _dump_args(args: dict):
|
||||
return ' '.join([f'{k}={v}' for k, v in args.items() if v is not Empty])
|
||||
|
||||
|
||||
def get_push_unixlike_system_user_tasks(system_user, username=None):
|
||||
if username is None:
|
||||
username = system_user.username
|
||||
password = system_user.password
|
||||
public_key = system_user.public_key
|
||||
|
||||
groups = _split_by_comma(system_user.system_groups)
|
||||
|
||||
if groups:
|
||||
groups = '"%s"' % ','.join(groups)
|
||||
|
||||
add_user_args = {
|
||||
'name': username,
|
||||
'shell': system_user.shell or Empty,
|
||||
'state': 'present',
|
||||
'home': system_user.home or Empty,
|
||||
'groups': groups or Empty
|
||||
}
|
||||
|
||||
tasks = [
|
||||
{
|
||||
'name': 'Add user {}'.format(username),
|
||||
'action': {
|
||||
'module': 'user',
|
||||
'args': 'name={} shell={} state=present'.format(
|
||||
username, system_user.shell or '/bin/bash',
|
||||
),
|
||||
'args': _dump_args(add_user_args),
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -102,6 +125,11 @@ def get_push_windows_system_user_tasks(system_user, username=None):
|
||||
if username is None:
|
||||
username = system_user.username
|
||||
password = system_user.password
|
||||
groups = {'Users', 'Remote Desktop Users'}
|
||||
if system_user.system_groups:
|
||||
groups.update(_split_by_comma(system_user.system_groups))
|
||||
groups = ','.join(groups)
|
||||
|
||||
tasks = []
|
||||
if not password:
|
||||
return tasks
|
||||
@@ -116,9 +144,9 @@ def get_push_windows_system_user_tasks(system_user, username=None):
|
||||
'update_password=always '
|
||||
'password_expired=no '
|
||||
'password_never_expires=yes '
|
||||
'groups="Users,Remote Desktop Users" '
|
||||
'groups="{}" '
|
||||
'groups_action=add '
|
||||
''.format(username, username, password),
|
||||
''.format(username, username, password, groups),
|
||||
}
|
||||
}
|
||||
tasks.append(task)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
# coding:utf-8
|
||||
from django.urls import path, re_path
|
||||
from rest_framework_nested import routers
|
||||
# from rest_framework.routers import DefaultRouter
|
||||
from rest_framework_bulk.routes import BulkRouter
|
||||
|
||||
from common import api as capi
|
||||
|
||||
@@ -125,6 +125,8 @@ class TreeService(Tree):
|
||||
|
||||
def assets(self, nid):
|
||||
node = self.get_node(nid)
|
||||
if not node:
|
||||
return set()
|
||||
return node.data.get("assets", set())
|
||||
|
||||
def valid_assets(self, nid):
|
||||
@@ -132,6 +134,8 @@ class TreeService(Tree):
|
||||
|
||||
def all_assets(self, nid):
|
||||
node = self.get_node(nid)
|
||||
if not node:
|
||||
return set()
|
||||
if node.data is None:
|
||||
node.data = {}
|
||||
all_assets = node.data.get("all_assets")
|
||||
|
||||
@@ -43,7 +43,7 @@ class UserLoginLogViewSet(ListModelMixin, CommonGenericViewSet):
|
||||
|
||||
@staticmethod
|
||||
def get_org_members():
|
||||
users = current_org.get_org_members().values_list('username', flat=True)
|
||||
users = current_org.get_members().values_list('username', flat=True)
|
||||
return users
|
||||
|
||||
def get_queryset(self):
|
||||
@@ -79,7 +79,7 @@ class PasswordChangeLogViewSet(ListModelMixin, CommonGenericViewSet):
|
||||
ordering = ['-datetime']
|
||||
|
||||
def get_queryset(self):
|
||||
users = current_org.get_org_members()
|
||||
users = current_org.get_members()
|
||||
queryset = super().get_queryset().filter(
|
||||
user__in=[user.__str__() for user in users]
|
||||
)
|
||||
@@ -107,7 +107,7 @@ class CommandExecutionViewSet(ListModelMixin, OrgGenericViewSet):
|
||||
class CommandExecutionHostRelationViewSet(OrgRelationMixin, OrgBulkModelViewSet):
|
||||
serializer_class = CommandExecutionHostsRelationSerializer
|
||||
m2m_field = CommandExecution.hosts.field
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
permission_classes = [IsOrgAdmin | IsOrgAuditor]
|
||||
filter_fields = [
|
||||
'id', 'asset', 'commandexecution'
|
||||
]
|
||||
|
||||
@@ -20,7 +20,7 @@ class CurrentOrgMembersFilter(filters.BaseFilterBackend):
|
||||
]
|
||||
|
||||
def _get_user_list(self):
|
||||
users = current_org.get_org_members(exclude=('Auditor',))
|
||||
users = current_org.get_members(exclude=('Auditor',))
|
||||
return users
|
||||
|
||||
def filter_queryset(self, request, queryset, view):
|
||||
|
||||
18
apps/audits/migrations/0010_auto_20200811_1122.py
Normal file
18
apps/audits/migrations/0010_auto_20200811_1122.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 2.2.13 on 2020-08-11 03:22
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('audits', '0009_auto_20200624_1654'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='operatelog',
|
||||
name='datetime',
|
||||
field=models.DateTimeField(auto_now=True, db_index=True, verbose_name='Datetime'),
|
||||
),
|
||||
]
|
||||
@@ -58,7 +58,7 @@ class OperateLog(OrgModelMixin):
|
||||
resource_type = models.CharField(max_length=64, verbose_name=_("Resource Type"))
|
||||
resource = models.CharField(max_length=128, verbose_name=_("Resource"))
|
||||
remote_addr = models.CharField(max_length=128, verbose_name=_("Remote addr"), blank=True, null=True)
|
||||
datetime = models.DateTimeField(auto_now=True, verbose_name=_('Datetime'))
|
||||
datetime = models.DateTimeField(auto_now=True, verbose_name=_('Datetime'), db_index=True)
|
||||
|
||||
def __str__(self):
|
||||
return "<{}> {} <{}>".format(self.user, self.action, self.resource)
|
||||
@@ -124,7 +124,7 @@ class UserLoginLog(models.Model):
|
||||
Q(username__contains=keyword)
|
||||
)
|
||||
if not current_org.is_root():
|
||||
username_list = current_org.get_org_members().values_list('username', flat=True)
|
||||
username_list = current_org.get_members().values_list('username', flat=True)
|
||||
login_logs = login_logs.filter(username__in=username_list)
|
||||
return login_logs
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ def clean_login_log_period():
|
||||
try:
|
||||
days = int(settings.LOGIN_LOG_KEEP_DAYS)
|
||||
except ValueError:
|
||||
days = 90
|
||||
days = 9999
|
||||
expired_day = now - datetime.timedelta(days=days)
|
||||
UserLoginLog.objects.filter(datetime__lt=expired_day).delete()
|
||||
|
||||
@@ -28,6 +28,6 @@ def clean_operation_log_period():
|
||||
try:
|
||||
days = int(settings.LOGIN_LOG_KEEP_DAYS)
|
||||
except ValueError:
|
||||
days = 90
|
||||
days = 9999
|
||||
expired_day = now - datetime.timedelta(days=days)
|
||||
OperateLog.objects.filter(datetime__lt=expired_day).delete()
|
||||
|
||||
@@ -6,3 +6,4 @@ from .token import *
|
||||
from .mfa import *
|
||||
from .access_key import *
|
||||
from .login_confirm import *
|
||||
from .sso import *
|
||||
|
||||
@@ -34,16 +34,6 @@ class LoginConfirmSettingUpdateApi(UpdateAPIView):
|
||||
class TicketStatusApi(mixins.AuthMixin, APIView):
|
||||
permission_classes = ()
|
||||
|
||||
def get_ticket(self):
|
||||
from tickets.models import Ticket
|
||||
ticket_id = self.request.session.get("auth_ticket_id")
|
||||
logger.debug('Login confirm ticket id: {}'.format(ticket_id))
|
||||
if not ticket_id:
|
||||
ticket = None
|
||||
else:
|
||||
ticket = get_object_or_none(Ticket, pk=ticket_id)
|
||||
return ticket
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
try:
|
||||
self.check_user_login_confirm()
|
||||
|
||||
86
apps/authentication/api/sso.py
Normal file
86
apps/authentication/api/sso.py
Normal file
@@ -0,0 +1,86 @@
|
||||
from uuid import UUID
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from django.contrib.auth import login
|
||||
from django.conf import settings
|
||||
from django.http.response import HttpResponseRedirect
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.request import Request
|
||||
|
||||
from common.utils.timezone import utcnow
|
||||
from common.const.http import POST, GET
|
||||
from common.drf.api import JmsGenericViewSet
|
||||
from common.drf.serializers import EmptySerializer
|
||||
from common.permissions import IsSuperUser
|
||||
from common.utils import reverse
|
||||
from users.models import User
|
||||
from ..serializers import SSOTokenSerializer
|
||||
from ..models import SSOToken
|
||||
from ..filters import AuthKeyQueryDeclaration
|
||||
from ..mixins import AuthMixin
|
||||
from ..errors import SSOAuthClosed
|
||||
|
||||
NEXT_URL = 'next'
|
||||
AUTH_KEY = 'authkey'
|
||||
|
||||
|
||||
class SSOViewSet(AuthMixin, JmsGenericViewSet):
|
||||
queryset = SSOToken.objects.all()
|
||||
serializer_classes = {
|
||||
'login_url': SSOTokenSerializer,
|
||||
'login': EmptySerializer
|
||||
}
|
||||
|
||||
@action(methods=[POST], detail=False, permission_classes=[IsSuperUser], url_path='login-url')
|
||||
def login_url(self, request, *args, **kwargs):
|
||||
if not settings.AUTH_SSO:
|
||||
raise SSOAuthClosed()
|
||||
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
username = serializer.validated_data['username']
|
||||
user = User.objects.get(username=username)
|
||||
next_url = serializer.validated_data.get(NEXT_URL)
|
||||
|
||||
operator = request.user.username
|
||||
# TODO `created_by` 和 `created_by` 可以通过 `ThreadLocal` 统一处理
|
||||
token = SSOToken.objects.create(user=user, created_by=operator, updated_by=operator)
|
||||
query = {
|
||||
AUTH_KEY: token.authkey,
|
||||
NEXT_URL: next_url or ''
|
||||
}
|
||||
login_url = '%s?%s' % (reverse('api-auth:sso-login', external=True), urlencode(query))
|
||||
return Response(data={'login_url': login_url})
|
||||
|
||||
@action(methods=[GET], detail=False, filter_backends=[AuthKeyQueryDeclaration], permission_classes=[])
|
||||
def login(self, request: Request, *args, **kwargs):
|
||||
"""
|
||||
此接口违反了 `Restful` 的规范
|
||||
`GET` 应该是安全的方法,但此接口是不安全的
|
||||
"""
|
||||
authkey = request.query_params.get(AUTH_KEY)
|
||||
next_url = request.query_params.get(NEXT_URL)
|
||||
if not next_url or not next_url.startswith('/'):
|
||||
next_url = reverse('index')
|
||||
|
||||
try:
|
||||
authkey = UUID(authkey)
|
||||
token = SSOToken.objects.get(authkey=authkey, expired=False)
|
||||
# 先过期,只能访问这一次
|
||||
token.expired = True
|
||||
token.save()
|
||||
except (ValueError, SSOToken.DoesNotExist):
|
||||
self.send_auth_signal(success=False, reason='authkey_invalid')
|
||||
return HttpResponseRedirect(reverse('authentication:login'))
|
||||
|
||||
# 判断是否过期
|
||||
if (utcnow().timestamp() - token.date_created.timestamp()) > settings.AUTH_SSO_AUTHKEY_TTL:
|
||||
self.send_auth_signal(success=False, reason='authkey_timeout')
|
||||
return HttpResponseRedirect(reverse('authentication:login'))
|
||||
|
||||
user = token.user
|
||||
login(self.request, user, 'authentication.backends.api.SSOAuthentication')
|
||||
self.send_auth_signal(success=True, user=user)
|
||||
return HttpResponseRedirect(next_url)
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from django.shortcuts import redirect
|
||||
from rest_framework.permissions import AllowAny
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.generics import CreateAPIView
|
||||
@@ -40,3 +40,5 @@ class TokenCreateApi(AuthMixin, CreateAPIView):
|
||||
return Response(e.as_data(), status=400)
|
||||
except errors.NeedMoreInfoError as e:
|
||||
return Response(e.as_data(), status=200)
|
||||
except errors.PasswdTooSimple as e:
|
||||
return redirect(e.url)
|
||||
|
||||
@@ -5,14 +5,13 @@ import uuid
|
||||
import time
|
||||
|
||||
from django.core.cache import cache
|
||||
from django.conf import settings
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.utils.six import text_type
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.backends import ModelBackend
|
||||
from rest_framework import HTTP_HEADER_ENCODING
|
||||
from rest_framework import authentication, exceptions
|
||||
from common.auth import signature
|
||||
from rest_framework.authentication import CSRFCheck
|
||||
|
||||
from common.utils import get_object_or_none, make_signature, http_to_unixtime
|
||||
from ..models import AccessKey, PrivateToken
|
||||
@@ -197,3 +196,12 @@ class SignatureAuthentication(signature.SignatureAuthentication):
|
||||
return user, secret
|
||||
except AccessKey.DoesNotExist:
|
||||
return None, None
|
||||
|
||||
|
||||
class SSOAuthentication(ModelBackend):
|
||||
"""
|
||||
什么也不做呀😺
|
||||
"""
|
||||
|
||||
def authenticate(self, request, sso_token=None, **kwargs):
|
||||
pass
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
import traceback
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth import get_user_model, authenticate
|
||||
from radiusauth.backends import RADIUSBackend, RADIUSRealmBackend
|
||||
from django.conf import settings
|
||||
|
||||
from pyrad.packet import AccessRequest
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
@@ -27,16 +27,23 @@ class CreateUserMixin:
|
||||
user.save()
|
||||
return user
|
||||
|
||||
def authenticate(self, *args, **kwargs):
|
||||
# 校验用户时,会传入public_key参数,父类authentication中不接受public_key参数,所以要pop掉
|
||||
# TODO:需要优化各backend的authenticate方法,django进行调用前会检测各authenticate的参数
|
||||
kwargs.pop('public_key', None)
|
||||
return super().authenticate(*args, *kwargs)
|
||||
def _perform_radius_auth(self, client, packet):
|
||||
# TODO: 等待官方库修复这个BUG
|
||||
try:
|
||||
return super()._perform_radius_auth(client, packet)
|
||||
except UnicodeError as e:
|
||||
import sys
|
||||
tb = ''.join(traceback.format_exception(*sys.exc_info(), limit=2, chain=False))
|
||||
if tb.find("cl.decode") != -1:
|
||||
return [], False, False
|
||||
return None
|
||||
|
||||
|
||||
class RadiusBackend(CreateUserMixin, RADIUSBackend):
|
||||
pass
|
||||
def authenticate(self, request, username='', password='', **kwargs):
|
||||
return super().authenticate(request, username=username, password=password)
|
||||
|
||||
|
||||
class RadiusRealmBackend(CreateUserMixin, RADIUSRealmBackend):
|
||||
pass
|
||||
def authenticate(self, request, username='', password='', realm=None, **kwargs):
|
||||
return super().authenticate(request, username=username, password=password, realm=realm)
|
||||
|
||||
2
apps/authentication/const.py
Normal file
2
apps/authentication/const.py
Normal file
@@ -0,0 +1,2 @@
|
||||
RSA_PRIVATE_KEY = 'rsa_private_key'
|
||||
RSA_PUBLIC_KEY = 'rsa_public_key'
|
||||
@@ -4,6 +4,7 @@ from django.utils.translation import ugettext_lazy as _
|
||||
from django.urls import reverse
|
||||
from django.conf import settings
|
||||
|
||||
from common.exceptions import JMSException
|
||||
from .signals import post_auth_failed
|
||||
from users.utils import (
|
||||
increase_login_failed_count, get_login_failed_count
|
||||
@@ -205,3 +206,17 @@ class LoginConfirmOtherError(LoginConfirmBaseError):
|
||||
def __init__(self, ticket_id, status):
|
||||
msg = login_confirm_error_msg.format(status)
|
||||
super().__init__(ticket_id=ticket_id, msg=msg)
|
||||
|
||||
|
||||
class SSOAuthClosed(JMSException):
|
||||
default_code = 'sso_auth_closed'
|
||||
default_detail = _('SSO auth closed')
|
||||
|
||||
|
||||
class PasswdTooSimple(JMSException):
|
||||
default_code = 'passwd_too_simple'
|
||||
default_detail = _('Your password is too simple, please change it for security')
|
||||
|
||||
def __init__(self, url, *args, **kwargs):
|
||||
super(PasswdTooSimple, self).__init__(*args, **kwargs)
|
||||
self.url = url
|
||||
|
||||
15
apps/authentication/filters.py
Normal file
15
apps/authentication/filters.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from rest_framework import filters
|
||||
from rest_framework.compat import coreapi, coreschema
|
||||
|
||||
|
||||
class AuthKeyQueryDeclaration(filters.BaseFilterBackend):
|
||||
def get_schema_fields(self, view):
|
||||
return [
|
||||
coreapi.Field(
|
||||
name='authkey', location='query', required=True, type='string',
|
||||
schema=coreschema.String(
|
||||
title='authkey',
|
||||
description='authkey'
|
||||
)
|
||||
)
|
||||
]
|
||||
@@ -2,6 +2,7 @@
|
||||
#
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from captcha.fields import CaptchaField
|
||||
|
||||
@@ -21,9 +22,24 @@ class UserLoginForm(forms.Form):
|
||||
)
|
||||
|
||||
|
||||
class UserLoginCaptchaForm(UserLoginForm):
|
||||
class UserCheckOtpCodeForm(forms.Form):
|
||||
otp_code = forms.CharField(label=_('MFA code'), max_length=6)
|
||||
|
||||
|
||||
class CaptchaMixin(forms.Form):
|
||||
captcha = CaptchaField()
|
||||
|
||||
|
||||
class UserCheckOtpCodeForm(forms.Form):
|
||||
otp_code = forms.CharField(label=_('MFA code'), max_length=6)
|
||||
class ChallengeMixin(forms.Form):
|
||||
challenge = forms.CharField(label=_('MFA code'), max_length=6,
|
||||
required=False)
|
||||
|
||||
|
||||
def get_user_login_form_cls(*, captcha=False):
|
||||
bases = []
|
||||
if settings.SECURITY_LOGIN_CAPTCHA_ENABLED and captcha:
|
||||
bases.append(CaptchaMixin)
|
||||
if settings.SECURITY_LOGIN_CHALLENGE_ENABLED:
|
||||
bases.append(ChallengeMixin)
|
||||
bases.append(UserLoginForm)
|
||||
return type('UserLoginForm', tuple(bases), {})
|
||||
|
||||
32
apps/authentication/migrations/0004_ssotoken.py
Normal file
32
apps/authentication/migrations/0004_ssotoken.py
Normal file
@@ -0,0 +1,32 @@
|
||||
# Generated by Django 2.2.10 on 2020-07-31 08:36
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('authentication', '0003_loginconfirmsetting'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='SSOToken',
|
||||
fields=[
|
||||
('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')),
|
||||
('updated_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Updated by')),
|
||||
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
|
||||
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
|
||||
('authkey', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False, verbose_name='Token')),
|
||||
('expired', models.BooleanField(default=False, verbose_name='Expired')),
|
||||
('user', models.ForeignKey(db_constraint=False, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL, verbose_name='User')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -1,7 +1,13 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from urllib.parse import urlencode
|
||||
from functools import partial
|
||||
import time
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import authenticate
|
||||
from django.shortcuts import reverse
|
||||
from django.contrib.auth import BACKEND_SESSION_KEY
|
||||
|
||||
from common.utils import get_object_or_none, get_request_ip, get_logger
|
||||
from users.models import User
|
||||
@@ -9,8 +15,9 @@ from users.utils import (
|
||||
is_block_login, clean_failed_count
|
||||
)
|
||||
from . import errors
|
||||
from .utils import check_user_valid
|
||||
from .utils import rsa_decrypt
|
||||
from .signals import post_auth_success, post_auth_failed
|
||||
from .const import RSA_PRIVATE_KEY
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
@@ -21,8 +28,14 @@ class AuthMixin:
|
||||
def get_user_from_session(self):
|
||||
if self.request.session.is_empty():
|
||||
raise errors.SessionEmptyError()
|
||||
if self.request.user and not self.request.user.is_anonymous:
|
||||
return self.request.user
|
||||
|
||||
if all((self.request.user,
|
||||
not self.request.user.is_anonymous,
|
||||
BACKEND_SESSION_KEY in self.request.session)):
|
||||
user = self.request.user
|
||||
user.backend = self.request.session[BACKEND_SESSION_KEY]
|
||||
return user
|
||||
|
||||
user_id = self.request.session.get('user_id')
|
||||
if not user_id:
|
||||
user = None
|
||||
@@ -50,25 +63,54 @@ class AuthMixin:
|
||||
logger.warn('Ip was blocked' + ': ' + username + ':' + ip)
|
||||
raise errors.BlockLoginError(username=username, ip=ip)
|
||||
|
||||
def check_user_auth(self):
|
||||
def decrypt_passwd(self, raw_passwd):
|
||||
# 获取解密密钥,对密码进行解密
|
||||
rsa_private_key = self.request.session.get(RSA_PRIVATE_KEY)
|
||||
if rsa_private_key is not None:
|
||||
try:
|
||||
return rsa_decrypt(raw_passwd, rsa_private_key)
|
||||
except Exception as e:
|
||||
logger.error(e, exc_info=True)
|
||||
logger.error(f'Decrypt password faild: password[{raw_passwd}] rsa_private_key[{rsa_private_key}]')
|
||||
return None
|
||||
return raw_passwd
|
||||
|
||||
def check_user_auth(self, decrypt_passwd=False):
|
||||
self.check_is_block()
|
||||
request = self.request
|
||||
if hasattr(request, 'data'):
|
||||
username = request.data.get('username', '')
|
||||
password = request.data.get('password', '')
|
||||
public_key = request.data.get('public_key', '')
|
||||
data = request.data
|
||||
else:
|
||||
username = request.POST.get('username', '')
|
||||
password = request.POST.get('password', '')
|
||||
public_key = request.POST.get('public_key', '')
|
||||
user, error = check_user_valid(
|
||||
request=request, username=username, password=password, public_key=public_key
|
||||
)
|
||||
data = request.POST
|
||||
username = data.get('username', '')
|
||||
password = data.get('password', '')
|
||||
challenge = data.get('challenge', '')
|
||||
public_key = data.get('public_key', '')
|
||||
ip = self.get_request_ip()
|
||||
|
||||
CredentialError = partial(errors.CredentialError, username=username, ip=ip, request=request)
|
||||
|
||||
if decrypt_passwd:
|
||||
password = self.decrypt_passwd(password)
|
||||
if not password:
|
||||
raise CredentialError(error=errors.reason_password_decrypt_failed)
|
||||
|
||||
user = authenticate(request,
|
||||
username=username,
|
||||
password=password + challenge.strip(),
|
||||
public_key=public_key)
|
||||
|
||||
if not user:
|
||||
raise errors.CredentialError(
|
||||
username=username, error=error, ip=ip, request=request
|
||||
)
|
||||
raise CredentialError(error=errors.reason_password_failed)
|
||||
elif user.is_expired:
|
||||
raise CredentialError(error=errors.reason_user_inactive)
|
||||
elif not user.is_active:
|
||||
raise CredentialError(error=errors.reason_user_inactive)
|
||||
elif user.password_has_expired:
|
||||
raise CredentialError(error=errors.reason_password_expired)
|
||||
|
||||
self._check_passwd_is_too_simple(user, password)
|
||||
|
||||
clean_failed_count(username, ip)
|
||||
request.session['auth_password'] = 1
|
||||
request.session['user_id'] = str(user.id)
|
||||
@@ -76,14 +118,30 @@ class AuthMixin:
|
||||
request.session['auth_backend'] = auth_backend
|
||||
return user
|
||||
|
||||
def check_user_auth_if_need(self):
|
||||
@classmethod
|
||||
def _check_passwd_is_too_simple(cls, user, password):
|
||||
if user.is_superuser and password == 'admin':
|
||||
reset_passwd_url = reverse('authentication:reset-password')
|
||||
query_str = urlencode({
|
||||
'token': user.generate_reset_token()
|
||||
})
|
||||
reset_passwd_url = f'{reset_passwd_url}?{query_str}'
|
||||
|
||||
flash_page_url = reverse('authentication:passwd-too-simple-flash-msg')
|
||||
query_str = urlencode({
|
||||
'redirect_url': reset_passwd_url
|
||||
})
|
||||
|
||||
raise errors.PasswdTooSimple(f'{flash_page_url}?{query_str}')
|
||||
|
||||
def check_user_auth_if_need(self, decrypt_passwd=False):
|
||||
request = self.request
|
||||
if request.session.get('auth_password') and \
|
||||
request.session.get('user_id'):
|
||||
user = self.get_user_from_session()
|
||||
if user:
|
||||
return user
|
||||
return self.check_user_auth()
|
||||
return self.check_user_auth(decrypt_passwd=decrypt_passwd)
|
||||
|
||||
def check_user_mfa_if_need(self, user):
|
||||
if self.request.session.get('auth_mfa'):
|
||||
@@ -112,12 +170,12 @@ class AuthMixin:
|
||||
if not ticket_id:
|
||||
ticket = None
|
||||
else:
|
||||
ticket = get_object_or_none(Ticket, pk=ticket_id)
|
||||
ticket = Ticket.origin_objects.get(pk=ticket_id)
|
||||
return ticket
|
||||
|
||||
def get_ticket_or_create(self, confirm_setting):
|
||||
ticket = self.get_ticket()
|
||||
if not ticket or ticket.status == ticket.STATUS_CLOSED:
|
||||
if not ticket or ticket.status == ticket.STATUS.CLOSED:
|
||||
ticket = confirm_setting.create_confirm_ticket(self.request)
|
||||
self.request.session['auth_ticket_id'] = str(ticket.id)
|
||||
return ticket
|
||||
@@ -126,12 +184,12 @@ class AuthMixin:
|
||||
ticket = self.get_ticket()
|
||||
if not ticket:
|
||||
raise errors.LoginConfirmOtherError('', "Not found")
|
||||
if ticket.status == ticket.STATUS_OPEN:
|
||||
if ticket.status == ticket.STATUS.OPEN:
|
||||
raise errors.LoginConfirmWaitError(ticket.id)
|
||||
elif ticket.action == ticket.ACTION_APPROVE:
|
||||
elif ticket.action == ticket.ACTION.APPROVE:
|
||||
self.request.session["auth_confirm"] = "1"
|
||||
return
|
||||
elif ticket.action == ticket.ACTION_REJECT:
|
||||
elif ticket.action == ticket.ACTION.REJECT:
|
||||
raise errors.LoginConfirmOtherError(
|
||||
ticket.id, ticket.get_action_display()
|
||||
)
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import uuid
|
||||
from django.db import models
|
||||
from functools import partial
|
||||
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import ugettext_lazy as _, ugettext as __
|
||||
from rest_framework.authtoken.models import Token
|
||||
from django.conf import settings
|
||||
from django.utils.crypto import get_random_string
|
||||
|
||||
from common.db import models
|
||||
from common.mixins.models import CommonModelMixin
|
||||
from common.utils import get_object_or_none, get_request_ip, get_ip_city
|
||||
|
||||
@@ -50,7 +53,7 @@ class LoginConfirmSetting(CommonModelMixin):
|
||||
|
||||
def create_confirm_ticket(self, request=None):
|
||||
from tickets.models import Ticket
|
||||
title = _('Login confirm') + '{}'.format(self.user)
|
||||
title = _('Login confirm') + ' {}'.format(self.user)
|
||||
if request:
|
||||
remote_addr = get_request_ip(request)
|
||||
city = get_ip_city(remote_addr)
|
||||
@@ -68,7 +71,7 @@ class LoginConfirmSetting(CommonModelMixin):
|
||||
reviewer = self.reviewers.all()
|
||||
ticket = Ticket.objects.create(
|
||||
user=self.user, title=title, body=body,
|
||||
type=Ticket.TYPE_LOGIN_CONFIRM,
|
||||
type=Ticket.TYPE.LOGIN_CONFIRM,
|
||||
)
|
||||
ticket.assignees.set(reviewer)
|
||||
return ticket
|
||||
@@ -76,3 +79,12 @@ class LoginConfirmSetting(CommonModelMixin):
|
||||
def __str__(self):
|
||||
return '{} confirm'.format(self.user.username)
|
||||
|
||||
|
||||
class SSOToken(models.JMSBaseModel):
|
||||
"""
|
||||
类似腾讯企业邮的 [单点登录](https://exmail.qq.com/qy_mng_logic/doc#10036)
|
||||
出于安全考虑,这里的 `token` 使用一次随即过期。但我们保留每一个生成过的 `token`。
|
||||
"""
|
||||
authkey = models.UUIDField(primary_key=True, default=uuid.uuid4, verbose_name=_('Token'))
|
||||
expired = models.BooleanField(default=False, verbose_name=_('Expired'))
|
||||
user = models.ForeignKey('users.User', on_delete=models.PROTECT, verbose_name=_('User'), db_constraint=False)
|
||||
|
||||
@@ -5,12 +5,12 @@ from rest_framework import serializers
|
||||
from common.utils import get_object_or_none
|
||||
from users.models import User
|
||||
from users.serializers import UserProfileSerializer
|
||||
from .models import AccessKey, LoginConfirmSetting
|
||||
from .models import AccessKey, LoginConfirmSetting, SSOToken
|
||||
|
||||
|
||||
__all__ = [
|
||||
'AccessKeySerializer', 'OtpVerifySerializer', 'BearerTokenSerializer',
|
||||
'MFAChallengeSerializer', 'LoginConfirmSettingSerializer',
|
||||
'MFAChallengeSerializer', 'LoginConfirmSettingSerializer', 'SSOTokenSerializer',
|
||||
]
|
||||
|
||||
|
||||
@@ -76,3 +76,9 @@ class LoginConfirmSettingSerializer(serializers.ModelSerializer):
|
||||
model = LoginConfirmSetting
|
||||
fields = ['id', 'user', 'reviewers', 'date_created', 'date_updated']
|
||||
read_only_fields = ['date_created', 'date_updated']
|
||||
|
||||
|
||||
class SSOTokenSerializer(serializers.Serializer):
|
||||
username = serializers.CharField(write_only=True)
|
||||
login_url = serializers.CharField(read_only=True)
|
||||
next = serializers.CharField(write_only=True, allow_blank=True, required=False, allow_null=True)
|
||||
|
||||
@@ -1,10 +1,27 @@
|
||||
from importlib import import_module
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import user_logged_in
|
||||
from django.core.cache import cache
|
||||
from django.dispatch import receiver
|
||||
from django_cas_ng.signals import cas_user_authenticated
|
||||
|
||||
from jms_oidc_rp.signals import openid_user_login_failed, openid_user_login_success
|
||||
|
||||
from .signals import post_auth_success, post_auth_failed
|
||||
|
||||
|
||||
@receiver(user_logged_in)
|
||||
def on_user_auth_login_success(sender, user, request, **kwargs):
|
||||
if settings.USER_LOGIN_SINGLE_MACHINE_ENABLED:
|
||||
user_id = 'single_machine_login_' + str(user.id)
|
||||
session_key = cache.get(user_id)
|
||||
if session_key and session_key != request.session.session_key:
|
||||
session = import_module(settings.SESSION_ENGINE).SessionStore(session_key)
|
||||
session.delete()
|
||||
cache.set(user_id, request.session.session_key, None)
|
||||
|
||||
|
||||
@receiver(openid_user_login_success)
|
||||
def on_oidc_user_login_success(sender, request, user, **kwargs):
|
||||
post_auth_success.send(sender, user=user, request=request)
|
||||
@@ -13,3 +30,8 @@ def on_oidc_user_login_success(sender, request, user, **kwargs):
|
||||
@receiver(openid_user_login_failed)
|
||||
def on_oidc_user_login_failed(sender, username, request, reason, **kwargs):
|
||||
post_auth_failed.send(sender, username=username, request=request, reason=reason)
|
||||
|
||||
|
||||
@receiver(cas_user_authenticated)
|
||||
def on_cas_user_login_success(sender, request, user, **kwargs):
|
||||
post_auth_success.send(sender, user=user, request=request)
|
||||
@@ -26,13 +26,24 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="password" class="form-control" id="password" name="{{ form.password.html_name }}" placeholder="{% trans 'Password' %}" required="">
|
||||
<input type="password" class="form-control" id="password" placeholder="{% trans 'Password' %}" required="">
|
||||
<input id="password-hidden" type="text" style="display:none" name="{{ form.password.html_name }}">
|
||||
{% if form.errors.password %}
|
||||
<div class="help-block field-error">
|
||||
<p class="red-fonts">{{ form.errors.password.as_text }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if form.challenge %}
|
||||
<div class="form-group">
|
||||
<input type="challenge" class="form-control" id="challenge" name="{{ form.challenge.html_name }}" placeholder="{% trans 'MFA code' %}" >
|
||||
{% if form.errors.challenge %}
|
||||
<div class="help-block field-error">
|
||||
<p class="red-fonts">{{ form.errors.challenge.as_text }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div>
|
||||
{{ form.captcha }}
|
||||
</div>
|
||||
@@ -76,7 +87,7 @@
|
||||
var rsaPublicKey = "{{ rsa_public_key }}"
|
||||
var password =$('#password').val(); //明文密码
|
||||
var passwordEncrypted = encryptLoginPassword(password, rsaPublicKey)
|
||||
$('#password').val(passwordEncrypted); //返回给密码输入input
|
||||
$('#password-hidden').val(passwordEncrypted); //返回给密码输入input
|
||||
$('#form').submit();//post提交
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -67,22 +67,30 @@
|
||||
</div>
|
||||
<div class="box-3">
|
||||
<div style="background-color: white">
|
||||
<div style="margin-top: 30px;padding-top: 40px;padding-left: 20px;padding-right: 20px;height: 80px">
|
||||
<span style="font-size: 21px;font-weight:400;color: #151515;letter-spacing: 0;">{{ JMS_TITLE }}</span>
|
||||
</div>
|
||||
{% if form.challenge %}
|
||||
<div style="margin-top: 20px;padding-top: 30px;padding-left: 20px;padding-right: 20px;height: 60px">
|
||||
{% else %}
|
||||
<div style="margin-top: 20px;padding-top: 40px;padding-left: 20px;padding-right: 20px;height: 80px">
|
||||
{% endif %}
|
||||
<span style="font-size: 21px;font-weight:400;color: #151515;letter-spacing: 0;">{{ JMS_TITLE }}</span>
|
||||
</div>
|
||||
<div style="font-size: 12px;color: #999999;letter-spacing: 0;line-height: 18px;margin-top: 18px">
|
||||
{% trans 'Welcome back, please enter username and password to login' %}
|
||||
</div>
|
||||
<div style="margin-bottom: 10px">
|
||||
<div style="margin-bottom: 0px">
|
||||
<div>
|
||||
<div class="col-md-1"></div>
|
||||
<div class="contact-form col-md-10" style="margin-top: 20px;height: 35px">
|
||||
<div class="contact-form col-md-10" style="margin-top: 0px;height: 35px">
|
||||
<form id="contact-form" action="" method="post" role="form" novalidate="novalidate">
|
||||
{% csrf_token %}
|
||||
{% if form.non_field_errors %}
|
||||
<div style="height: 70px;color: red;line-height: 17px;">
|
||||
<p class="red-fonts">{{ form.non_field_errors.as_text }}</p>
|
||||
</div>
|
||||
{% if form.challenge %}
|
||||
<div style="height: 50px;color: red;line-height: 17px;">
|
||||
{% else %}
|
||||
<div style="height: 70px;color: red;line-height: 17px;">
|
||||
{% endif %}
|
||||
<p class="red-fonts">{{ form.non_field_errors.as_text }}</p>
|
||||
</div>
|
||||
{% elif form.errors.captcha %}
|
||||
<p class="red-fonts">{% trans 'Captcha invalid' %}</p>
|
||||
{% else %}
|
||||
@@ -98,13 +106,24 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="password" class="form-control" id="password" name="{{ form.password.html_name }}" placeholder="{% trans 'Password' %}" required="">
|
||||
<input type="password" class="form-control" id="password" placeholder="{% trans 'Password' %}" required="">
|
||||
<input id="password-hidden" type="text" style="display:none" name="{{ form.password.html_name }}">
|
||||
{% if form.errors.password %}
|
||||
<div class="help-block field-error">
|
||||
<p class="red-fonts">{{ form.errors.password.as_text }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if form.challenge %}
|
||||
<div class="form-group">
|
||||
<input type="challenge" class="form-control" id="challenge" name="{{ form.challenge.html_name }}" placeholder="{% trans 'MFA code' %}" >
|
||||
{% if form.errors.challenge %}
|
||||
<div class="help-block field-error">
|
||||
<p class="red-fonts">{{ form.errors.challenge.as_text }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="form-group" style="height: 50px;margin-bottom: 0;font-size: 13px">
|
||||
{{ form.captcha }}
|
||||
</div>
|
||||
@@ -139,7 +158,7 @@
|
||||
var rsaPublicKey = "{{ rsa_public_key }}"
|
||||
var password =$('#password').val(); //明文密码
|
||||
var passwordEncrypted = encryptLoginPassword(password, rsaPublicKey)
|
||||
$('#password').val(passwordEncrypted); //返回给密码输入input
|
||||
$('#password-hidden').val(passwordEncrypted); //返回给密码输入input
|
||||
$('#contact-form').submit();//post提交
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -8,6 +8,7 @@ from .. import api
|
||||
app_name = 'authentication'
|
||||
router = DefaultRouter()
|
||||
router.register('access-keys', api.AccessKeyViewSet, 'access-key')
|
||||
router.register('sso', api.SSOViewSet, 'sso')
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
|
||||
@@ -21,6 +21,7 @@ urlpatterns = [
|
||||
path('password/forgot/sendmail-success/', users_view.UserForgotPasswordSendmailSuccessView.as_view(),
|
||||
name='forgot-password-sendmail-success'),
|
||||
path('password/reset/', users_view.UserResetPasswordView.as_view(), name='reset-password'),
|
||||
path('password/too-simple-flash-msg/', views.FlashPasswdTooSimpleMsgView.as_view(), name='passwd-too-simple-flash-msg'),
|
||||
path('password/reset/success/', users_view.UserResetPasswordSuccessView.as_view(), name='reset-password-success'),
|
||||
path('password/verify/', users_view.UserVerifyPasswordView.as_view(), name='user-verify-password'),
|
||||
|
||||
|
||||
@@ -4,12 +4,9 @@ import base64
|
||||
from Crypto.PublicKey import RSA
|
||||
from Crypto.Cipher import PKCS1_v1_5
|
||||
from Crypto import Random
|
||||
from django.contrib.auth import authenticate
|
||||
|
||||
from common.utils import get_logger
|
||||
|
||||
from . import errors
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
@@ -37,37 +34,13 @@ def rsa_decrypt(cipher_text, rsa_private_key=None):
|
||||
if rsa_private_key is None:
|
||||
# rsa_private_key 为 None,可以能是API请求认证,不需要解密
|
||||
return cipher_text
|
||||
|
||||
key = RSA.importKey(rsa_private_key)
|
||||
cipher = PKCS1_v1_5.new(key)
|
||||
message = cipher.decrypt(base64.b64decode(cipher_text.encode()), 'error').decode()
|
||||
cipher_decoded = base64.b64decode(cipher_text.encode())
|
||||
# Todo: 弄明白为何要以下这么写,https://xbuba.com/questions/57035263
|
||||
if len(cipher_decoded) == 127:
|
||||
hex_fixed = '00' + cipher_decoded.hex()
|
||||
cipher_decoded = base64.b16decode(hex_fixed.upper())
|
||||
message = cipher.decrypt(cipher_decoded, b'error').decode()
|
||||
return message
|
||||
|
||||
|
||||
def check_user_valid(**kwargs):
|
||||
password = kwargs.pop('password', None)
|
||||
public_key = kwargs.pop('public_key', None)
|
||||
username = kwargs.pop('username', None)
|
||||
request = kwargs.get('request')
|
||||
|
||||
# 获取解密密钥,对密码进行解密
|
||||
rsa_private_key = request.session.get('rsa_private_key')
|
||||
if rsa_private_key is not None:
|
||||
try:
|
||||
password = rsa_decrypt(password, rsa_private_key)
|
||||
except Exception as e:
|
||||
logger.error(e, exc_info=True)
|
||||
logger.error('Need decrypt password => {}'.format(password))
|
||||
return None, errors.reason_password_decrypt_failed
|
||||
|
||||
user = authenticate(request, username=username,
|
||||
password=password, public_key=public_key)
|
||||
if not user:
|
||||
return None, errors.reason_password_failed
|
||||
elif user.is_expired:
|
||||
return None, errors.reason_user_inactive
|
||||
elif not user.is_active:
|
||||
return None, errors.reason_user_inactive
|
||||
elif user.password_has_expired:
|
||||
return None, errors.reason_password_expired
|
||||
|
||||
return user, ''
|
||||
|
||||
@@ -17,17 +17,22 @@ from django.views.generic.base import TemplateView, RedirectView
|
||||
from django.views.generic.edit import FormView
|
||||
from django.conf import settings
|
||||
from django.urls import reverse_lazy
|
||||
from django.contrib.auth import BACKEND_SESSION_KEY
|
||||
|
||||
from common.const.front_urls import TICKET_DETAIL
|
||||
from common.utils import get_request_ip, get_object_or_none
|
||||
from users.utils import (
|
||||
redirect_user_first_login_or_index
|
||||
)
|
||||
from .. import forms, mixins, errors, utils
|
||||
from ..const import RSA_PRIVATE_KEY, RSA_PUBLIC_KEY
|
||||
from .. import mixins, errors, utils
|
||||
from ..forms import get_user_login_form_cls
|
||||
|
||||
|
||||
__all__ = [
|
||||
'UserLoginView', 'UserLogoutView',
|
||||
'UserLoginGuardView', 'UserLoginWaitConfirmView',
|
||||
'FlashPasswdTooSimpleMsgView',
|
||||
]
|
||||
|
||||
|
||||
@@ -35,8 +40,6 @@ __all__ = [
|
||||
@method_decorator(csrf_protect, name='dispatch')
|
||||
@method_decorator(never_cache, name='dispatch')
|
||||
class UserLoginView(mixins.AuthMixin, FormView):
|
||||
form_class = forms.UserLoginForm
|
||||
form_class_captcha = forms.UserLoginCaptchaForm
|
||||
key_prefix_captcha = "_LOGIN_INVALID_{}"
|
||||
redirect_field_name = 'next'
|
||||
|
||||
@@ -82,15 +85,19 @@ class UserLoginView(mixins.AuthMixin, FormView):
|
||||
if not self.request.session.test_cookie_worked():
|
||||
return HttpResponse(_("Please enable cookies and try again."))
|
||||
try:
|
||||
self.check_user_auth()
|
||||
self.check_user_auth(decrypt_passwd=True)
|
||||
except errors.AuthFailedError as e:
|
||||
form.add_error(None, e.msg)
|
||||
ip = self.get_request_ip()
|
||||
cache.set(self.key_prefix_captcha.format(ip), 1, 3600)
|
||||
new_form = self.form_class_captcha(data=form.data)
|
||||
form_cls = get_user_login_form_cls(captcha=True)
|
||||
new_form = form_cls(data=form.data)
|
||||
new_form._errors = form.errors
|
||||
context = self.get_context_data(form=new_form)
|
||||
return self.render_to_response(context)
|
||||
except errors.PasswdTooSimple as e:
|
||||
return redirect(e.url)
|
||||
self.clear_rsa_key()
|
||||
return self.redirect_to_guard_view()
|
||||
|
||||
def redirect_to_guard_view(self):
|
||||
@@ -103,18 +110,28 @@ class UserLoginView(mixins.AuthMixin, FormView):
|
||||
def get_form_class(self):
|
||||
ip = get_request_ip(self.request)
|
||||
if cache.get(self.key_prefix_captcha.format(ip)):
|
||||
return self.form_class_captcha
|
||||
return get_user_login_form_cls(captcha=True)
|
||||
else:
|
||||
return self.form_class
|
||||
return get_user_login_form_cls()
|
||||
|
||||
def clear_rsa_key(self):
|
||||
self.request.session[RSA_PRIVATE_KEY] = None
|
||||
self.request.session[RSA_PUBLIC_KEY] = None
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
# 生成加解密密钥对,public_key传递给前端,private_key存入session中供解密使用
|
||||
rsa_private_key, rsa_public_key = utils.gen_key_pair()
|
||||
self.request.session['rsa_private_key'] = rsa_private_key
|
||||
rsa_private_key = self.request.session.get(RSA_PRIVATE_KEY)
|
||||
rsa_public_key = self.request.session.get(RSA_PUBLIC_KEY)
|
||||
if not all((rsa_private_key, rsa_public_key)):
|
||||
rsa_private_key, rsa_public_key = utils.gen_key_pair()
|
||||
rsa_public_key = rsa_public_key.replace('\n', '\\n')
|
||||
self.request.session[RSA_PRIVATE_KEY] = rsa_private_key
|
||||
self.request.session[RSA_PUBLIC_KEY] = rsa_public_key
|
||||
|
||||
context = {
|
||||
'demo_mode': os.environ.get("DEMO_MODE"),
|
||||
'AUTH_OPENID': settings.AUTH_OPENID,
|
||||
'rsa_public_key': rsa_public_key.replace('\n', '\\n')
|
||||
'rsa_public_key': rsa_public_key
|
||||
}
|
||||
kwargs.update(context)
|
||||
return super().get_context_data(**kwargs)
|
||||
@@ -145,6 +162,8 @@ class UserLoginGuardView(mixins.AuthMixin, RedirectView):
|
||||
return self.format_redirect_url(self.login_confirm_url)
|
||||
except errors.MFAUnsetError as e:
|
||||
return e.url
|
||||
except errors.PasswdTooSimple as e:
|
||||
return e.url
|
||||
else:
|
||||
auth_login(self.request, user)
|
||||
self.send_auth_signal(success=True, user=user)
|
||||
@@ -168,7 +187,7 @@ class UserLoginWaitConfirmView(TemplateView):
|
||||
context = super().get_context_data(**kwargs)
|
||||
if ticket:
|
||||
timestamp_created = datetime.datetime.timestamp(ticket.date_created)
|
||||
ticket_detail_url = reverse('tickets:ticket-detail', kwargs={'pk': ticket_id})
|
||||
ticket_detail_url = TICKET_DETAIL.format(id=ticket_id)
|
||||
msg = _("""Wait for <b>{}</b> confirm, You also can copy link to her/him <br/>
|
||||
Don't close this page""").format(ticket.assignees_display)
|
||||
else:
|
||||
@@ -187,12 +206,12 @@ class UserLoginWaitConfirmView(TemplateView):
|
||||
class UserLogoutView(TemplateView):
|
||||
template_name = 'flash_message_standalone.html'
|
||||
|
||||
@staticmethod
|
||||
def get_backend_logout_url():
|
||||
if settings.AUTH_OPENID:
|
||||
def get_backend_logout_url(self):
|
||||
backend = self.request.session.get(BACKEND_SESSION_KEY, '')
|
||||
if 'OIDC' in backend:
|
||||
return settings.AUTH_OPENID_AUTH_LOGOUT_URL_NAME
|
||||
# if settings.AUTH_CAS:
|
||||
# return settings.CAS_LOGOUT_URL_NAME
|
||||
elif 'CAS' in backend:
|
||||
return settings.CAS_LOGOUT_URL_NAME
|
||||
return None
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
@@ -216,4 +235,16 @@ class UserLogoutView(TemplateView):
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
@method_decorator(never_cache, name='dispatch')
|
||||
class FlashPasswdTooSimpleMsgView(TemplateView):
|
||||
template_name = 'flash_message_standalone.html'
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
context = {
|
||||
'title': _('Please change your password'),
|
||||
'messages': _('Your password is too simple, please change it for security'),
|
||||
'interval': 5,
|
||||
'redirect_url': request.GET.get('redirect_url'),
|
||||
'auto_redirect': True,
|
||||
}
|
||||
return self.render_to_response(context)
|
||||
|
||||
8
apps/common/const/choices.py
Normal file
8
apps/common/const/choices.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from common.db.models import ChoiceSet
|
||||
|
||||
|
||||
ADMIN = 'Admin'
|
||||
USER = 'User'
|
||||
AUDITOR = 'Auditor'
|
||||
2
apps/common/const/front_urls.py
Normal file
2
apps/common/const/front_urls.py
Normal file
@@ -0,0 +1,2 @@
|
||||
|
||||
TICKET_DETAIL = '/ui/#/tickets/tickets/{id}'
|
||||
@@ -3,10 +3,10 @@ from django.db.models import Aggregate
|
||||
|
||||
class GroupConcat(Aggregate):
|
||||
function = 'GROUP_CONCAT'
|
||||
template = '%(function)s(%(distinct)s %(expressions)s %(order_by)s %(separator))'
|
||||
template = '%(function)s(%(expressions)s %(order_by)s %(separator)s)'
|
||||
allow_distinct = False
|
||||
|
||||
def __init__(self, expression, distinct=False, order_by=None, separator=',', **extra):
|
||||
def __init__(self, expression, order_by=None, separator=',', **extra):
|
||||
order_by_clause = ''
|
||||
if order_by is not None:
|
||||
order = 'ASC'
|
||||
@@ -21,8 +21,7 @@ class GroupConcat(Aggregate):
|
||||
|
||||
super().__init__(
|
||||
expression,
|
||||
distinct='DISTINCT' if distinct else '',
|
||||
order_by=order_by_clause,
|
||||
separator=f'SEPARATOR {separator}',
|
||||
separator=f"SEPARATOR '{separator}'",
|
||||
**extra
|
||||
)
|
||||
|
||||
84
apps/common/db/models.py
Normal file
84
apps/common/db/models.py
Normal file
@@ -0,0 +1,84 @@
|
||||
"""
|
||||
此文件作为 `django.db.models` 的 shortcut
|
||||
|
||||
这样做的优点与缺点为:
|
||||
优点:
|
||||
- 包命名都统一为 `models`
|
||||
- 用户在使用的时候只导入本文件即可
|
||||
缺点:
|
||||
- 此文件中添加代码的时候,注意不要跟 `django.db.models` 中的命名冲突
|
||||
"""
|
||||
|
||||
import uuid
|
||||
|
||||
from django.db.models import *
|
||||
from django.db.models.functions import Concat
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
|
||||
class Choice(str):
|
||||
def __new__(cls, value, label=''): # `deepcopy` 的时候不会传 `label`
|
||||
self = super().__new__(cls, value)
|
||||
self.label = label
|
||||
return self
|
||||
|
||||
|
||||
class ChoiceSetType(type):
|
||||
def __new__(cls, name, bases, attrs):
|
||||
_choices = []
|
||||
collected = set()
|
||||
new_attrs = {}
|
||||
for k, v in attrs.items():
|
||||
if isinstance(v, tuple):
|
||||
v = Choice(*v)
|
||||
assert v not in collected, 'Cannot be defined repeatedly'
|
||||
_choices.append(v)
|
||||
collected.add(v)
|
||||
new_attrs[k] = v
|
||||
for base in bases:
|
||||
if hasattr(base, '_choices'):
|
||||
for c in base._choices:
|
||||
if c not in collected:
|
||||
_choices.append(c)
|
||||
collected.add(c)
|
||||
new_attrs['_choices'] = _choices
|
||||
new_attrs['_choices_dict'] = {c: c.label for c in _choices}
|
||||
return type.__new__(cls, name, bases, new_attrs)
|
||||
|
||||
def __contains__(self, item):
|
||||
return self._choices_dict.__contains__(item)
|
||||
|
||||
def __getitem__(self, item):
|
||||
return self._choices_dict.__getitem__(item)
|
||||
|
||||
def get(self, item, default=None):
|
||||
return self._choices_dict.get(item, default)
|
||||
|
||||
@property
|
||||
def choices(self):
|
||||
return [(c, c.label) for c in self._choices]
|
||||
|
||||
|
||||
class ChoiceSet(metaclass=ChoiceSetType):
|
||||
choices = None # 用于 Django Model 中的 choices 配置, 为了代码提示在此声明
|
||||
|
||||
|
||||
class JMSBaseModel(Model):
|
||||
created_by = CharField(max_length=32, null=True, blank=True, verbose_name=_('Created by'))
|
||||
updated_by = CharField(max_length=32, null=True, blank=True, verbose_name=_('Updated by'))
|
||||
date_created = DateTimeField(auto_now_add=True, null=True, blank=True, verbose_name=_('Date created'))
|
||||
date_updated = DateTimeField(auto_now=True, verbose_name=_('Date updated'))
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
class JMSModel(JMSBaseModel):
|
||||
id = UUIDField(default=uuid.uuid4, primary_key=True)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
def concated_display(name1, name2):
|
||||
return Concat(F(name1), Value('('), F(name2), Value(')'))
|
||||
@@ -1,11 +1,42 @@
|
||||
from rest_framework.viewsets import GenericViewSet, ModelViewSet
|
||||
from rest_framework_bulk import BulkModelViewSet
|
||||
|
||||
from ..mixins.api import SerializerMixin2, QuerySetMixin, ExtraFilterFieldsMixin
|
||||
from ..mixins.api import (
|
||||
SerializerMixin2, QuerySetMixin, ExtraFilterFieldsMixin, PaginatedResponseMixin,
|
||||
RelationMixin, AllowBulkDestoryMixin
|
||||
)
|
||||
|
||||
|
||||
class JmsGenericViewSet(SerializerMixin2, QuerySetMixin, ExtraFilterFieldsMixin, GenericViewSet):
|
||||
class JmsGenericViewSet(SerializerMixin2,
|
||||
QuerySetMixin,
|
||||
ExtraFilterFieldsMixin,
|
||||
PaginatedResponseMixin,
|
||||
GenericViewSet):
|
||||
pass
|
||||
|
||||
|
||||
class JMSModelViewSet(SerializerMixin2, QuerySetMixin, ExtraFilterFieldsMixin, ModelViewSet):
|
||||
class JMSModelViewSet(SerializerMixin2,
|
||||
QuerySetMixin,
|
||||
ExtraFilterFieldsMixin,
|
||||
PaginatedResponseMixin,
|
||||
ModelViewSet):
|
||||
pass
|
||||
|
||||
|
||||
class JMSBulkModelViewSet(SerializerMixin2,
|
||||
QuerySetMixin,
|
||||
ExtraFilterFieldsMixin,
|
||||
PaginatedResponseMixin,
|
||||
AllowBulkDestoryMixin,
|
||||
BulkModelViewSet):
|
||||
pass
|
||||
|
||||
|
||||
class JMSBulkRelationModelViewSet(SerializerMixin2,
|
||||
QuerySetMixin,
|
||||
ExtraFilterFieldsMixin,
|
||||
PaginatedResponseMixin,
|
||||
RelationMixin,
|
||||
AllowBulkDestoryMixin,
|
||||
BulkModelViewSet):
|
||||
pass
|
||||
|
||||
45
apps/common/drf/exc_handlers.py
Normal file
45
apps/common/drf/exc_handlers.py
Normal file
@@ -0,0 +1,45 @@
|
||||
from django.core.exceptions import PermissionDenied, ObjectDoesNotExist as DJObjectDoesNotExist
|
||||
from django.http import Http404
|
||||
from django.utils.translation import gettext
|
||||
|
||||
from rest_framework import exceptions
|
||||
from rest_framework.views import set_rollback
|
||||
from rest_framework.response import Response
|
||||
|
||||
from common.exceptions import JMSObjectDoesNotExist
|
||||
|
||||
|
||||
def extract_object_name(exc, index=0):
|
||||
"""
|
||||
`index` 是从 0 开始数的, 比如:
|
||||
`No User matches the given query.`
|
||||
提取 `User`,`index=1`
|
||||
"""
|
||||
(msg, *_) = exc.args
|
||||
return gettext(msg.split(sep=' ', maxsplit=index + 1)[index])
|
||||
|
||||
|
||||
def common_exception_handler(exc, context):
|
||||
if isinstance(exc, Http404):
|
||||
exc = JMSObjectDoesNotExist(object_name=extract_object_name(exc, 1))
|
||||
elif isinstance(exc, PermissionDenied):
|
||||
exc = exceptions.PermissionDenied()
|
||||
elif isinstance(exc, DJObjectDoesNotExist):
|
||||
exc = JMSObjectDoesNotExist(object_name=extract_object_name(exc, 0))
|
||||
|
||||
if isinstance(exc, exceptions.APIException):
|
||||
headers = {}
|
||||
if getattr(exc, 'auth_header', None):
|
||||
headers['WWW-Authenticate'] = exc.auth_header
|
||||
if getattr(exc, 'wait', None):
|
||||
headers['Retry-After'] = '%d' % exc.wait
|
||||
|
||||
if isinstance(exc.detail, (list, dict)):
|
||||
data = exc.detail
|
||||
else:
|
||||
data = {'detail': exc.detail}
|
||||
|
||||
set_rollback()
|
||||
return Response(data, status=exc.status_code, headers=headers)
|
||||
|
||||
return None
|
||||
43
apps/common/drf/fields.py
Normal file
43
apps/common/drf/fields.py
Normal file
@@ -0,0 +1,43 @@
|
||||
from uuid import UUID
|
||||
|
||||
from rest_framework.fields import get_attribute
|
||||
from rest_framework.relations import ManyRelatedField, PrimaryKeyRelatedField, MANY_RELATION_KWARGS
|
||||
|
||||
|
||||
class GroupConcatedManyRelatedField(ManyRelatedField):
|
||||
def get_attribute(self, instance):
|
||||
if hasattr(instance, 'pk') and instance.pk is None:
|
||||
return []
|
||||
|
||||
attr = self.source_attrs[-1]
|
||||
|
||||
# `gc` 是 `GroupConcat` 的缩写
|
||||
gc_attr = f'gc_{attr}'
|
||||
if hasattr(instance, gc_attr):
|
||||
gc_value = getattr(instance, gc_attr)
|
||||
if isinstance(gc_value, str):
|
||||
return [UUID(pk) for pk in set(gc_value.split(','))]
|
||||
else:
|
||||
return ''
|
||||
|
||||
relationship = get_attribute(instance, self.source_attrs)
|
||||
return relationship.all() if hasattr(relationship, 'all') else relationship
|
||||
|
||||
|
||||
class GroupConcatedPrimaryKeyRelatedField(PrimaryKeyRelatedField):
|
||||
@classmethod
|
||||
def many_init(cls, *args, **kwargs):
|
||||
list_kwargs = {'child_relation': cls(*args, **kwargs)}
|
||||
for key in kwargs:
|
||||
if key in MANY_RELATION_KWARGS:
|
||||
list_kwargs[key] = kwargs[key]
|
||||
return GroupConcatedManyRelatedField(**list_kwargs)
|
||||
|
||||
def to_representation(self, value):
|
||||
if self.pk_field is not None:
|
||||
return self.pk_field.to_representation(value.pk)
|
||||
|
||||
if hasattr(value, 'pk'):
|
||||
return value.pk
|
||||
else:
|
||||
return value
|
||||
@@ -47,9 +47,9 @@ class JMSCSVParser(BaseParser):
|
||||
yield row
|
||||
|
||||
@staticmethod
|
||||
def _get_fields_map(serializer):
|
||||
def _get_fields_map(serializer_cls):
|
||||
fields_map = {}
|
||||
fields = serializer.fields
|
||||
fields = serializer_cls().fields
|
||||
fields_map.update({v.label: k for k, v in fields.items()})
|
||||
fields_map.update({k: k for k, _ in fields.items()})
|
||||
return fields_map
|
||||
@@ -101,7 +101,7 @@ class JMSCSVParser(BaseParser):
|
||||
try:
|
||||
view = parser_context['view']
|
||||
meta = view.request.META
|
||||
serializer = view.get_serializer()
|
||||
serializer_cls = view.get_serializer_class()
|
||||
except Exception as e:
|
||||
logger.debug(e, exc_info=True)
|
||||
raise ParseError('The resource does not support imports!')
|
||||
@@ -121,7 +121,7 @@ class JMSCSVParser(BaseParser):
|
||||
rows = self._gen_rows(binary, charset=encoding)
|
||||
|
||||
header = next(rows)
|
||||
fields_map = self._get_fields_map(serializer)
|
||||
fields_map = self._get_fields_map(serializer_cls)
|
||||
header = [fields_map.get(name.strip('*'), '') for name in header]
|
||||
|
||||
data = []
|
||||
|
||||
@@ -1,5 +1,25 @@
|
||||
from rest_framework.serializers import Serializer
|
||||
from rest_framework.serializers import ModelSerializer
|
||||
from rest_framework import serializers
|
||||
from rest_framework_bulk.serializers import BulkListSerializer
|
||||
|
||||
from common.mixins.serializers import BulkSerializerMixin
|
||||
from common.mixins import BulkListSerializerMixin
|
||||
|
||||
__all__ = ['EmptySerializer', 'BulkModelSerializer']
|
||||
|
||||
|
||||
class EmptySerializer(Serializer):
|
||||
pass
|
||||
|
||||
|
||||
class BulkModelSerializer(BulkSerializerMixin, ModelSerializer):
|
||||
pass
|
||||
|
||||
|
||||
class AdaptedBulkListSerializer(BulkListSerializerMixin, BulkListSerializer):
|
||||
pass
|
||||
|
||||
|
||||
class CeleryTaskSerializer(serializers.Serializer):
|
||||
task = serializers.CharField(read_only=True)
|
||||
|
||||
@@ -1,7 +1,20 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework.exceptions import APIException
|
||||
from rest_framework import status
|
||||
|
||||
|
||||
class JMSException(APIException):
|
||||
pass
|
||||
status_code = status.HTTP_400_BAD_REQUEST
|
||||
|
||||
|
||||
class JMSObjectDoesNotExist(APIException):
|
||||
status_code = status.HTTP_404_NOT_FOUND
|
||||
default_code = 'object_does_not_exist'
|
||||
default_detail = _('%s object does not exist.')
|
||||
|
||||
def __init__(self, detail=None, code=None, object_name=None):
|
||||
if detail is None and object_name:
|
||||
detail = self.default_detail % object_name
|
||||
super(JMSObjectDoesNotExist, self).__init__(detail=detail, code=code)
|
||||
|
||||
@@ -5,7 +5,7 @@ from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.encoding import force_text
|
||||
|
||||
from ..utils import signer, aes_crypto
|
||||
from ..utils import signer, aes_crypto, aes_ecb_crypto
|
||||
|
||||
|
||||
__all__ = [
|
||||
@@ -117,9 +117,17 @@ class EncryptMixin:
|
||||
return signer.unsign(value) or ''
|
||||
|
||||
def decrypt_from_aes(self, value):
|
||||
"""
|
||||
先尝试使用GCM模式解密,如果解不开,再尝试使用原来的ECB模式解密
|
||||
"""
|
||||
try:
|
||||
return aes_crypto.decrypt(value)
|
||||
except (TypeError, ValueError):
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
try:
|
||||
return aes_ecb_crypto.decrypt(value)
|
||||
except (TypeError, ValueError, UnicodeDecodeError):
|
||||
pass
|
||||
|
||||
def from_db_value(self, value, expression, connection, context):
|
||||
|
||||
@@ -11,6 +11,8 @@ from django.core.cache import cache
|
||||
from django.http import JsonResponse
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.settings import api_settings
|
||||
from rest_framework import status
|
||||
from rest_framework_bulk.drf3.mixins import BulkDestroyModelMixin
|
||||
|
||||
from common.drf.filters import IDSpmFilter, CustomFilter, IDInFilter
|
||||
from ..utils import lazyproperty
|
||||
@@ -67,6 +69,17 @@ class ExtraFilterFieldsMixin:
|
||||
return queryset
|
||||
|
||||
|
||||
class PaginatedResponseMixin:
|
||||
def get_paginated_response_with_query_set(self, queryset):
|
||||
page = self.paginate_queryset(queryset)
|
||||
if page is not None:
|
||||
serializer = self.get_serializer(page, many=True)
|
||||
return self.get_paginated_response(serializer.data)
|
||||
|
||||
serializer = self.get_serializer(queryset, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
class CommonApiMixin(SerializerMixin, ExtraFilterFieldsMixin):
|
||||
pass
|
||||
|
||||
@@ -212,10 +225,11 @@ class RelationMixin:
|
||||
self.through = getattr(self.m2m_field.model, self.m2m_field.attname).through
|
||||
|
||||
def get_queryset(self):
|
||||
# 注意,此处拦截了 `get_queryset` 没有 `super`
|
||||
queryset = self.through.objects.all()
|
||||
return queryset
|
||||
|
||||
def send_post_add_signal(self, instances):
|
||||
def send_m2m_changed_signal(self, instances, action):
|
||||
if not isinstance(instances, list):
|
||||
instances = [instances]
|
||||
|
||||
@@ -228,13 +242,17 @@ class RelationMixin:
|
||||
|
||||
for from_obj, to_ids in from_to_mapper.items():
|
||||
m2m_changed.send(
|
||||
sender=self.through, instance=from_obj, action='post_add',
|
||||
sender=self.through, instance=from_obj, action=action,
|
||||
reverse=False, model=self.to_model, pk_set=to_ids
|
||||
)
|
||||
|
||||
def perform_create(self, serializer):
|
||||
instance = serializer.save()
|
||||
self.send_post_add_signal(instance)
|
||||
self.send_m2m_changed_signal(instance, 'post_add')
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
instance.delete()
|
||||
self.send_m2m_changed_signal(instance, 'post_remove')
|
||||
|
||||
|
||||
class SerializerMixin2:
|
||||
@@ -264,3 +282,12 @@ class QuerySetMixin:
|
||||
queryset = serializer_class.setup_eager_loading(queryset)
|
||||
|
||||
return queryset
|
||||
|
||||
|
||||
class AllowBulkDestoryMixin:
|
||||
def allow_bulk_destroy(self, qs, filtered):
|
||||
"""
|
||||
我们规定,批量删除的情况必须用 `id` 指定要删除的数据。
|
||||
"""
|
||||
query = str(filtered.query)
|
||||
return '`id` IN (' in query or '`id` =' in query
|
||||
|
||||
@@ -8,7 +8,6 @@ from rest_framework.utils import html
|
||||
from rest_framework.settings import api_settings
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.fields import SkipField, empty
|
||||
|
||||
__all__ = ['BulkSerializerMixin', 'BulkListSerializerMixin', 'CommonSerializerMixin', 'CommonBulkSerializerMixin']
|
||||
|
||||
|
||||
@@ -50,6 +49,15 @@ class BulkSerializerMixin(object):
|
||||
self.initial_data = data
|
||||
return super().run_validation(data)
|
||||
|
||||
@classmethod
|
||||
def many_init(cls, *args, **kwargs):
|
||||
meta = getattr(cls, 'Meta', None)
|
||||
assert meta is not None, 'Must have `Meta`'
|
||||
if not hasattr(meta, 'list_serializer_class'):
|
||||
from common.drf.serializers import AdaptedBulkListSerializer
|
||||
meta.list_serializer_class = AdaptedBulkListSerializer
|
||||
return super(BulkSerializerMixin, cls).many_init(*args, **kwargs)
|
||||
|
||||
|
||||
class BulkListSerializerMixin(object):
|
||||
"""
|
||||
|
||||
@@ -182,3 +182,9 @@ class CanUpdateDeleteUser(permissions.BasePermission):
|
||||
if request.method in ['PUT', 'PATCH']:
|
||||
return self.has_update_object_permission(request, view, obj)
|
||||
return True
|
||||
|
||||
|
||||
class IsObjectOwner(IsValidUser):
|
||||
def has_object_permission(self, request, view, obj):
|
||||
return (super().has_object_permission(request, view, obj) and
|
||||
request.user == getattr(obj, 'user', None))
|
||||
|
||||
@@ -1,14 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
"""
|
||||
老的代码统一到 `apps/common/drf/serializers.py` 中,
|
||||
之后此文件废弃
|
||||
"""
|
||||
|
||||
from rest_framework_bulk.serializers import BulkListSerializer
|
||||
from rest_framework import serializers
|
||||
from .mixins import BulkListSerializerMixin
|
||||
|
||||
|
||||
class AdaptedBulkListSerializer(BulkListSerializerMixin, BulkListSerializer):
|
||||
pass
|
||||
|
||||
|
||||
class CeleryTaskSerializer(serializers.Serializer):
|
||||
task = serializers.CharField(read_only=True)
|
||||
from common.drf.serializers import AdaptedBulkListSerializer, CeleryTaskSerializer
|
||||
|
||||
@@ -24,7 +24,7 @@ def send_mail_async(*args, **kwargs):
|
||||
"""
|
||||
if len(args) == 3:
|
||||
args = list(args)
|
||||
args[0] = settings.EMAIL_SUBJECT_PREFIX + args[0]
|
||||
args[0] = (settings.EMAIL_SUBJECT_PREFIX or '') + args[0]
|
||||
email_from = settings.EMAIL_FROM or settings.EMAIL_HOST_USER
|
||||
args.insert(2, email_from)
|
||||
args = tuple(args)
|
||||
|
||||
@@ -11,6 +11,8 @@ import time
|
||||
import ipaddress
|
||||
import psutil
|
||||
|
||||
from .timezone import dt_formater
|
||||
|
||||
|
||||
UUID_PATTERN = re.compile(r'\w{8}(-\w{4}){3}-\w{12}')
|
||||
ipip_db = None
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import base64
|
||||
from Crypto.Cipher import AES
|
||||
from Crypto.Util.Padding import pad
|
||||
from Crypto.Random import get_random_bytes
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
@@ -44,11 +46,69 @@ class AESCrypto:
|
||||
return str(aes.decrypt(base64.decodebytes(bytes(text, encoding='utf8'))).rstrip(b'\0').decode("utf8")) # 解密
|
||||
|
||||
|
||||
def get_aes_crypto(key=None):
|
||||
class AESCryptoGCM:
|
||||
"""
|
||||
使用AES GCM模式
|
||||
"""
|
||||
|
||||
def __init__(self, key):
|
||||
self.key = self.process_key(key)
|
||||
|
||||
@staticmethod
|
||||
def process_key(key):
|
||||
"""
|
||||
返回32 bytes 的key
|
||||
"""
|
||||
if not isinstance(key, bytes):
|
||||
key = bytes(key, encoding='utf-8')
|
||||
|
||||
if len(key) >= 32:
|
||||
return key[:32]
|
||||
|
||||
return pad(key, 32)
|
||||
|
||||
def encrypt(self, text):
|
||||
"""
|
||||
加密text,并将 header, nonce, tag (3*16 bytes, base64后变为 3*24 bytes)
|
||||
附在密文前。解密时要用到。
|
||||
"""
|
||||
header = get_random_bytes(16)
|
||||
cipher = AES.new(self.key, AES.MODE_GCM)
|
||||
cipher.update(header)
|
||||
ciphertext, tag = cipher.encrypt_and_digest(bytes(text, encoding='utf-8'))
|
||||
|
||||
result = []
|
||||
for byte_data in (header, cipher.nonce, tag, ciphertext):
|
||||
result.append(base64.b64encode(byte_data).decode('utf-8'))
|
||||
|
||||
return ''.join(result)
|
||||
|
||||
def decrypt(self, text):
|
||||
"""
|
||||
提取header, nonce, tag并解密text。
|
||||
"""
|
||||
metadata = text[:72]
|
||||
header = base64.b64decode(metadata[:24])
|
||||
nonce = base64.b64decode(metadata[24:48])
|
||||
tag = base64.b64decode(metadata[48:])
|
||||
ciphertext = base64.b64decode(text[72:])
|
||||
|
||||
cipher = AES.new(self.key, AES.MODE_GCM, nonce=nonce)
|
||||
|
||||
cipher.update(header)
|
||||
plain_text_bytes = cipher.decrypt_and_verify(ciphertext, tag)
|
||||
return plain_text_bytes.decode('utf-8')
|
||||
|
||||
|
||||
def get_aes_crypto(key=None, mode='GCM'):
|
||||
if key is None:
|
||||
key = settings.SECRET_KEY
|
||||
a = AESCrypto(key)
|
||||
if mode == 'ECB':
|
||||
a = AESCrypto(key)
|
||||
elif mode == 'GCM':
|
||||
a = AESCryptoGCM(key)
|
||||
return a
|
||||
|
||||
|
||||
aes_crypto = get_aes_crypto()
|
||||
aes_ecb_crypto = get_aes_crypto(mode='ECB')
|
||||
aes_crypto = get_aes_crypto(mode='GCM')
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
#
|
||||
import re
|
||||
from django.shortcuts import reverse as dj_reverse
|
||||
from django.db.models import Subquery, QuerySet
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
|
||||
|
||||
33
apps/common/utils/timezone.py
Normal file
33
apps/common/utils/timezone.py
Normal file
@@ -0,0 +1,33 @@
|
||||
import datetime
|
||||
|
||||
import pytz
|
||||
from django.utils import timezone as dj_timezone
|
||||
from rest_framework.fields import DateTimeField
|
||||
|
||||
max = datetime.datetime.max.replace(tzinfo=datetime.timezone.utc)
|
||||
|
||||
|
||||
def astimezone(dt: datetime.datetime, tzinfo: pytz.tzinfo.DstTzInfo):
|
||||
assert dj_timezone.is_aware(dt)
|
||||
return tzinfo.normalize(dt.astimezone(tzinfo))
|
||||
|
||||
|
||||
def as_china_cst(dt: datetime.datetime):
|
||||
return astimezone(dt, pytz.timezone('Asia/Shanghai'))
|
||||
|
||||
|
||||
def as_current_tz(dt: datetime.datetime):
|
||||
return astimezone(dt, dj_timezone.get_current_timezone())
|
||||
|
||||
|
||||
def utcnow():
|
||||
return dj_timezone.now()
|
||||
|
||||
|
||||
def now():
|
||||
return as_current_tz(utcnow())
|
||||
|
||||
|
||||
_rest_dt_field = DateTimeField()
|
||||
dt_parser = _rest_dt_field.to_internal_value
|
||||
dt_formater = _rest_dt_field.to_representation
|
||||
@@ -128,7 +128,7 @@ class DatesLoginMetricMixin:
|
||||
|
||||
@lazyproperty
|
||||
def dates_total_count_inactive_users(self):
|
||||
total = current_org.get_org_members().count()
|
||||
total = current_org.get_members().count()
|
||||
active = self.dates_total_count_active_users
|
||||
count = total - active
|
||||
if count < 0:
|
||||
@@ -137,7 +137,7 @@ class DatesLoginMetricMixin:
|
||||
|
||||
@lazyproperty
|
||||
def dates_total_count_disabled_users(self):
|
||||
return current_org.get_org_members().filter(is_active=False).count()
|
||||
return current_org.get_members().filter(is_active=False).count()
|
||||
|
||||
@lazyproperty
|
||||
def dates_total_count_active_assets(self):
|
||||
@@ -207,7 +207,7 @@ class DatesLoginMetricMixin:
|
||||
class TotalCountMixin:
|
||||
@staticmethod
|
||||
def get_total_count_users():
|
||||
return current_org.get_org_members().count()
|
||||
return current_org.get_members().count()
|
||||
|
||||
@staticmethod
|
||||
def get_total_count_assets():
|
||||
|
||||
@@ -163,7 +163,7 @@ class Config(dict):
|
||||
'AUTH_LDAP_SEARCH_FILTER': '(cn=%(user)s)',
|
||||
'AUTH_LDAP_START_TLS': False,
|
||||
'AUTH_LDAP_USER_ATTR_MAP': {"username": "cn", "name": "sn", "email": "mail"},
|
||||
'AUTH_LDAP_CONNECT_TIMEOUT': 30,
|
||||
'AUTH_LDAP_CONNECT_TIMEOUT': 10,
|
||||
'AUTH_LDAP_SEARCH_PAGED_SIZE': 1000,
|
||||
'AUTH_LDAP_SYNC_IS_PERIODIC': False,
|
||||
'AUTH_LDAP_SYNC_INTERVAL': None,
|
||||
@@ -211,6 +211,9 @@ class Config(dict):
|
||||
'CAS_LOGOUT_COMPLETELY': True,
|
||||
'CAS_VERSION': 3,
|
||||
|
||||
'AUTH_SSO': False,
|
||||
'AUTH_SSO_AUTHKEY_TTL': 60 * 15,
|
||||
|
||||
'OTP_VALID_WINDOW': 2,
|
||||
'OTP_ISSUER_NAME': 'JumpServer',
|
||||
'EMAIL_SUFFIX': 'jumpserver.org',
|
||||
@@ -238,11 +241,13 @@ class Config(dict):
|
||||
'SECURITY_PASSWORD_LOWER_CASE': False,
|
||||
'SECURITY_PASSWORD_NUMBER': False,
|
||||
'SECURITY_PASSWORD_SPECIAL_CHAR': False,
|
||||
'SECURITY_LOGIN_CHALLENGE_ENABLED': False,
|
||||
'SECURITY_LOGIN_CAPTCHA_ENABLED': True,
|
||||
|
||||
'HTTP_BIND_HOST': '0.0.0.0',
|
||||
'HTTP_LISTEN_PORT': 8080,
|
||||
'WS_LISTEN_PORT': 8070,
|
||||
'LOGIN_LOG_KEEP_DAYS': 90,
|
||||
'LOGIN_LOG_KEEP_DAYS': 9999,
|
||||
'TASK_LOG_KEEP_DAYS': 10,
|
||||
'ASSETS_PERM_CACHE_TIME': 3600 * 24,
|
||||
'SECURITY_MFA_VERIFY_TTL': 3600,
|
||||
@@ -261,7 +266,9 @@ class Config(dict):
|
||||
'ORG_CHANGE_TO_URL': '',
|
||||
'LANGUAGE_CODE': 'zh',
|
||||
'TIME_ZONE': 'Asia/Shanghai',
|
||||
'CHANGE_AUTH_PLAN_SECURE_MODE_ENABLED': True
|
||||
'CHANGE_AUTH_PLAN_SECURE_MODE_ENABLED': True,
|
||||
'USER_LOGIN_SINGLE_MACHINE_ENABLED': False,
|
||||
'TICKETS_ENABLED': True
|
||||
}
|
||||
|
||||
def compatible_auth_openid_of_key(self):
|
||||
@@ -438,6 +445,8 @@ class DynamicConfig:
|
||||
backends.insert(0, 'jms_oidc_rp.backends.OIDCAuthCodeBackend')
|
||||
if self.static_config.get('AUTH_RADIUS'):
|
||||
backends.insert(0, 'authentication.backends.radius.RadiusBackend')
|
||||
if self.static_config.get('AUTH_SSO'):
|
||||
backends.insert(0, 'authentication.backends.api.SSOAuthentication')
|
||||
return backends
|
||||
|
||||
def XPACK_LICENSE_IS_VALID(self):
|
||||
|
||||
@@ -32,7 +32,8 @@ if os.path.isfile(LDAP_CERT_FILE):
|
||||
# AUTH_LDAP_GROUP_SEARCH_OU, ldap.SCOPE_SUBTREE, AUTH_LDAP_GROUP_SEARCH_FILTER
|
||||
# )
|
||||
AUTH_LDAP_CONNECTION_OPTIONS = {
|
||||
ldap.OPT_TIMEOUT: CONFIG.AUTH_LDAP_CONNECT_TIMEOUT
|
||||
ldap.OPT_TIMEOUT: CONFIG.AUTH_LDAP_CONNECT_TIMEOUT,
|
||||
ldap.OPT_NETWORK_TIMEOUT: CONFIG.AUTH_LDAP_CONNECT_TIMEOUT
|
||||
}
|
||||
AUTH_LDAP_CACHE_TIMEOUT = 1
|
||||
AUTH_LDAP_ALWAYS_UPDATE_USER = True
|
||||
@@ -89,10 +90,15 @@ CAS_LOGIN_URL_NAME = "authentication:cas:cas-login"
|
||||
CAS_LOGOUT_URL_NAME = "authentication:cas:cas-logout"
|
||||
CAS_LOGIN_MSG = None
|
||||
CAS_LOGGED_MSG = None
|
||||
CAS_IGNORE_REFERER = True
|
||||
CAS_LOGOUT_COMPLETELY = CONFIG.CAS_LOGOUT_COMPLETELY
|
||||
CAS_VERSION = CONFIG.CAS_VERSION
|
||||
CAS_ROOT_PROXIED_AS = CONFIG.CAS_ROOT_PROXIED_AS
|
||||
CAS_CHECK_NEXT = lambda: lambda _next_page: True
|
||||
|
||||
# SSO Auth
|
||||
AUTH_SSO = CONFIG.AUTH_SSO
|
||||
AUTH_SSO_AUTHKEY_TTL = CONFIG.AUTH_SSO_AUTHKEY_TTL
|
||||
|
||||
# Other setting
|
||||
TOKEN_EXPIRATION = CONFIG.TOKEN_EXPIRATION
|
||||
|
||||
@@ -41,6 +41,8 @@ SECURITY_PASSWORD_RULES = [
|
||||
SECURITY_MFA_VERIFY_TTL = CONFIG.SECURITY_MFA_VERIFY_TTL
|
||||
SECURITY_VIEW_AUTH_NEED_MFA = CONFIG.SECURITY_VIEW_AUTH_NEED_MFA
|
||||
SECURITY_SERVICE_ACCOUNT_REGISTRATION = DYNAMIC.SECURITY_SERVICE_ACCOUNT_REGISTRATION
|
||||
SECURITY_LOGIN_CAPTCHA_ENABLED = CONFIG.SECURITY_LOGIN_CAPTCHA_ENABLED
|
||||
SECURITY_LOGIN_CHALLENGE_ENABLED = CONFIG.SECURITY_LOGIN_CHALLENGE_ENABLED
|
||||
|
||||
# Terminal other setting
|
||||
TERMINAL_PASSWORD_AUTH = DYNAMIC.TERMINAL_PASSWORD_AUTH
|
||||
@@ -68,6 +70,9 @@ FLOWER_URL = CONFIG.FLOWER_URL
|
||||
# Enable internal period task
|
||||
PERIOD_TASK_ENABLED = CONFIG.PERIOD_TASK_ENABLED
|
||||
|
||||
# only allow single machine login with the same account
|
||||
USER_LOGIN_SINGLE_MACHINE_ENABLED = CONFIG.USER_LOGIN_SINGLE_MACHINE_ENABLED
|
||||
|
||||
# Email custom content
|
||||
EMAIL_SUBJECT_PREFIX = DYNAMIC.EMAIL_SUBJECT_PREFIX
|
||||
EMAIL_SUFFIX = DYNAMIC.EMAIL_SUFFIX
|
||||
@@ -94,3 +99,7 @@ XPACK_LICENSE_IS_VALID = DYNAMIC.XPACK_LICENSE_IS_VALID
|
||||
LOGO_URLS = DYNAMIC.LOGO_URLS
|
||||
|
||||
CHANGE_AUTH_PLAN_SECURE_MODE_ENABLED = CONFIG.CHANGE_AUTH_PLAN_SECURE_MODE_ENABLED
|
||||
|
||||
DATETIME_DISPLAY_FORMAT = '%Y-%m-%d %H:%M:%S'
|
||||
|
||||
TICKETS_ENABLED = CONFIG.TICKETS_ENABLED
|
||||
|
||||
@@ -40,6 +40,7 @@ REST_FRAMEWORK = {
|
||||
'DATETIME_FORMAT': '%Y-%m-%d %H:%M:%S %z',
|
||||
'DATETIME_INPUT_FORMATS': ['iso-8601', '%Y-%m-%d %H:%M:%S %z'],
|
||||
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
|
||||
'EXCEPTION_HANDLER': 'common.drf.exc_handlers.common_exception_handler',
|
||||
# 'PAGE_SIZE': 100,
|
||||
# 'MAX_PAGE_SIZE': 5000
|
||||
|
||||
|
||||
@@ -75,8 +75,8 @@ if settings.DEBUG:
|
||||
urlpatterns += [
|
||||
re_path('^api/swagger(?P<format>\.json|\.yaml)$',
|
||||
views.get_swagger_view().without_ui(cache_timeout=1), name='schema-json'),
|
||||
path('api/docs/', views.get_swagger_view().with_ui('swagger', cache_timeout=1), name="docs"),
|
||||
path('api/redoc/', views.get_swagger_view().with_ui('redoc', cache_timeout=1), name='redoc'),
|
||||
re_path('api/docs/?', views.get_swagger_view().with_ui('swagger', cache_timeout=1), name="docs"),
|
||||
re_path('api/redoc/?', views.get_swagger_view().with_ui('redoc', cache_timeout=1), name='redoc'),
|
||||
|
||||
re_path('^api/v2/swagger(?P<format>\.json|\.yaml)$',
|
||||
views.get_swagger_view().without_ui(cache_timeout=1), name='schema-json'),
|
||||
|
||||
@@ -52,6 +52,7 @@ def redirect_format_api(request, *args, **kwargs):
|
||||
return JsonResponse({"msg": "Redirect url failed: {}".format(_path)}, status=404)
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
def redirect_old_apps_view(request, *args, **kwargs):
|
||||
path = request.get_full_path()
|
||||
if path.find('/core') != -1:
|
||||
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -93,6 +93,12 @@ def delete_celery_periodic_task(task_name):
|
||||
PeriodicTasks.update_changed()
|
||||
|
||||
|
||||
def get_celery_periodic_task(task_name):
|
||||
from django_celery_beat.models import PeriodicTask
|
||||
task = PeriodicTask.objects.filter(name=task_name).first()
|
||||
return task
|
||||
|
||||
|
||||
def get_celery_task_log_path(task_id):
|
||||
task_id = str(task_id)
|
||||
rel_path = os.path.join(task_id[0], task_id[1], task_id + '.log')
|
||||
|
||||
@@ -15,7 +15,10 @@ from .celery.decorator import (
|
||||
register_as_period_task, after_app_shutdown_clean_periodic,
|
||||
after_app_ready_start
|
||||
)
|
||||
from .celery.utils import create_or_update_celery_periodic_tasks
|
||||
from .celery.utils import (
|
||||
create_or_update_celery_periodic_tasks, get_celery_periodic_task,
|
||||
disable_celery_periodic_task, delete_celery_periodic_task
|
||||
)
|
||||
from .models import Task, CommandExecution, CeleryTask
|
||||
from .utils import send_server_performance_mail
|
||||
|
||||
@@ -95,6 +98,29 @@ def clean_celery_tasks_period():
|
||||
subprocess.call(command, shell=True)
|
||||
|
||||
|
||||
@shared_task
|
||||
@after_app_ready_start
|
||||
def clean_celery_periodic_tasks():
|
||||
"""清除celery定时任务"""
|
||||
need_cleaned_tasks = [
|
||||
'handle_be_interrupted_change_auth_task_periodic',
|
||||
]
|
||||
logger.info('Start clean celery periodic tasks: {}'.format(need_cleaned_tasks))
|
||||
for task_name in need_cleaned_tasks:
|
||||
logger.info('Start clean task: {}'.format(task_name))
|
||||
task = get_celery_periodic_task(task_name)
|
||||
if task is None:
|
||||
logger.info('Task does not exist: {}'.format(task_name))
|
||||
continue
|
||||
disable_celery_periodic_task(task_name)
|
||||
delete_celery_periodic_task(task_name)
|
||||
task = get_celery_periodic_task(task_name)
|
||||
if task is None:
|
||||
logger.info('Clean task success: {}'.format(task_name))
|
||||
else:
|
||||
logger.info('Clean task failure: {}'.format(task))
|
||||
|
||||
|
||||
@shared_task
|
||||
@after_app_ready_start
|
||||
def create_or_update_registered_periodic_tasks():
|
||||
|
||||
@@ -69,7 +69,7 @@ def send_server_performance_mail(path, usage, usages):
|
||||
from users.models import User
|
||||
subject = _("Disk used more than 80%: {} => {}").format(path, usage.percent)
|
||||
message = subject
|
||||
admins = User.objects.filter(role=User.ROLE_ADMIN)
|
||||
admins = User.objects.filter(role=User.ROLE.ADMIN)
|
||||
recipient_list = [u.email for u in admins if u.email]
|
||||
logger.info(subject)
|
||||
send_mail_async(subject, message, recipient_list, html_message=message)
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils.translation import ugettext as _
|
||||
from rest_framework import status, generics
|
||||
from rest_framework.views import Response
|
||||
from rest_framework_bulk import BulkModelViewSet
|
||||
|
||||
from common.permissions import IsSuperUserOrAppUser
|
||||
from .models import Organization
|
||||
from .serializers import OrgSerializer, OrgReadSerializer, \
|
||||
OrgMembershipUserSerializer, OrgMembershipAdminSerializer, \
|
||||
OrgAllUserSerializer, OrgRetrieveSerializer
|
||||
from common.drf.api import JMSBulkRelationModelViewSet
|
||||
from .models import Organization, ROLE
|
||||
from .serializers import (
|
||||
OrgSerializer, OrgReadSerializer,
|
||||
OrgRetrieveSerializer, OrgMemberSerializer
|
||||
)
|
||||
from users.models import User, UserGroup
|
||||
from assets.models import Asset, Domain, AdminUser, SystemUser, Label
|
||||
from perms.models import AssetPermission
|
||||
from orgs.utils import current_org
|
||||
from common.utils import get_logger
|
||||
from .mixins.api import OrgMembershipModelViewSetMixin
|
||||
from .filters import OrgMemberRelationFilterSet
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
@@ -39,7 +40,7 @@ class OrgViewSet(BulkModelViewSet):
|
||||
|
||||
def get_data_from_model(self, model):
|
||||
if model == User:
|
||||
data = model.objects.filter(related_user_orgs__id=self.org.id)
|
||||
data = model.objects.filter(orgs__id=self.org.id, m2m_org_members__role=ROLE.USER)
|
||||
else:
|
||||
data = model.objects.filter(org_id=self.org.id)
|
||||
return data
|
||||
@@ -64,26 +65,13 @@ class OrgViewSet(BulkModelViewSet):
|
||||
return Response({'msg': True}, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class OrgMembershipAdminsViewSet(OrgMembershipModelViewSetMixin, BulkModelViewSet):
|
||||
serializer_class = OrgMembershipAdminSerializer
|
||||
membership_class = Organization.admins.through
|
||||
permission_classes = (IsSuperUserOrAppUser, )
|
||||
|
||||
|
||||
class OrgMembershipUsersViewSet(OrgMembershipModelViewSetMixin, BulkModelViewSet):
|
||||
serializer_class = OrgMembershipUserSerializer
|
||||
membership_class = Organization.users.through
|
||||
permission_classes = (IsSuperUserOrAppUser, )
|
||||
|
||||
|
||||
class OrgAllUserListApi(generics.ListAPIView):
|
||||
class OrgMemberRelationBulkViewSet(JMSBulkRelationModelViewSet):
|
||||
permission_classes = (IsSuperUserOrAppUser,)
|
||||
serializer_class = OrgAllUserSerializer
|
||||
filter_fields = ("username", "name")
|
||||
search_fields = filter_fields
|
||||
m2m_field = Organization.members.field
|
||||
serializer_class = OrgMemberSerializer
|
||||
filterset_class = OrgMemberRelationFilterSet
|
||||
|
||||
def get_queryset(self):
|
||||
pk = self.kwargs.get("pk")
|
||||
org = get_object_or_404(Organization, pk=pk)
|
||||
users = org.get_org_users().only(*self.serializer_class.Meta.only_fields)
|
||||
return users
|
||||
def perform_bulk_destroy(self, queryset):
|
||||
objs = list(queryset.all().prefetch_related('user', 'org'))
|
||||
queryset.delete()
|
||||
self.send_m2m_changed_signal(objs, action='post_remove')
|
||||
|
||||
16
apps/orgs/filters.py
Normal file
16
apps/orgs/filters.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from django_filters.rest_framework import filterset
|
||||
from django_filters.rest_framework import filters
|
||||
|
||||
from .models import OrganizationMember
|
||||
|
||||
|
||||
class UUIDInFilter(filters.BaseInFilter, filters.UUIDFilter):
|
||||
pass
|
||||
|
||||
|
||||
class OrgMemberRelationFilterSet(filterset.FilterSet):
|
||||
id = UUIDInFilter(field_name='id', lookup_expr='in')
|
||||
|
||||
class Meta:
|
||||
model = OrganizationMember
|
||||
fields = ('org_id', 'user_id', 'role', 'id')
|
||||
33
apps/orgs/migrations/0004_organizationmember.py
Normal file
33
apps/orgs/migrations/0004_organizationmember.py
Normal file
@@ -0,0 +1,33 @@
|
||||
# Generated by Django 2.2.10 on 2020-07-21 11:27
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('orgs', '0003_auto_20190916_1057'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='OrganizationMember',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
|
||||
('role', models.CharField(choices=[('Admin', 'Administrator'), ('User', 'User'), ('Auditor', 'Auditor')], default='User', max_length=16, verbose_name='Role')),
|
||||
('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date created')),
|
||||
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
|
||||
('created_by', models.CharField(max_length=128, null=True, verbose_name='Created by')),
|
||||
('org', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='m2m_org_members', to='orgs.Organization', verbose_name='Organization')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='m2m_org_members', to=settings.AUTH_USER_MODEL, verbose_name='User')),
|
||||
],
|
||||
options={
|
||||
'db_table': 'orgs_organization_members',
|
||||
'unique_together': {('org', 'user', 'role')},
|
||||
},
|
||||
),
|
||||
]
|
||||
35
apps/orgs/migrations/0005_auto_20200721_1937.py
Normal file
35
apps/orgs/migrations/0005_auto_20200721_1937.py
Normal file
@@ -0,0 +1,35 @@
|
||||
# Generated by Django 2.2.10 on 2020-07-21 11:37
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def migrate_old_organization_members(apps, schema_editor):
|
||||
org_model = apps.get_model("orgs", "Organization")
|
||||
org_member_model = apps.get_model('orgs', 'OrganizationMember')
|
||||
orgs = org_model.objects.all()
|
||||
|
||||
roles = ['User', 'Auditor', 'Admin']
|
||||
|
||||
for org in orgs:
|
||||
users = org.users.all().only('id')
|
||||
auditors = org.auditors.all().only('id')
|
||||
admins = org.admins.all().only('id')
|
||||
total_members = zip([users, auditors, admins], roles)
|
||||
|
||||
org_members = []
|
||||
for members, role in total_members:
|
||||
for user in members:
|
||||
org_user = org_member_model(user=user, org=org, role=role)
|
||||
org_members.append(org_user)
|
||||
org_member_model.objects.bulk_create(org_members)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('orgs', '0004_organizationmember'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(migrate_old_organization_members)
|
||||
]
|
||||
32
apps/orgs/migrations/0006_auto_20200721_1937.py
Normal file
32
apps/orgs/migrations/0006_auto_20200721_1937.py
Normal file
@@ -0,0 +1,32 @@
|
||||
# Generated by Django 2.2.10 on 2020-07-21 11:37
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('orgs', '0005_auto_20200721_1937'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='organization',
|
||||
name='admins',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='organization',
|
||||
name='auditors',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='organization',
|
||||
name='users',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='organization',
|
||||
name='members',
|
||||
field=models.ManyToManyField(related_name='orgs', through='orgs.OrganizationMember', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
]
|
||||
18
apps/orgs/migrations/0007_auto_20200728_1805.py
Normal file
18
apps/orgs/migrations/0007_auto_20200728_1805.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 2.2.10 on 2020-07-28 10:05
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('orgs', '0006_auto_20200721_1937'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='organizationmember',
|
||||
name='role',
|
||||
field=models.CharField(choices=[('Admin', 'Organization administrator'), ('User', 'User'), ('Auditor', 'Organization auditor')], default='User', max_length=16, verbose_name='Role'),
|
||||
),
|
||||
]
|
||||
18
apps/orgs/migrations/0008_auto_20200819_2041.py
Normal file
18
apps/orgs/migrations/0008_auto_20200819_2041.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 2.2.13 on 2020-08-19 12:41
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('orgs', '0007_auto_20200728_1805'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='organizationmember',
|
||||
name='role',
|
||||
field=models.CharField(choices=[('Admin', 'Organization administrator'), ('Auditor', 'Organization auditor'), ('User', 'User')], default='User', max_length=16, verbose_name='Role'),
|
||||
),
|
||||
]
|
||||
@@ -55,8 +55,8 @@ class OrgBulkModelViewSet(CommonApiMixin, OrgQuerySetMixin, BulkModelViewSet):
|
||||
filtered_count = filtered.count()
|
||||
if filtered_count == 1:
|
||||
return True
|
||||
if qs_count <= filtered_count:
|
||||
return False
|
||||
if qs_count > filtered_count:
|
||||
return True
|
||||
if self.request.query_params.get('spm', ''):
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -62,7 +62,6 @@ class OrgModelMixin(models.Model):
|
||||
org = get_current_org()
|
||||
if org is None:
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
if org.is_real() or org.is_system():
|
||||
self.org_id = org.id
|
||||
elif org.is_default():
|
||||
|
||||
@@ -11,8 +11,7 @@ from ..utils import get_current_org_id_for_serializer
|
||||
|
||||
__all__ = [
|
||||
"OrgResourceSerializerMixin", "BulkOrgResourceSerializerMixin",
|
||||
"BulkOrgResourceModelSerializer", "OrgMembershipSerializerMixin",
|
||||
"OrgResourceModelSerializerMixin",
|
||||
"BulkOrgResourceModelSerializer", "OrgResourceModelSerializerMixin",
|
||||
]
|
||||
|
||||
|
||||
@@ -53,9 +52,3 @@ class BulkOrgResourceSerializerMixin(BulkSerializerMixin, OrgResourceSerializerM
|
||||
|
||||
class BulkOrgResourceModelSerializer(BulkOrgResourceSerializerMixin, serializers.ModelSerializer):
|
||||
pass
|
||||
|
||||
|
||||
class OrgMembershipSerializerMixin:
|
||||
def run_validation(self, initial_data=None):
|
||||
initial_data['organization'] = str(self.context['org'].id)
|
||||
return super().run_validation(initial_data)
|
||||
|
||||
@@ -1,21 +1,31 @@
|
||||
import uuid
|
||||
from django.conf import settings
|
||||
from functools import partial
|
||||
from itertools import chain
|
||||
|
||||
from django.db import models
|
||||
from django.db.models import signals
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from common.utils import is_uuid, lazyproperty
|
||||
from common.utils import is_uuid
|
||||
from common.const import choices
|
||||
from common.db.models import ChoiceSet
|
||||
|
||||
|
||||
class ROLE(ChoiceSet):
|
||||
ADMIN = choices.ADMIN, _('Organization administrator')
|
||||
AUDITOR = choices.AUDITOR, _("Organization auditor")
|
||||
USER = choices.USER, _('User')
|
||||
|
||||
|
||||
class Organization(models.Model):
|
||||
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||
name = models.CharField(max_length=128, unique=True, verbose_name=_("Name"))
|
||||
users = models.ManyToManyField('users.User', related_name='related_user_orgs', blank=True)
|
||||
admins = models.ManyToManyField('users.User', related_name='related_admin_orgs', blank=True)
|
||||
auditors = models.ManyToManyField('users.User', related_name='related_audit_orgs', blank=True)
|
||||
created_by = models.CharField(max_length=32, null=True, blank=True, verbose_name=_('Created by'))
|
||||
date_created = models.DateTimeField(auto_now_add=True, null=True, blank=True, verbose_name=_('Date created'))
|
||||
comment = models.TextField(max_length=128, default='', blank=True, verbose_name=_('Comment'))
|
||||
members = models.ManyToManyField('users.User', related_name='orgs', through='orgs.OrganizationMember',
|
||||
through_fields=('org', 'user'))
|
||||
|
||||
orgs = None
|
||||
CACHE_PREFIX = 'JMS_ORG_{}'
|
||||
@@ -72,29 +82,24 @@ class Organization(models.Model):
|
||||
org = cls.default() if default else None
|
||||
return org
|
||||
|
||||
# @lazyproperty
|
||||
# lazyproperty 导致用户列表中角色显示出现不稳定的情况, 如果不加会导致数据库操作次数太多
|
||||
def org_users(self):
|
||||
def get_org_members_by_role(self, role):
|
||||
from users.models import User
|
||||
if self.is_real():
|
||||
return self.users.all()
|
||||
users = User.objects.filter(role=User.ROLE_USER)
|
||||
if self.is_default() and not settings.DEFAULT_ORG_SHOW_ALL_USERS:
|
||||
users = users.filter(related_user_orgs__isnull=True)
|
||||
return self.members.filter(m2m_org_members__role=role)
|
||||
users = User.objects.filter(role=role)
|
||||
return users
|
||||
|
||||
def get_org_users(self):
|
||||
return self.org_users()
|
||||
@property
|
||||
def users(self):
|
||||
return self.get_org_members_by_role(ROLE.USER)
|
||||
|
||||
# @lazyproperty
|
||||
def org_admins(self):
|
||||
from users.models import User
|
||||
if self.is_real():
|
||||
return self.admins.all()
|
||||
return User.objects.filter(role=User.ROLE_ADMIN)
|
||||
@property
|
||||
def admins(self):
|
||||
return self.get_org_members_by_role(ROLE.ADMIN)
|
||||
|
||||
def get_org_admins(self):
|
||||
return self.org_admins()
|
||||
@property
|
||||
def auditors(self):
|
||||
return self.get_org_members_by_role(ROLE.AUDITOR)
|
||||
|
||||
def org_id(self):
|
||||
if self.is_real():
|
||||
@@ -104,87 +109,86 @@ class Organization(models.Model):
|
||||
else:
|
||||
return ''
|
||||
|
||||
# @lazyproperty
|
||||
def org_auditors(self):
|
||||
def get_members(self, exclude=()):
|
||||
from users.models import User
|
||||
if self.is_real():
|
||||
return self.auditors.all()
|
||||
return User.objects.filter(role=User.ROLE_AUDITOR)
|
||||
members = self.members.exclude(m2m_org_members__role__in=exclude)
|
||||
else:
|
||||
members = User.objects.exclude(role__in=exclude)
|
||||
|
||||
def get_org_auditors(self):
|
||||
return self.org_auditors()
|
||||
|
||||
def get_org_members(self, exclude=()):
|
||||
from users.models import User
|
||||
members = User.objects.none()
|
||||
if 'Admin' not in exclude:
|
||||
members |= self.get_org_admins()
|
||||
if 'User' not in exclude:
|
||||
members |= self.get_org_users()
|
||||
if 'Auditor' not in exclude:
|
||||
members |= self.get_org_auditors()
|
||||
return members.exclude(role=User.ROLE_APP).distinct()
|
||||
return members.exclude(role=User.ROLE.APP).distinct()
|
||||
|
||||
def can_admin_by(self, user):
|
||||
if user.is_superuser:
|
||||
return True
|
||||
if self.get_org_admins().filter(id=user.id):
|
||||
if self.admins.filter(id=user.id).exists():
|
||||
return True
|
||||
return False
|
||||
|
||||
def can_audit_by(self, user):
|
||||
if user.is_super_auditor:
|
||||
return True
|
||||
if self.get_org_auditors().filter(id=user.id):
|
||||
if self.auditors.filter(id=user.id).exists():
|
||||
return True
|
||||
return False
|
||||
|
||||
def can_user_by(self, user):
|
||||
if self.get_org_users().filter(id=user.id):
|
||||
if self.users.filter(id=user.id).exists():
|
||||
return True
|
||||
return False
|
||||
|
||||
def is_real(self):
|
||||
return self.id not in (self.DEFAULT_NAME, self.ROOT_ID, self.SYSTEM_ID)
|
||||
|
||||
@classmethod
|
||||
def get_user_orgs_by_role(cls, user, role):
|
||||
if not isinstance(role, (tuple, list)):
|
||||
role = (role, )
|
||||
|
||||
return cls.objects.filter(
|
||||
m2m_org_members__role__in=role,
|
||||
m2m_org_members__user_id=user.id
|
||||
).distinct()
|
||||
|
||||
@classmethod
|
||||
def get_user_all_orgs(cls, user):
|
||||
return [
|
||||
*cls.objects.filter(members=user).distinct(),
|
||||
cls.default()
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def get_user_admin_orgs(cls, user):
|
||||
admin_orgs = []
|
||||
if user.is_anonymous:
|
||||
return admin_orgs
|
||||
elif user.is_superuser:
|
||||
admin_orgs = list(cls.objects.all())
|
||||
admin_orgs.append(cls.default())
|
||||
elif user.is_org_admin:
|
||||
admin_orgs = user.related_admin_orgs.all()
|
||||
return admin_orgs
|
||||
return cls.objects.none()
|
||||
if user.is_superuser:
|
||||
return [*cls.objects.all(), cls.default()]
|
||||
return cls.get_user_orgs_by_role(user, ROLE.ADMIN)
|
||||
|
||||
@classmethod
|
||||
def get_user_user_orgs(cls, user):
|
||||
user_orgs = []
|
||||
if user.is_anonymous:
|
||||
return user_orgs
|
||||
user_orgs = user.related_user_orgs.all()
|
||||
return user_orgs
|
||||
return cls.objects.none()
|
||||
return [
|
||||
*cls.get_user_orgs_by_role(user, ROLE.USER),
|
||||
cls.default()
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def get_user_audit_orgs(cls, user):
|
||||
audit_orgs = []
|
||||
if user.is_anonymous:
|
||||
return audit_orgs
|
||||
elif user.is_super_auditor:
|
||||
audit_orgs = list(cls.objects.all())
|
||||
audit_orgs.append(cls.default())
|
||||
elif user.is_org_auditor:
|
||||
audit_orgs = user.related_audit_orgs.all()
|
||||
return audit_orgs
|
||||
return cls.objects.none()
|
||||
if user.is_super_auditor:
|
||||
return [*cls.objects.all(), cls.default()]
|
||||
return cls.get_user_orgs_by_role(user, ROLE.AUDITOR)
|
||||
|
||||
@classmethod
|
||||
def get_user_admin_or_audit_orgs(self, user):
|
||||
admin_orgs = self.get_user_admin_orgs(user)
|
||||
audit_orgs = self.get_user_audit_orgs(user)
|
||||
orgs = set(admin_orgs) | set(audit_orgs)
|
||||
return orgs
|
||||
def get_user_admin_or_audit_orgs(cls, user):
|
||||
if user.is_anonymous:
|
||||
return cls.objects.none()
|
||||
if user.is_superuser or user.is_super_auditor:
|
||||
return [*cls.objects.all(), cls.default()]
|
||||
return cls.get_user_orgs_by_role(user, (ROLE.AUDITOR, ROLE.ADMIN))
|
||||
|
||||
@classmethod
|
||||
def default(cls):
|
||||
@@ -211,8 +215,189 @@ class Organization(models.Model):
|
||||
from .utils import set_current_org
|
||||
set_current_org(self)
|
||||
|
||||
@classmethod
|
||||
def all_orgs(cls):
|
||||
orgs = list(cls.objects.all())
|
||||
orgs.append(cls.default())
|
||||
return orgs
|
||||
|
||||
def _convert_to_uuid_set(users):
|
||||
rst = set()
|
||||
for user in users:
|
||||
if isinstance(user, models.Model):
|
||||
rst.add(user.id)
|
||||
elif not isinstance(user, uuid.UUID):
|
||||
rst.add(uuid.UUID(user))
|
||||
return rst
|
||||
|
||||
|
||||
def _none2list(*args):
|
||||
return ([] if v is None else v for v in args)
|
||||
|
||||
|
||||
def _users2pks(users, admins, auditors):
|
||||
return [user.pk for user in chain(users, admins, auditors)]
|
||||
|
||||
|
||||
class UserRoleMapper(dict):
|
||||
def __init__(self, container=set):
|
||||
super().__init__()
|
||||
self.users = container()
|
||||
self.admins = container()
|
||||
self.auditors = container()
|
||||
|
||||
self[ROLE.USER] = self.users
|
||||
self[ROLE.ADMIN] = self.admins
|
||||
self[ROLE.AUDITOR] = self.auditors
|
||||
|
||||
|
||||
class OrgMemeberManager(models.Manager):
|
||||
|
||||
def remove_users(self, org, users):
|
||||
from users.models import User
|
||||
pk_set = []
|
||||
for user in users:
|
||||
if hasattr(user, 'pk'):
|
||||
pk_set.append(user.pk)
|
||||
else:
|
||||
pk_set.append(user)
|
||||
|
||||
send = partial(signals.m2m_changed.send, sender=self.model, instance=org, reverse=False,
|
||||
model=User, pk_set=pk_set, using=self.db)
|
||||
send(action="pre_remove")
|
||||
self.filter(org_id=org.id, user_id__in=pk_set).delete()
|
||||
send(action="post_remove")
|
||||
|
||||
def remove_users_by_role(self, org, users=None, admins=None, auditors=None):
|
||||
from users.models import User
|
||||
|
||||
if not any((users, admins, auditors)):
|
||||
return
|
||||
users, admins, auditors = _none2list(users, admins, auditors)
|
||||
|
||||
send = partial(signals.m2m_changed.send, sender=self.model, instance=org, reverse=False,
|
||||
model=User, pk_set=_users2pks(users, admins, auditors), using=self.db)
|
||||
|
||||
send(action="pre_remove")
|
||||
self.filter(org_id=org.id).filter(
|
||||
Q(user__in=users, role=ROLE.USER) |
|
||||
Q(user__in=admins, role=ROLE.ADMIN) |
|
||||
Q(user__in=auditors, role=ROLE.AUDITOR)
|
||||
).delete()
|
||||
send(action="post_remove")
|
||||
|
||||
def add_users_by_role(self, org, users=None, admins=None, auditors=None):
|
||||
from users.models import User
|
||||
|
||||
if not any((users, admins, auditors)):
|
||||
return
|
||||
users, admins, auditors = _none2list(users, admins, auditors)
|
||||
|
||||
add_mapper = (
|
||||
(users, ROLE.USER),
|
||||
(admins, ROLE.ADMIN),
|
||||
(auditors, ROLE.AUDITOR)
|
||||
)
|
||||
|
||||
oms_add = []
|
||||
for users, role in add_mapper:
|
||||
for user in users:
|
||||
if isinstance(user, models.Model):
|
||||
user = user.id
|
||||
oms_add.append(self.model(org_id=org.id, user_id=user, role=role))
|
||||
|
||||
send = partial(signals.m2m_changed.send, sender=self.model, instance=org, reverse=False,
|
||||
model=User, pk_set=_users2pks(users, admins, auditors), using=self.db)
|
||||
|
||||
send(action='pre_add')
|
||||
self.bulk_create(oms_add)
|
||||
send(action='post_add')
|
||||
|
||||
def _get_remove_add_set(self, new_users, old_users):
|
||||
if new_users is None:
|
||||
return None, None
|
||||
new_users = _convert_to_uuid_set(new_users)
|
||||
return (old_users - new_users), (new_users - old_users)
|
||||
|
||||
def set_user_roles(self, org, user, roles):
|
||||
"""
|
||||
设置某个用户在某个组织里的角色
|
||||
"""
|
||||
old_roles = set(self.filter(org_id=org.id, user=user).values_list('role', flat=True))
|
||||
new_roles = set(roles)
|
||||
|
||||
roles_remove = old_roles - new_roles
|
||||
roles_add = new_roles - old_roles
|
||||
|
||||
to_remove = UserRoleMapper()
|
||||
to_add = UserRoleMapper()
|
||||
|
||||
for role in roles_remove:
|
||||
if role in to_remove:
|
||||
to_remove[role].add(user)
|
||||
for role in roles_add:
|
||||
if role in to_add:
|
||||
to_add[role].add(user)
|
||||
|
||||
self.remove_users_by_role(
|
||||
org,
|
||||
to_remove.users,
|
||||
to_remove.admins,
|
||||
to_remove.auditors
|
||||
)
|
||||
|
||||
self.add_users_by_role(
|
||||
org,
|
||||
to_add.users,
|
||||
to_add.admins,
|
||||
to_add.auditors
|
||||
)
|
||||
|
||||
def set_users_by_role(self, org, users=None, admins=None, auditors=None):
|
||||
"""
|
||||
给组织设置带角色的用户
|
||||
"""
|
||||
|
||||
oms = self.filter(org_id=org.id).values_list('role', 'user_id')
|
||||
|
||||
old_mapper = UserRoleMapper()
|
||||
|
||||
for role, user_id in oms:
|
||||
if role in old_mapper:
|
||||
old_mapper[role].add(user_id)
|
||||
|
||||
users_remove, users_add = self._get_remove_add_set(users, old_mapper.users)
|
||||
admins_remove, admins_add = self._get_remove_add_set(admins, old_mapper.admins)
|
||||
auditors_remove, auditors_add = self._get_remove_add_set(auditors, old_mapper.auditors)
|
||||
|
||||
self.remove_users_by_role(
|
||||
org,
|
||||
users_remove,
|
||||
admins_remove,
|
||||
auditors_remove
|
||||
)
|
||||
|
||||
self.add_users_by_role(
|
||||
org,
|
||||
users_add,
|
||||
admins_add,
|
||||
auditors_add
|
||||
)
|
||||
|
||||
|
||||
class OrganizationMember(models.Model):
|
||||
"""
|
||||
注意:直接调用该 `Model.delete` `Model.objects.delete` 不会触发清理该用户的信号
|
||||
"""
|
||||
|
||||
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||
org = models.ForeignKey(Organization, related_name='m2m_org_members', on_delete=models.CASCADE, verbose_name=_('Organization'))
|
||||
user = models.ForeignKey('users.User', related_name='m2m_org_members', on_delete=models.CASCADE, verbose_name=_('User'))
|
||||
role = models.CharField(max_length=16, choices=ROLE.choices, default=ROLE.USER, verbose_name=_("Role"))
|
||||
date_created = models.DateTimeField(auto_now_add=True, verbose_name=_("Date created"))
|
||||
date_updated = models.DateTimeField(auto_now=True, verbose_name=_("Date updated"))
|
||||
created_by = models.CharField(max_length=128, null=True, verbose_name=_('Created by'))
|
||||
|
||||
objects = OrgMemeberManager()
|
||||
|
||||
class Meta:
|
||||
unique_together = [('org', 'user', 'role')]
|
||||
db_table = 'orgs_organization_members'
|
||||
|
||||
def __str__(self):
|
||||
return '{} is {}: {}'.format(self.user.name, self.org.name, self.role)
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
|
||||
from django.db.models import F
|
||||
from rest_framework.serializers import ModelSerializer
|
||||
from rest_framework import serializers
|
||||
from users.models import UserGroup
|
||||
from assets.models import Asset, Domain, AdminUser, SystemUser, Label
|
||||
from perms.models import AssetPermission
|
||||
|
||||
from users.models.user import User
|
||||
from common.serializers import AdaptedBulkListSerializer
|
||||
from .utils import set_current_org, get_current_org
|
||||
from .models import Organization
|
||||
from .mixins.serializers import OrgMembershipSerializerMixin
|
||||
from common.drf.serializers import BulkModelSerializer
|
||||
from common.db.models import concated_display as display
|
||||
from .models import Organization, OrganizationMember
|
||||
|
||||
|
||||
class OrgSerializer(ModelSerializer):
|
||||
users = serializers.PrimaryKeyRelatedField(many=True, queryset=User.objects.all(), write_only=True, required=False)
|
||||
admins = serializers.PrimaryKeyRelatedField(many=True, queryset=User.objects.all(), write_only=True, required=False)
|
||||
auditors = serializers.PrimaryKeyRelatedField(many=True, queryset=User.objects.all(), write_only=True, required=False)
|
||||
|
||||
class Meta:
|
||||
model = Organization
|
||||
list_serializer_class = AdaptedBulkListSerializer
|
||||
@@ -21,41 +24,48 @@ class OrgSerializer(ModelSerializer):
|
||||
fields_m2m = ['users', 'admins', 'auditors']
|
||||
fields = fields_small + fields_m2m
|
||||
read_only_fields = ['created_by', 'date_created']
|
||||
extra_kwargs = {
|
||||
'admins': {'write_only': True},
|
||||
'users': {'write_only': True},
|
||||
'auditors': {'write_only': True},
|
||||
}
|
||||
|
||||
def create(self, validated_data):
|
||||
members = self._pop_memebers(validated_data)
|
||||
instance = Organization.objects.create(**validated_data)
|
||||
OrganizationMember.objects.add_users_by_role(instance, *members)
|
||||
return instance
|
||||
|
||||
def _pop_memebers(self, validated_data):
|
||||
return (
|
||||
validated_data.pop('users', None),
|
||||
validated_data.pop('admins', None),
|
||||
validated_data.pop('auditors', None)
|
||||
)
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
members = self._pop_memebers(validated_data)
|
||||
for attr, value in validated_data.items():
|
||||
setattr(instance, attr, value)
|
||||
instance.save()
|
||||
OrganizationMember.objects.set_users_by_role(instance, *members)
|
||||
return instance
|
||||
|
||||
|
||||
class OrgReadSerializer(OrgSerializer):
|
||||
pass
|
||||
|
||||
|
||||
class OrgMembershipAdminSerializer(OrgMembershipSerializerMixin, ModelSerializer):
|
||||
class Meta:
|
||||
model = Organization.admins.through
|
||||
list_serializer_class = AdaptedBulkListSerializer
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class OrgMembershipUserSerializer(OrgMembershipSerializerMixin, ModelSerializer):
|
||||
class Meta:
|
||||
model = Organization.users.through
|
||||
list_serializer_class = AdaptedBulkListSerializer
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class OrgAllUserSerializer(serializers.Serializer):
|
||||
user = serializers.UUIDField(read_only=True, source='id')
|
||||
user_display = serializers.SerializerMethodField()
|
||||
class OrgMemberSerializer(BulkModelSerializer):
|
||||
org_display = serializers.CharField(read_only=True)
|
||||
user_display = serializers.CharField(read_only=True)
|
||||
role_display = serializers.CharField(source='get_role_display', read_only=True)
|
||||
|
||||
class Meta:
|
||||
only_fields = ['id', 'username', 'name']
|
||||
model = OrganizationMember
|
||||
fields = ('id', 'org', 'user', 'role', 'org_display', 'user_display', 'role_display')
|
||||
|
||||
@staticmethod
|
||||
def get_user_display(obj):
|
||||
return str(obj)
|
||||
@classmethod
|
||||
def setup_eager_loading(cls, queryset):
|
||||
return queryset.annotate(
|
||||
org_display=F('org__name'),
|
||||
user_display=display('user__name', 'user__username')
|
||||
).distinct()
|
||||
|
||||
|
||||
class OrgRetrieveSerializer(OrgReadSerializer):
|
||||
|
||||
@@ -5,9 +5,11 @@ from django.db.models.signals import m2m_changed
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
|
||||
from .models import Organization
|
||||
from orgs.utils import tmp_to_org
|
||||
from .models import Organization, OrganizationMember
|
||||
from .hands import set_current_org, current_org, Node, get_current_org
|
||||
from perms.models import AssetPermission
|
||||
from perms.models import (AssetPermission, DatabaseAppPermission,
|
||||
K8sAppPermission, RemoteAppPermission)
|
||||
from users.models import UserGroup
|
||||
|
||||
|
||||
@@ -26,23 +28,47 @@ def on_org_create_or_update(sender, instance=None, created=False, **kwargs):
|
||||
instance.expire_cache()
|
||||
|
||||
|
||||
@receiver(m2m_changed, sender=Organization.users.through)
|
||||
def on_org_user_changed(sender, instance=None, **kwargs):
|
||||
if isinstance(instance, Organization):
|
||||
old_org = current_org
|
||||
set_current_org(instance)
|
||||
if kwargs['action'] == 'pre_remove':
|
||||
users = kwargs['model'].objects.filter(pk__in=kwargs['pk_set'])
|
||||
for user in users:
|
||||
perms = AssetPermission.objects.filter(users=user)
|
||||
user_groups = UserGroup.objects.filter(users=user)
|
||||
for perm in perms:
|
||||
perm.users.remove(user)
|
||||
for user_group in user_groups:
|
||||
user_group.users.remove(user)
|
||||
set_current_org(old_org)
|
||||
def _remove_users(model, users, org):
|
||||
with tmp_to_org(org):
|
||||
if not isinstance(users, (tuple, list, set)):
|
||||
users = (users, )
|
||||
|
||||
m2m_model = model.users.through
|
||||
if model.users.reverse:
|
||||
m2m_field_name = model.users.field.m2m_reverse_field_name()
|
||||
else:
|
||||
m2m_field_name = model.users.field.m2m_field_name()
|
||||
m2m_model.objects.filter(**{'user__in': users, f'{m2m_field_name}__org_id': org.id}).delete()
|
||||
|
||||
|
||||
@receiver(m2m_changed, sender=Organization.admins.through)
|
||||
def on_org_admin_change(sender, **kwargs):
|
||||
Organization._user_admin_orgs = None
|
||||
def _clear_users_from_org(org, users):
|
||||
"""
|
||||
清理用户在该组织下的相关数据
|
||||
"""
|
||||
if not users:
|
||||
return
|
||||
|
||||
models = (AssetPermission, DatabaseAppPermission,
|
||||
RemoteAppPermission, K8sAppPermission, UserGroup)
|
||||
|
||||
for m in models:
|
||||
_remove_users(m, users, org)
|
||||
|
||||
|
||||
@receiver(m2m_changed, sender=OrganizationMember)
|
||||
def on_org_user_changed(action, instance, reverse, pk_set, **kwargs):
|
||||
if action == 'post_remove':
|
||||
if reverse:
|
||||
user = instance
|
||||
org_pk_set = pk_set
|
||||
|
||||
orgs = Organization.objects.filter(id__in=org_pk_set)
|
||||
for org in orgs:
|
||||
if not org.members.filter(id=user.id).exists():
|
||||
_clear_users_from_org(org, user)
|
||||
else:
|
||||
org = instance
|
||||
user_pk_set = pk_set
|
||||
|
||||
leaved_users = set(pk_set) - set(org.members.filter(id__in=user_pk_set).values_list('id', flat=True))
|
||||
_clear_users_from_org(org, leaved_users)
|
||||
|
||||
@@ -1,3 +1,16 @@
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
from rest_framework.test import APITestCase
|
||||
from rest_framework import status
|
||||
|
||||
from users.models.user import User
|
||||
|
||||
|
||||
class OrgTests(APITestCase):
|
||||
def test_create(self):
|
||||
print(User.objects.all())
|
||||
reverse('api-orgs:org-list')
|
||||
|
||||
|
||||
|
||||
{"name":"a-07","admins":["138167d2-6843-4e25-b838-59657157c6c6"],"auditors":["8d4b3ec4-8339-4a2c-b33c-c2633da62c84"],"users":["ea60e8ce-876d-493b-a641-ff836258629c"]}
|
||||
|
||||
# Create your tests here.
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from django.urls import re_path, path
|
||||
from django.urls import re_path
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from rest_framework_bulk.routes import BulkRouter
|
||||
|
||||
from common import api as capi
|
||||
from .. import api
|
||||
@@ -10,21 +11,13 @@ from .. import api
|
||||
|
||||
app_name = 'orgs'
|
||||
router = DefaultRouter()
|
||||
|
||||
# 将会删除
|
||||
router.register(r'orgs/(?P<org_id>[0-9a-zA-Z\-]{36})/membership/admins',
|
||||
api.OrgMembershipAdminsViewSet, 'membership-admins')
|
||||
router.register(r'orgs/(?P<org_id>[0-9a-zA-Z\-]{36})/membership/users',
|
||||
api.OrgMembershipUsersViewSet, 'membership-users'),
|
||||
bulk_router = BulkRouter()
|
||||
|
||||
router.register(r'orgs', api.OrgViewSet, 'org')
|
||||
bulk_router.register(r'org-memeber-relation', api.OrgMemberRelationBulkViewSet, 'org-memeber-relation')
|
||||
|
||||
old_version_urlpatterns = [
|
||||
re_path('(?P<resource>org)/.*', capi.redirect_plural_name_api)
|
||||
]
|
||||
|
||||
urlpatterns = [
|
||||
path('<uuid:pk>/users/all/', api.OrgAllUserListApi.as_view(), name='org-all-users'),
|
||||
]
|
||||
|
||||
urlpatterns += router.urls + old_version_urlpatterns
|
||||
urlpatterns = router.urls + bulk_router.urls + old_version_urlpatterns
|
||||
|
||||
@@ -12,3 +12,6 @@ from .database_app_permission import *
|
||||
from .database_app_permission_relation import *
|
||||
from .user_database_app_permission import *
|
||||
from .system_user_permission import *
|
||||
from .k8s_app_permission import *
|
||||
from .k8s_app_permission_relation import *
|
||||
from .user_k8s_app_permission import *
|
||||
|
||||
21
apps/perms/api/k8s_app_permission.py
Normal file
21
apps/perms/api/k8s_app_permission.py
Normal file
@@ -0,0 +1,21 @@
|
||||
# coding: utf-8
|
||||
#
|
||||
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
|
||||
from .. import models, serializers
|
||||
from common.permissions import IsOrgAdmin
|
||||
|
||||
|
||||
__all__ = ['K8sAppPermissionViewSet']
|
||||
|
||||
|
||||
class K8sAppPermissionViewSet(OrgBulkModelViewSet):
|
||||
model = models.K8sAppPermission
|
||||
serializer_classes = {
|
||||
'default': serializers.K8sAppPermissionSerializer,
|
||||
'display': serializers.K8sAppPermissionListSerializer
|
||||
}
|
||||
filter_fields = ('name',)
|
||||
search_fields = filter_fields
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
111
apps/perms/api/k8s_app_permission_relation.py
Normal file
111
apps/perms/api/k8s_app_permission_relation.py
Normal file
@@ -0,0 +1,111 @@
|
||||
# coding: utf-8
|
||||
#
|
||||
from rest_framework import generics
|
||||
from django.db.models import F, Value
|
||||
from django.db.models.functions import Concat
|
||||
from django.shortcuts import get_object_or_404
|
||||
|
||||
from common.permissions import IsOrgAdmin
|
||||
from .base import RelationViewSet
|
||||
from .. import models, serializers
|
||||
|
||||
|
||||
class K8sAppPermissionUserRelationViewSet(RelationViewSet):
|
||||
serializer_class = serializers.K8sAppPermissionUserRelationSerializer
|
||||
m2m_field = models.K8sAppPermission.users.field
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
filter_fields = [
|
||||
'id', 'user', 'k8sapppermission'
|
||||
]
|
||||
search_fields = ('user__name', 'user__username', 'k8sapppermission__name')
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
queryset = queryset.annotate(user_display=F('user__name'))
|
||||
return queryset
|
||||
|
||||
|
||||
class K8sAppPermissionUserGroupRelationViewSet(RelationViewSet):
|
||||
serializer_class = serializers.K8sAppPermissionUserGroupRelationSerializer
|
||||
m2m_field = models.K8sAppPermission.user_groups.field
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
filter_fields = [
|
||||
'id', "usergroup", "k8sapppermission"
|
||||
]
|
||||
search_fields = ["usergroup__name", "k8sapppermission__name"]
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
queryset = queryset \
|
||||
.annotate(usergroup_display=F('usergroup__name'))
|
||||
return queryset
|
||||
|
||||
|
||||
class K8sAppPermissionAllUserListApi(generics.ListAPIView):
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
serializer_class = serializers.K8sAppPermissionAllUserSerializer
|
||||
filter_fields = ("username", "name")
|
||||
search_fields = filter_fields
|
||||
|
||||
def get_queryset(self):
|
||||
pk = self.kwargs.get("pk")
|
||||
perm = get_object_or_404(models.K8sAppPermission, pk=pk)
|
||||
users = perm.get_all_users().only(
|
||||
*self.serializer_class.Meta.only_fields
|
||||
)
|
||||
return users
|
||||
|
||||
|
||||
class K8sAppPermissionK8sAppRelationViewSet(RelationViewSet):
|
||||
serializer_class = serializers.K8sAppPermissionK8sAppRelationSerializer
|
||||
m2m_field = models.K8sAppPermission.k8s_apps.field
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
filter_fields = [
|
||||
'id', 'k8sapp', 'k8sapppermission',
|
||||
]
|
||||
search_fields = [
|
||||
"id", "k8sapp__name", "k8sapppermission__name"
|
||||
]
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
queryset = queryset \
|
||||
.annotate(k8sapp_display=F('k8sapp__name'))
|
||||
return queryset
|
||||
|
||||
|
||||
class K8sAppPermissionAllK8sAppListApi(generics.ListAPIView):
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
serializer_class = serializers.K8sAppPermissionAllK8sAppSerializer
|
||||
filter_fields = ("name",)
|
||||
search_fields = filter_fields
|
||||
|
||||
def get_queryset(self):
|
||||
pk = self.kwargs.get("pk")
|
||||
perm = get_object_or_404(models.K8sAppPermission, pk=pk)
|
||||
database_apps = perm.get_all_k8s_apps().only(
|
||||
*self.serializer_class.Meta.only_fields
|
||||
)
|
||||
return database_apps
|
||||
|
||||
|
||||
class K8sAppPermissionSystemUserRelationViewSet(RelationViewSet):
|
||||
serializer_class = serializers.K8sAppPermissionSystemUserRelationSerializer
|
||||
m2m_field = models.K8sAppPermission.system_users.field
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
filter_fields = [
|
||||
'id', 'systemuser', 'k8sapppermission'
|
||||
]
|
||||
search_fields = [
|
||||
'k8sapppermission__name', 'systemuser__name', 'systemuser__username'
|
||||
]
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
queryset = queryset.annotate(
|
||||
systemuser_display=Concat(
|
||||
F('systemuser__name'), Value('('), F('systemuser__username'),
|
||||
Value(')')
|
||||
)
|
||||
)
|
||||
return queryset
|
||||
@@ -21,7 +21,7 @@ class UserPermissionMixin:
|
||||
obj = None
|
||||
|
||||
def initial(self, *args, **kwargs):
|
||||
super().initial(*args, *kwargs)
|
||||
super().initial(*args, **kwargs)
|
||||
self.obj = self.get_obj()
|
||||
|
||||
def get_obj(self):
|
||||
|
||||
119
apps/perms/api/user_k8s_app_permission.py
Normal file
119
apps/perms/api/user_k8s_app_permission.py
Normal file
@@ -0,0 +1,119 @@
|
||||
# coding: utf-8
|
||||
#
|
||||
|
||||
import uuid
|
||||
from django.shortcuts import get_object_or_404
|
||||
from rest_framework.views import APIView, Response
|
||||
from common.permissions import IsOrgAdminOrAppUser, IsValidUser
|
||||
from common.tree import TreeNodeSerializer
|
||||
from orgs.mixins import generics
|
||||
from users.models import User, UserGroup
|
||||
from applications.serializers import K8sAppSerializer
|
||||
from applications.models import K8sApp
|
||||
from assets.models import SystemUser
|
||||
from .. import utils, serializers
|
||||
from .mixin import UserPermissionMixin
|
||||
|
||||
|
||||
class UserGrantedK8sAppsApi(generics.ListAPIView):
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
serializer_class = K8sAppSerializer
|
||||
filter_fields = ['id', 'name', 'type', 'comment']
|
||||
search_fields = ['name', 'comment']
|
||||
|
||||
def get_object(self):
|
||||
user_id = self.kwargs.get('pk', '')
|
||||
if user_id:
|
||||
user = get_object_or_404(User, id=user_id)
|
||||
else:
|
||||
user = self.request.user
|
||||
return user
|
||||
|
||||
def get_queryset(self):
|
||||
util = utils.K8sAppPermissionUtil(self.get_object())
|
||||
queryset = util.get_k8s_apps()
|
||||
return queryset
|
||||
|
||||
def get_permissions(self):
|
||||
if self.kwargs.get('pk') is None:
|
||||
self.permission_classes = (IsValidUser,)
|
||||
return super().get_permissions()
|
||||
|
||||
|
||||
class UserGrantedK8sAppsAsTreeApi(UserGrantedK8sAppsApi):
|
||||
serializer_class = TreeNodeSerializer
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
|
||||
def get_serializer(self, k8s_apps, *args, **kwargs):
|
||||
if k8s_apps is None:
|
||||
k8s_apps = []
|
||||
only_k8s_app = self.request.query_params.get('only', '0') == '1'
|
||||
tree_root = None
|
||||
data = []
|
||||
if not only_k8s_app:
|
||||
tree_root = utils.construct_k8s_apps_tree_root()
|
||||
data.append(tree_root)
|
||||
for k8s_app in k8s_apps:
|
||||
node = utils.parse_k8s_app_to_tree_node(tree_root, k8s_app)
|
||||
data.append(node)
|
||||
data.sort()
|
||||
return super().get_serializer(data, many=True)
|
||||
|
||||
|
||||
class UserGrantedK8sAppSystemUsersApi(UserPermissionMixin, generics.ListAPIView):
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
serializer_class = serializers.K8sAppSystemUserSerializer
|
||||
only_fields = serializers.K8sAppSystemUserSerializer.Meta.only_fields
|
||||
|
||||
def get_queryset(self):
|
||||
util = utils.K8sAppPermissionUtil(self.obj)
|
||||
k8s_app_id = self.kwargs.get('k8s_app_id')
|
||||
k8s_app = get_object_or_404(K8sApp, id=k8s_app_id)
|
||||
system_users = util.get_k8s_app_system_users(k8s_app)
|
||||
return system_users
|
||||
|
||||
|
||||
# Validate
|
||||
|
||||
class ValidateUserK8sAppPermissionApi(APIView):
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
user_id = request.query_params.get('user_id', '')
|
||||
k8s_app_id = request.query_params.get('k8s_app_id', '')
|
||||
system_user_id = request.query_params.get('system_user_id', '')
|
||||
|
||||
try:
|
||||
user_id = uuid.UUID(user_id)
|
||||
k8s_app_id = uuid.UUID(k8s_app_id)
|
||||
system_user_id = uuid.UUID(system_user_id)
|
||||
except ValueError:
|
||||
return Response({'msg': False}, status=403)
|
||||
|
||||
user = get_object_or_404(User, id=user_id)
|
||||
k8s_app = get_object_or_404(K8sApp, id=k8s_app_id)
|
||||
system_user = get_object_or_404(SystemUser, id=system_user_id)
|
||||
|
||||
util = utils.K8sAppPermissionUtil(user)
|
||||
system_users = util.get_k8s_app_system_users(k8s_app)
|
||||
if system_user in system_users:
|
||||
return Response({'msg': True}, status=200)
|
||||
|
||||
return Response({'msg': False}, status=403)
|
||||
|
||||
|
||||
# UserGroup
|
||||
|
||||
class UserGroupGrantedK8sAppsApi(generics.ListAPIView):
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
serializer_class = K8sAppSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = []
|
||||
user_group_id = self.kwargs.get('pk')
|
||||
if not user_group_id:
|
||||
return queryset
|
||||
user_group = get_object_or_404(UserGroup, id=user_group_id)
|
||||
util = utils.K8sAppPermissionUtil(user_group)
|
||||
queryset = util.get_k8s_apps()
|
||||
return queryset
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user