mirror of https://github.com/jumpserver/jumpserver
commit
7f52675bd3
|
@ -1,18 +0,0 @@
|
|||
name: Send LGTM reaction
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@1.0.0
|
||||
- uses: micnncim/action-lgtm-reaction@master
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
trigger: '["^.?lgtm$"]'
|
45
Dockerfile
45
Dockerfile
|
@ -1,20 +1,5 @@
|
|||
# 编译代码
|
||||
FROM python:3.8-slim as stage-build
|
||||
MAINTAINER JumpServer Team <ibuler@qq.com>
|
||||
ARG VERSION
|
||||
ENV VERSION=$VERSION
|
||||
|
||||
WORKDIR /opt/jumpserver
|
||||
ADD . .
|
||||
RUN cd utils && bash -ixeu build.sh
|
||||
|
||||
FROM python:3.8-slim
|
||||
ARG PIP_MIRROR=https://pypi.douban.com/simple
|
||||
ENV PIP_MIRROR=$PIP_MIRROR
|
||||
ARG PIP_JMS_MIRROR=https://pypi.douban.com/simple
|
||||
ENV PIP_JMS_MIRROR=$PIP_JMS_MIRROR
|
||||
|
||||
WORKDIR /opt/jumpserver
|
||||
MAINTAINER JumpServer Team <ibuler@qq.com>
|
||||
|
||||
ARG BUILD_DEPENDENCIES=" \
|
||||
g++ \
|
||||
|
@ -62,21 +47,37 @@ RUN sed -i 's/deb.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list \
|
|||
&& mv /bin/sh /bin/sh.bak \
|
||||
&& ln -s /bin/bash /bin/sh
|
||||
|
||||
RUN mkdir -p /opt/jumpserver/oracle/ \
|
||||
RUN mkdir -p /opt/oracle/ \
|
||||
&& wget https://download.jumpserver.org/public/instantclient-basiclite-linux.x64-21.1.0.0.0.tar \
|
||||
&& tar xf instantclient-basiclite-linux.x64-21.1.0.0.0.tar -C /opt/jumpserver/oracle/ \
|
||||
&& echo "/opt/jumpserver/oracle/instantclient_21_1" > /etc/ld.so.conf.d/oracle-instantclient.conf \
|
||||
&& tar xf instantclient-basiclite-linux.x64-21.1.0.0.0.tar -C /opt/oracle/ \
|
||||
&& echo "/opt/oracle/instantclient_21_1" > /etc/ld.so.conf.d/oracle-instantclient.conf \
|
||||
&& ldconfig \
|
||||
&& rm -f instantclient-basiclite-linux.x64-21.1.0.0.0.tar
|
||||
|
||||
COPY --from=stage-build /opt/jumpserver/release/jumpserver /opt/jumpserver
|
||||
WORKDIR /tmp/build
|
||||
COPY ./requirements ./requirements
|
||||
|
||||
RUN echo > config.yml \
|
||||
&& pip install --upgrade pip==20.2.4 setuptools==49.6.0 wheel==0.34.2 -i ${PIP_MIRROR} \
|
||||
ARG PIP_MIRROR=https://mirrors.aliyun.com/pypi/simple/
|
||||
ENV PIP_MIRROR=$PIP_MIRROR
|
||||
ARG PIP_JMS_MIRROR=https://mirrors.aliyun.com/pypi/simple/
|
||||
ENV PIP_JMS_MIRROR=$PIP_JMS_MIRROR
|
||||
# 因为以 jms 或者 jumpserver 开头的 mirror 上可能没有
|
||||
RUN pip install --upgrade pip==20.2.4 setuptools==49.6.0 wheel==0.34.2 -i ${PIP_MIRROR} \
|
||||
&& pip install --no-cache-dir $(grep -E 'jms|jumpserver' requirements/requirements.txt) -i ${PIP_JMS_MIRROR} \
|
||||
&& pip install --no-cache-dir -r requirements/requirements.txt -i ${PIP_MIRROR} \
|
||||
&& rm -rf ~/.cache/pip
|
||||
|
||||
ARG VERSION
|
||||
ENV VERSION=$VERSION
|
||||
|
||||
ADD . .
|
||||
RUN cd utils \
|
||||
&& bash -ixeu build.sh \
|
||||
&& mv ../release/jumpserver /opt/jumpserver \
|
||||
&& rm -rf /tmp/build \
|
||||
&& echo > /opt/jumpserver/config.yml
|
||||
|
||||
WORKDIR /opt/jumpserver
|
||||
VOLUME /opt/jumpserver/data
|
||||
VOLUME /opt/jumpserver/logs
|
||||
|
||||
|
|
|
@ -131,4 +131,3 @@ Licensed under The GNU General Public License version 3 (GPLv3) (the "License")
|
|||
https://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ from django.db.models import F, Q
|
|||
|
||||
from common.drf.filters import BaseFilterSet
|
||||
from common.drf.api import JMSBulkModelViewSet
|
||||
from common.mixins import RecordViewLogMixin
|
||||
from rbac.permissions import RBACPermission
|
||||
from assets.models import SystemUser
|
||||
from ..models import Account
|
||||
|
@ -54,7 +55,7 @@ class SystemUserAppRelationViewSet(ApplicationAccountViewSet):
|
|||
perm_model = SystemUser
|
||||
|
||||
|
||||
class ApplicationAccountSecretViewSet(ApplicationAccountViewSet):
|
||||
class ApplicationAccountSecretViewSet(RecordViewLogMixin, ApplicationAccountViewSet):
|
||||
serializer_class = serializers.AppAccountSecretSerializer
|
||||
permission_classes = [RBACPermission, NeedMFAVerify]
|
||||
http_method_names = ['get', 'options']
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# Generated by Django 2.1.7 on 2019-05-20 11:04
|
||||
|
||||
import common.fields.model
|
||||
import common.db.fields
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
|
@ -23,7 +23,7 @@ class Migration(migrations.Migration):
|
|||
('name', models.CharField(max_length=128, verbose_name='Name')),
|
||||
('type', models.CharField(choices=[('Browser', (('chrome', 'Chrome'),)), ('Database tools', (('mysql_workbench', 'MySQL Workbench'),)), ('Virtualization tools', (('vmware_client', 'vSphere Client'),)), ('custom', 'Custom')], default='chrome', max_length=128, verbose_name='App type')),
|
||||
('path', models.CharField(max_length=128, verbose_name='App path')),
|
||||
('params', common.fields.model.EncryptJsonDictTextField(blank=True, default={}, max_length=4096, null=True, verbose_name='Parameters')),
|
||||
('params', common.db.fields.EncryptJsonDictTextField(blank=True, default={}, max_length=4096, null=True, verbose_name='Parameters')),
|
||||
('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')),
|
||||
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
|
||||
('comment', models.TextField(blank=True, default='', max_length=128, verbose_name='Comment')),
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# Generated by Django 3.1.12 on 2021-08-26 09:07
|
||||
|
||||
import assets.models.base
|
||||
import common.fields.model
|
||||
import common.db.fields
|
||||
from django.conf import settings
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
|
@ -26,9 +26,9 @@ class Migration(migrations.Migration):
|
|||
('id', models.UUIDField(db_index=True, default=uuid.uuid4)),
|
||||
('name', models.CharField(max_length=128, verbose_name='Name')),
|
||||
('username', 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')),
|
||||
('password', common.fields.model.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password')),
|
||||
('private_key', common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH private key')),
|
||||
('public_key', common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH public key')),
|
||||
('password', common.db.fields.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password')),
|
||||
('private_key', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH private key')),
|
||||
('public_key', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH public key')),
|
||||
('comment', models.TextField(blank=True, verbose_name='Comment')),
|
||||
('date_created', models.DateTimeField(blank=True, editable=False, verbose_name='Date created')),
|
||||
('date_updated', models.DateTimeField(blank=True, editable=False, verbose_name='Date updated')),
|
||||
|
@ -56,9 +56,9 @@ class Migration(migrations.Migration):
|
|||
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=128, verbose_name='Name')),
|
||||
('username', 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')),
|
||||
('password', common.fields.model.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password')),
|
||||
('private_key', common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH private key')),
|
||||
('public_key', common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH public key')),
|
||||
('password', common.db.fields.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password')),
|
||||
('private_key', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH private key')),
|
||||
('public_key', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH public key')),
|
||||
('comment', models.TextField(blank=True, verbose_name='Comment')),
|
||||
('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date created')),
|
||||
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
|
||||
|
|
|
@ -8,6 +8,7 @@ from rest_framework.generics import CreateAPIView
|
|||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
from rbac.permissions import RBACPermission
|
||||
from common.drf.filters import BaseFilterSet
|
||||
from common.mixins import RecordViewLogMixin
|
||||
from common.permissions import NeedMFAVerify
|
||||
from ..tasks.account_connectivity import test_accounts_connectivity_manual
|
||||
from ..models import AuthBook, Node
|
||||
|
@ -79,7 +80,7 @@ class AccountViewSet(OrgBulkModelViewSet):
|
|||
return Response(data={'task': task.id})
|
||||
|
||||
|
||||
class AccountSecretsViewSet(AccountViewSet):
|
||||
class AccountSecretsViewSet(RecordViewLogMixin, AccountViewSet):
|
||||
"""
|
||||
因为可能要导出所有账号,所以单独建立了一个 viewset
|
||||
"""
|
||||
|
|
|
@ -4,7 +4,6 @@ from rest_framework.response import Response
|
|||
from rest_framework.decorators import action
|
||||
|
||||
from common.utils import get_logger, get_object_or_none
|
||||
from common.utils.crypto import get_aes_crypto
|
||||
from common.permissions import IsValidUser
|
||||
from common.mixins.api import SuggestionMixin
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
|
@ -102,27 +101,17 @@ class SystemUserTempAuthInfoApi(generics.CreateAPIView):
|
|||
permission_classes = (IsValidUser,)
|
||||
serializer_class = SystemUserTempAuthSerializer
|
||||
|
||||
def decrypt_data_if_need(self, data):
|
||||
csrf_token = self.request.META.get('CSRF_COOKIE')
|
||||
aes = get_aes_crypto(csrf_token, 'ECB')
|
||||
password = data.get('password', '')
|
||||
try:
|
||||
data['password'] = aes.decrypt(password)
|
||||
except:
|
||||
pass
|
||||
return data
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
serializer = super().get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
pk = kwargs.get('pk')
|
||||
data = self.decrypt_data_if_need(serializer.validated_data)
|
||||
instance_id = data.get('instance_id')
|
||||
data = serializer.validated_data
|
||||
asset_or_app_id = data.get('instance_id')
|
||||
|
||||
with tmp_to_root_org():
|
||||
instance = get_object_or_404(SystemUser, pk=pk)
|
||||
instance.set_temp_auth(instance_id, self.request.user.id, data)
|
||||
instance.set_temp_auth(asset_or_app_id, self.request.user.id, data)
|
||||
return Response(serializer.data, status=201)
|
||||
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# Generated by Django 2.1.7 on 2019-06-24 13:08
|
||||
|
||||
import assets.models.utils
|
||||
import common.fields.model
|
||||
import common.db.fields
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
|
@ -15,61 +15,61 @@ class Migration(migrations.Migration):
|
|||
migrations.AlterField(
|
||||
model_name='adminuser',
|
||||
name='_password',
|
||||
field=common.fields.model.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password'),
|
||||
field=common.db.fields.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='adminuser',
|
||||
name='_private_key',
|
||||
field=common.fields.model.EncryptTextField(blank=True, null=True, validators=[assets.models.utils.private_key_validator], verbose_name='SSH private key'),
|
||||
field=common.db.fields.EncryptTextField(blank=True, null=True, validators=[assets.models.utils.private_key_validator], verbose_name='SSH private key'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='adminuser',
|
||||
name='_public_key',
|
||||
field=common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH public key'),
|
||||
field=common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH public key'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='authbook',
|
||||
name='_password',
|
||||
field=common.fields.model.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password'),
|
||||
field=common.db.fields.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='authbook',
|
||||
name='_private_key',
|
||||
field=common.fields.model.EncryptTextField(blank=True, null=True, validators=[assets.models.utils.private_key_validator], verbose_name='SSH private key'),
|
||||
field=common.db.fields.EncryptTextField(blank=True, null=True, validators=[assets.models.utils.private_key_validator], verbose_name='SSH private key'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='authbook',
|
||||
name='_public_key',
|
||||
field=common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH public key'),
|
||||
field=common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH public key'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='gateway',
|
||||
name='_password',
|
||||
field=common.fields.model.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password'),
|
||||
field=common.db.fields.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='gateway',
|
||||
name='_private_key',
|
||||
field=common.fields.model.EncryptTextField(blank=True, null=True, validators=[assets.models.utils.private_key_validator], verbose_name='SSH private key'),
|
||||
field=common.db.fields.EncryptTextField(blank=True, null=True, validators=[assets.models.utils.private_key_validator], verbose_name='SSH private key'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='gateway',
|
||||
name='_public_key',
|
||||
field=common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH public key'),
|
||||
field=common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH public key'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='systemuser',
|
||||
name='_password',
|
||||
field=common.fields.model.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password'),
|
||||
field=common.db.fields.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='systemuser',
|
||||
name='_private_key',
|
||||
field=common.fields.model.EncryptTextField(blank=True, null=True, validators=[assets.models.utils.private_key_validator], verbose_name='SSH private key'),
|
||||
field=common.db.fields.EncryptTextField(blank=True, null=True, validators=[assets.models.utils.private_key_validator], verbose_name='SSH private key'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='systemuser',
|
||||
name='_public_key',
|
||||
field=common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH public key'),
|
||||
field=common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH public key'),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# Generated by Django 2.1.7 on 2019-07-11 12:18
|
||||
|
||||
import common.fields.model
|
||||
import common.db.fields
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
|
@ -14,21 +14,21 @@ class Migration(migrations.Migration):
|
|||
migrations.AlterField(
|
||||
model_name='adminuser',
|
||||
name='private_key',
|
||||
field=common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH private key'),
|
||||
field=common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH private key'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='authbook',
|
||||
name='private_key',
|
||||
field=common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH private key'),
|
||||
field=common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH private key'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='gateway',
|
||||
name='private_key',
|
||||
field=common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH private key'),
|
||||
field=common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH private key'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='systemuser',
|
||||
name='private_key',
|
||||
field=common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH private key'),
|
||||
field=common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH private key'),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# Generated by Django 2.2.7 on 2019-12-06 07:26
|
||||
|
||||
import common.fields.model
|
||||
import common.db.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
|
@ -36,7 +36,7 @@ class Migration(migrations.Migration):
|
|||
('name', models.SlugField(allow_unicode=True, unique=True, verbose_name='Name')),
|
||||
('base', models.CharField(choices=[('Linux', 'Linux'), ('Unix', 'Unix'), ('MacOS', 'MacOS'), ('BSD', 'BSD'), ('Windows', 'Windows'), ('Other', 'Other')], default='Linux', max_length=16, verbose_name='Base')),
|
||||
('charset', models.CharField(choices=[('utf8', 'UTF-8'), ('gbk', 'GBK')], default='utf8', max_length=8, verbose_name='Charset')),
|
||||
('meta', common.fields.model.JsonDictTextField(blank=True, null=True, verbose_name='Meta')),
|
||||
('meta', common.db.fields.JsonDictTextField(blank=True, null=True, verbose_name='Meta')),
|
||||
('internal', models.BooleanField(default=False, verbose_name='Internal')),
|
||||
('comment', models.TextField(blank=True, null=True, verbose_name='Comment')),
|
||||
],
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# Generated by Django 3.1.6 on 2021-06-05 16:10
|
||||
|
||||
import common.fields.model
|
||||
import common.db.fields
|
||||
from django.conf import settings
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
|
@ -58,9 +58,9 @@ class Migration(migrations.Migration):
|
|||
('id', models.UUIDField(db_index=True, default=uuid.uuid4)),
|
||||
('name', models.CharField(max_length=128, verbose_name='Name')),
|
||||
('username', 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')),
|
||||
('password', common.fields.model.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password')),
|
||||
('private_key', common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH private key')),
|
||||
('public_key', common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH public key')),
|
||||
('password', common.db.fields.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password')),
|
||||
('private_key', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH private key')),
|
||||
('public_key', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH public key')),
|
||||
('comment', models.TextField(blank=True, verbose_name='Comment')),
|
||||
('date_created', models.DateTimeField(blank=True, editable=False, verbose_name='Date created')),
|
||||
('date_updated', models.DateTimeField(blank=True, editable=False, verbose_name='Date updated')),
|
||||
|
|
|
@ -3,6 +3,20 @@
|
|||
from django.db import migrations, models
|
||||
|
||||
|
||||
def create_internal_platform(apps, schema_editor):
|
||||
model = apps.get_model("assets", "Platform")
|
||||
db_alias = schema_editor.connection.alias
|
||||
type_platforms = (
|
||||
('AIX', 'Unix', None),
|
||||
)
|
||||
for name, base, meta in type_platforms:
|
||||
defaults = {'name': name, 'base': base, 'meta': meta, 'internal': True}
|
||||
model.objects.using(db_alias).update_or_create(
|
||||
name=name, defaults=defaults
|
||||
)
|
||||
migrations.RunPython(create_internal_platform)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
|
@ -15,4 +29,5 @@ class Migration(migrations.Migration):
|
|||
name='number',
|
||||
field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Asset number'),
|
||||
),
|
||||
migrations.RunPython(create_internal_platform)
|
||||
]
|
||||
|
|
|
@ -11,7 +11,7 @@ from django.db import models
|
|||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
from common.fields.model import JsonDictTextField
|
||||
from common.db.fields import JsonDictTextField
|
||||
from common.utils import lazyproperty
|
||||
from orgs.mixins.models import OrgModelMixin, OrgManager
|
||||
|
||||
|
@ -301,7 +301,7 @@ class Asset(AbsConnectivity, AbsHardwareInfo, ProtocolsMixin, NodesRelationMixin
|
|||
'private_key': auth_user.private_key_file
|
||||
}
|
||||
|
||||
if not with_become:
|
||||
if not with_become or self.is_windows():
|
||||
return info
|
||||
|
||||
if become_user:
|
||||
|
|
|
@ -19,7 +19,7 @@ from common.utils import (
|
|||
)
|
||||
from common.utils.encode import ssh_pubkey_gen
|
||||
from common.validators import alphanumeric
|
||||
from common import fields
|
||||
from common.db import fields
|
||||
from orgs.mixins.models import OrgModelMixin
|
||||
|
||||
|
||||
|
|
|
@ -6,12 +6,13 @@ from django.utils.translation import ugettext_lazy as _
|
|||
from rest_framework import serializers
|
||||
|
||||
from common.utils import ssh_pubkey_gen, ssh_private_key_gen, validate_ssh_private_key
|
||||
from common.drf.fields import EncryptedField
|
||||
from assets.models import Type
|
||||
|
||||
|
||||
class AuthSerializer(serializers.ModelSerializer):
|
||||
password = serializers.CharField(required=False, allow_blank=True, allow_null=True, max_length=1024)
|
||||
private_key = serializers.CharField(required=False, allow_blank=True, allow_null=True, max_length=4096)
|
||||
password = EncryptedField(required=False, allow_blank=True, allow_null=True, max_length=1024)
|
||||
private_key = EncryptedField(required=False, allow_blank=True, allow_null=True, max_length=4096)
|
||||
|
||||
def gen_keys(self, private_key=None, password=None):
|
||||
if private_key is None:
|
||||
|
@ -31,6 +32,8 @@ class AuthSerializer(serializers.ModelSerializer):
|
|||
|
||||
|
||||
class AuthSerializerMixin(serializers.ModelSerializer):
|
||||
password = EncryptedField(required=False, allow_blank=True, allow_null=True, max_length=1024)
|
||||
private_key = EncryptedField(required=False, allow_blank=True, allow_null=True, max_length=4096)
|
||||
passphrase = serializers.CharField(
|
||||
allow_blank=True, allow_null=True, required=False, max_length=512,
|
||||
write_only=True, label=_('Key password')
|
||||
|
|
|
@ -32,9 +32,9 @@ 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):
|
||||
def get_push_unixlike_system_user_tasks(system_user, username=None, **kwargs):
|
||||
comment = system_user.name
|
||||
|
||||
algorithm = kwargs.get('algorithm')
|
||||
if username is None:
|
||||
username = system_user.username
|
||||
|
||||
|
@ -104,7 +104,7 @@ def get_push_unixlike_system_user_tasks(system_user, username=None):
|
|||
'module': 'user',
|
||||
'args': 'name={} shell={} state=present password={}'.format(
|
||||
username, system_user.shell,
|
||||
encrypt_password(password, salt="K3mIlKK"),
|
||||
encrypt_password(password, salt="K3mIlKK", algorithm=algorithm),
|
||||
),
|
||||
}
|
||||
})
|
||||
|
@ -138,7 +138,7 @@ def get_push_unixlike_system_user_tasks(system_user, username=None):
|
|||
return tasks
|
||||
|
||||
|
||||
def get_push_windows_system_user_tasks(system_user: SystemUser, username=None):
|
||||
def get_push_windows_system_user_tasks(system_user: SystemUser, username=None, **kwargs):
|
||||
if username is None:
|
||||
username = system_user.username
|
||||
password = system_user.password
|
||||
|
@ -176,7 +176,7 @@ def get_push_windows_system_user_tasks(system_user: SystemUser, username=None):
|
|||
return tasks
|
||||
|
||||
|
||||
def get_push_system_user_tasks(system_user, platform="unixlike", username=None):
|
||||
def get_push_system_user_tasks(system_user, platform="unixlike", username=None, algorithm=None):
|
||||
"""
|
||||
获取推送系统用户的 ansible 命令,跟资产无关
|
||||
:param system_user:
|
||||
|
@ -190,16 +190,16 @@ def get_push_system_user_tasks(system_user, platform="unixlike", username=None):
|
|||
}
|
||||
get_tasks = get_task_map.get(platform, get_push_unixlike_system_user_tasks)
|
||||
if not system_user.username_same_with_user:
|
||||
return get_tasks(system_user)
|
||||
return get_tasks(system_user, algorithm=algorithm)
|
||||
tasks = []
|
||||
# 仅推送这个username
|
||||
if username is not None:
|
||||
tasks.extend(get_tasks(system_user, username))
|
||||
tasks.extend(get_tasks(system_user, username, algorithm=algorithm))
|
||||
return tasks
|
||||
users = system_user.users.all().values_list('username', flat=True)
|
||||
print(_("System user is dynamic: {}").format(list(users)))
|
||||
for _username in users:
|
||||
tasks.extend(get_tasks(system_user, _username))
|
||||
tasks.extend(get_tasks(system_user, _username, algorithm=algorithm))
|
||||
return tasks
|
||||
|
||||
|
||||
|
@ -244,7 +244,11 @@ def push_system_user_util(system_user, assets, task_name, username=None):
|
|||
for u in usernames:
|
||||
for a in _assets:
|
||||
system_user.load_asset_special_auth(a, u)
|
||||
tasks = get_push_system_user_tasks(system_user, platform, username=u)
|
||||
algorithm = 'des' if a.platform.name == 'AIX' else 'sha512'
|
||||
tasks = get_push_system_user_tasks(
|
||||
system_user, platform, username=u,
|
||||
algorithm=algorithm
|
||||
)
|
||||
run_task(tasks, [a])
|
||||
|
||||
|
||||
|
|
|
@ -3,3 +3,23 @@
|
|||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
DEFAULT_CITY = _("Unknown")
|
||||
|
||||
MODELS_NEED_RECORD = (
|
||||
# users
|
||||
'User', 'UserGroup',
|
||||
# acls
|
||||
'LoginACL', 'LoginAssetACL', 'LoginConfirmSetting',
|
||||
# assets
|
||||
'Asset', 'Node', 'AdminUser', 'SystemUser', 'Domain', 'Gateway', 'CommandFilterRule',
|
||||
'CommandFilter', 'Platform', 'AuthBook',
|
||||
# applications
|
||||
'Application',
|
||||
# orgs
|
||||
'Organization',
|
||||
# settings
|
||||
'Setting',
|
||||
# perms
|
||||
'AssetPermission', 'ApplicationPermission',
|
||||
# xpack
|
||||
'License', 'Account', 'SyncInstanceTask', 'ChangeAuthPlan', 'GatherUserTask',
|
||||
)
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 3.1.14 on 2022-05-05 11:02
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('audits', '0013_auto_20211130_1037'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='operatelog',
|
||||
name='action',
|
||||
field=models.CharField(choices=[('create', 'Create'), ('view', 'View'), ('update', 'Update'), ('delete', 'Delete')], max_length=16, verbose_name='Action'),
|
||||
),
|
||||
]
|
|
@ -49,10 +49,12 @@ class FTPLog(OrgModelMixin):
|
|||
|
||||
class OperateLog(OrgModelMixin):
|
||||
ACTION_CREATE = 'create'
|
||||
ACTION_VIEW = 'view'
|
||||
ACTION_UPDATE = 'update'
|
||||
ACTION_DELETE = 'delete'
|
||||
ACTION_CHOICES = (
|
||||
(ACTION_CREATE, _("Create")),
|
||||
(ACTION_VIEW, _("View")),
|
||||
(ACTION_UPDATE, _("Update")),
|
||||
(ACTION_DELETE, _("Delete"))
|
||||
)
|
||||
|
|
|
@ -21,7 +21,7 @@ from jumpserver.utils import current_request
|
|||
from users.models import User
|
||||
from users.signals import post_user_change_password
|
||||
from terminal.models import Session, Command
|
||||
from .utils import write_login_log
|
||||
from .utils import write_login_log, create_operate_log
|
||||
from . import models, serializers
|
||||
from .models import OperateLog
|
||||
from orgs.utils import current_org
|
||||
|
@ -36,26 +36,6 @@ logger = get_logger(__name__)
|
|||
sys_logger = get_syslogger(__name__)
|
||||
json_render = JSONRenderer()
|
||||
|
||||
MODELS_NEED_RECORD = (
|
||||
# users
|
||||
'User', 'UserGroup',
|
||||
# acls
|
||||
'LoginACL', 'LoginAssetACL', 'LoginConfirmSetting',
|
||||
# assets
|
||||
'Asset', 'Node', 'AdminUser', 'SystemUser', 'Domain', 'Gateway', 'CommandFilterRule',
|
||||
'CommandFilter', 'Platform', 'AuthBook',
|
||||
# applications
|
||||
'Application',
|
||||
# orgs
|
||||
'Organization',
|
||||
# settings
|
||||
'Setting',
|
||||
# perms
|
||||
'AssetPermission', 'ApplicationPermission',
|
||||
# xpack
|
||||
'License', 'Account', 'SyncInstanceTask', 'ChangeAuthPlan', 'GatherUserTask',
|
||||
)
|
||||
|
||||
|
||||
class AuthBackendLabelMapping(LazyObject):
|
||||
@staticmethod
|
||||
|
@ -80,28 +60,6 @@ class AuthBackendLabelMapping(LazyObject):
|
|||
AUTH_BACKEND_LABEL_MAPPING = AuthBackendLabelMapping()
|
||||
|
||||
|
||||
def create_operate_log(action, sender, resource):
|
||||
user = current_request.user if current_request else None
|
||||
if not user or not user.is_authenticated:
|
||||
return
|
||||
model_name = sender._meta.object_name
|
||||
if model_name not in MODELS_NEED_RECORD:
|
||||
return
|
||||
with translation.override('en'):
|
||||
resource_type = sender._meta.verbose_name
|
||||
remote_addr = get_request_ip(current_request)
|
||||
|
||||
data = {
|
||||
"user": str(user), 'action': action, 'resource_type': resource_type,
|
||||
'resource': str(resource), 'remote_addr': remote_addr,
|
||||
}
|
||||
with transaction.atomic():
|
||||
try:
|
||||
models.OperateLog.objects.create(**data)
|
||||
except Exception as e:
|
||||
logger.error("Create operate log error: {}".format(e))
|
||||
|
||||
|
||||
M2M_NEED_RECORD = {
|
||||
User.groups.through._meta.object_name: (
|
||||
_('User and Group'),
|
||||
|
|
|
@ -1,9 +1,17 @@
|
|||
import csv
|
||||
import codecs
|
||||
from django.http import HttpResponse
|
||||
|
||||
from .const import DEFAULT_CITY
|
||||
from common.utils import validate_ip, get_ip_city
|
||||
from django.http import HttpResponse
|
||||
from django.db import transaction
|
||||
from django.utils import translation
|
||||
|
||||
from audits.models import OperateLog
|
||||
from common.utils import validate_ip, get_ip_city, get_request_ip, get_logger
|
||||
from jumpserver.utils import current_request
|
||||
from .const import DEFAULT_CITY, MODELS_NEED_RECORD
|
||||
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
def get_excel_response(filename):
|
||||
|
@ -36,3 +44,25 @@ def write_login_log(*args, **kwargs):
|
|||
city = get_ip_city(ip) or DEFAULT_CITY
|
||||
kwargs.update({'ip': ip, 'city': city})
|
||||
UserLoginLog.objects.create(**kwargs)
|
||||
|
||||
|
||||
def create_operate_log(action, sender, resource):
|
||||
user = current_request.user if current_request else None
|
||||
if not user or not user.is_authenticated:
|
||||
return
|
||||
model_name = sender._meta.object_name
|
||||
if model_name not in MODELS_NEED_RECORD:
|
||||
return
|
||||
with translation.override('en'):
|
||||
resource_type = sender._meta.verbose_name
|
||||
remote_addr = get_request_ip(current_request)
|
||||
|
||||
data = {
|
||||
"user": str(user), 'action': action, 'resource_type': resource_type,
|
||||
'resource': str(resource), 'remote_addr': remote_addr,
|
||||
}
|
||||
with transaction.atomic():
|
||||
try:
|
||||
OperateLog.objects.create(**data)
|
||||
except Exception as e:
|
||||
logger.error("Create operate log error: {}".format(e))
|
||||
|
|
|
@ -7,7 +7,6 @@ import os
|
|||
import base64
|
||||
import ctypes
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.http import HttpResponse
|
||||
|
@ -33,11 +32,11 @@ from perms.utils.asset.permission import get_asset_actions
|
|||
from common.const.http import PATCH
|
||||
from terminal.models import EndpointRule
|
||||
from ..serializers import (
|
||||
ConnectionTokenSerializer, ConnectionTokenSecretSerializer,
|
||||
ConnectionTokenSerializer, ConnectionTokenSecretSerializer, SuperConnectionTokenSerializer
|
||||
)
|
||||
|
||||
logger = get_logger(__name__)
|
||||
__all__ = ['UserConnectionTokenViewSet', 'TokenCacheMixin']
|
||||
__all__ = ['UserConnectionTokenViewSet', 'UserSuperConnectionTokenViewSet', 'TokenCacheMixin']
|
||||
|
||||
|
||||
class ClientProtocolMixin:
|
||||
|
@ -70,8 +69,7 @@ class ClientProtocolMixin:
|
|||
system_user = serializer.validated_data['system_user']
|
||||
|
||||
user = serializer.validated_data.get('user')
|
||||
if not user or not self.request.user.is_superuser:
|
||||
user = self.request.user
|
||||
user = user if user else self.request.user
|
||||
return asset, application, system_user, user
|
||||
|
||||
@staticmethod
|
||||
|
@ -105,7 +103,7 @@ class ClientProtocolMixin:
|
|||
'bookmarktype:i': '3',
|
||||
'use redirection server name:i': '0',
|
||||
'smart sizing:i': '1',
|
||||
#'drivestoredirect:s': '*',
|
||||
# 'drivestoredirect:s': '*',
|
||||
# 'domain:s': ''
|
||||
# 'alternate shell:s:': '||MySQLWorkbench',
|
||||
# 'remoteapplicationname:s': 'Firefox',
|
||||
|
@ -206,21 +204,6 @@ class ClientProtocolMixin:
|
|||
rst = rst.decode('ascii')
|
||||
return rst
|
||||
|
||||
@action(methods=['POST', 'GET'], detail=False, url_path='rdp/file')
|
||||
def get_rdp_file(self, request, *args, **kwargs):
|
||||
if self.request.method == 'GET':
|
||||
data = self.request.query_params
|
||||
else:
|
||||
data = self.request.data
|
||||
serializer = self.get_serializer(data=data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
name, data = self.get_rdp_file_content(serializer)
|
||||
response = HttpResponse(data, content_type='application/octet-stream')
|
||||
filename = "{}-{}-jumpserver.rdp".format(self.request.user.username, name)
|
||||
filename = urllib.parse.quote(filename)
|
||||
response['Content-Disposition'] = 'attachment; filename*=UTF-8\'\'%s' % filename
|
||||
return response
|
||||
|
||||
def get_valid_serializer(self):
|
||||
if self.request.method == 'GET':
|
||||
data = self.request.query_params
|
||||
|
@ -252,6 +235,21 @@ class ClientProtocolMixin:
|
|||
}
|
||||
return data
|
||||
|
||||
@action(methods=['POST', 'GET'], detail=False, url_path='rdp/file')
|
||||
def get_rdp_file(self, request, *args, **kwargs):
|
||||
if self.request.method == 'GET':
|
||||
data = self.request.query_params
|
||||
else:
|
||||
data = self.request.data
|
||||
serializer = self.get_serializer(data=data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
name, data = self.get_rdp_file_content(serializer)
|
||||
response = HttpResponse(data, content_type='application/octet-stream')
|
||||
filename = "{}-{}-jumpserver.rdp".format(self.request.user.username, name)
|
||||
filename = urllib.parse.quote(filename)
|
||||
response['Content-Disposition'] = 'attachment; filename*=UTF-8\'\'%s' % filename
|
||||
return response
|
||||
|
||||
@action(methods=['POST', 'GET'], detail=False, url_path='client-url')
|
||||
def get_client_protocol_url(self, request, *args, **kwargs):
|
||||
serializer = self.get_valid_serializer()
|
||||
|
@ -370,7 +368,7 @@ class TokenCacheMixin:
|
|||
key = self.get_token_cache_key(token)
|
||||
return cache.ttl(key)
|
||||
|
||||
def set_token_to_cache(self, token, value, ttl=5*60):
|
||||
def set_token_to_cache(self, token, value, ttl=5 * 60):
|
||||
key = self.get_token_cache_key(token)
|
||||
cache.set(key, value, timeout=ttl)
|
||||
|
||||
|
@ -379,7 +377,7 @@ class TokenCacheMixin:
|
|||
value = cache.get(key, None)
|
||||
return value
|
||||
|
||||
def renewal_token(self, token, ttl=5*60):
|
||||
def renewal_token(self, token, ttl=5 * 60):
|
||||
value = self.get_token_from_cache(token)
|
||||
if value:
|
||||
pre_ttl = self.get_token_ttl(token)
|
||||
|
@ -397,22 +395,10 @@ class TokenCacheMixin:
|
|||
return data
|
||||
|
||||
|
||||
class UserConnectionTokenViewSet(
|
||||
class BaseUserConnectionTokenViewSet(
|
||||
RootOrgViewMixin, SerializerMixin, ClientProtocolMixin,
|
||||
SecretDetailMixin, TokenCacheMixin, GenericViewSet
|
||||
TokenCacheMixin, GenericViewSet
|
||||
):
|
||||
serializer_classes = {
|
||||
'default': ConnectionTokenSerializer,
|
||||
'get_secret_detail': ConnectionTokenSecretSerializer,
|
||||
}
|
||||
rbac_perms = {
|
||||
'GET': 'authentication.view_connectiontoken',
|
||||
'create': 'authentication.add_connectiontoken',
|
||||
'renewal': 'authentication.add_superconnectiontoken',
|
||||
'get_secret_detail': 'authentication.view_connectiontokensecret',
|
||||
'get_rdp_file': 'authentication.add_connectiontoken',
|
||||
'get_client_protocol_url': 'authentication.add_connectiontoken',
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def check_resource_permission(user, asset, application, system_user):
|
||||
|
@ -429,22 +415,7 @@ class UserConnectionTokenViewSet(
|
|||
raise PermissionDenied(error)
|
||||
return True
|
||||
|
||||
@action(methods=[PATCH], detail=False)
|
||||
def renewal(self, request, *args, **kwargs):
|
||||
""" 续期 Token """
|
||||
perm_required = 'authentication.add_superconnectiontoken'
|
||||
if not request.user.has_perm(perm_required):
|
||||
raise PermissionDenied('No permissions for authentication.add_superconnectiontoken')
|
||||
token = request.data.get('token', '')
|
||||
data = self.renewal_token(token)
|
||||
status_code = 200 if data.get('ok') else 404
|
||||
return Response(data=data, status=status_code)
|
||||
|
||||
def create_token(self, user, asset, application, system_user, ttl=5*60):
|
||||
# 再次强调一下权限
|
||||
perm_required = 'authentication.add_superconnectiontoken'
|
||||
if user != self.request.user and not self.request.user.has_perm(perm_required):
|
||||
raise PermissionDenied('Only can create user token')
|
||||
def create_token(self, user, asset, application, system_user, ttl=5 * 60):
|
||||
self.check_resource_permission(user, asset, application, system_user)
|
||||
token = random_string(36)
|
||||
secret = random_string(16)
|
||||
|
@ -489,6 +460,20 @@ class UserConnectionTokenViewSet(
|
|||
}
|
||||
return Response(data, status=201)
|
||||
|
||||
|
||||
class UserConnectionTokenViewSet(BaseUserConnectionTokenViewSet, SecretDetailMixin):
|
||||
serializer_classes = {
|
||||
'default': ConnectionTokenSerializer,
|
||||
'get_secret_detail': ConnectionTokenSecretSerializer,
|
||||
}
|
||||
rbac_perms = {
|
||||
'GET': 'authentication.view_connectiontoken',
|
||||
'create': 'authentication.add_connectiontoken',
|
||||
'get_secret_detail': 'authentication.view_connectiontokensecret',
|
||||
'get_rdp_file': 'authentication.add_connectiontoken',
|
||||
'get_client_protocol_url': 'authentication.add_connectiontoken',
|
||||
}
|
||||
|
||||
def valid_token(self, token):
|
||||
from users.models import User
|
||||
from assets.models import SystemUser, Asset
|
||||
|
@ -526,3 +511,23 @@ class UserConnectionTokenViewSet(
|
|||
if not value:
|
||||
return Response('', status=404)
|
||||
return Response(value)
|
||||
|
||||
|
||||
class UserSuperConnectionTokenViewSet(
|
||||
BaseUserConnectionTokenViewSet, TokenCacheMixin, GenericViewSet
|
||||
):
|
||||
serializer_classes = {
|
||||
'default': SuperConnectionTokenSerializer,
|
||||
}
|
||||
rbac_perms = {
|
||||
'create': 'authentication.add_superconnectiontoken',
|
||||
'renewal': 'authentication.add_superconnectiontoken'
|
||||
}
|
||||
|
||||
@action(methods=[PATCH], detail=False)
|
||||
def renewal(self, request, *args, **kwargs):
|
||||
""" 续期 Token """
|
||||
token = request.data.get('token', '')
|
||||
data = self.renewal_token(token)
|
||||
status_code = 200 if data.get('ok') else 404
|
||||
return Response(data=data, status=status_code)
|
||||
|
|
|
@ -27,8 +27,10 @@ class TokenCreateApi(AuthMixin, CreateAPIView):
|
|||
def create(self, request, *args, **kwargs):
|
||||
self.create_session_if_need()
|
||||
# 如果认证没有过,检查账号密码
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
try:
|
||||
user = self.check_user_auth_if_need()
|
||||
user = self.get_user_or_auth(serializer.validated_data)
|
||||
self.check_user_mfa_if_need(user)
|
||||
self.check_user_login_confirm_if_need(user)
|
||||
self.send_auth_signal(success=True, user=user)
|
||||
|
|
|
@ -103,21 +103,44 @@ class OIDCAuthCodeBackend(OIDCBaseBackend):
|
|||
# Prepares the token payload that will be used to request an authentication token to the
|
||||
# token endpoint of the OIDC provider.
|
||||
logger.debug(log_prompt.format('Prepares token payload'))
|
||||
"""
|
||||
The reason for need not client_id and client_secret in token_payload.
|
||||
OIDC protocol indicate client's token_endpoint_auth_method only accept one type in
|
||||
- client_secret_basic
|
||||
- client_secret_post
|
||||
- client_secret_jwt
|
||||
- private_key_jwt
|
||||
- none
|
||||
If the client offer more than one auth method type to OIDC, OIDC will auth client failed.
|
||||
OIDC default use client_secret_basic,
|
||||
this type only need in headers add Authorization=Basic xxx.
|
||||
|
||||
More info see: https://github.com/jumpserver/jumpserver/issues/8165
|
||||
More info see: https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication
|
||||
"""
|
||||
token_payload = {
|
||||
'client_id': settings.AUTH_OPENID_CLIENT_ID,
|
||||
'client_secret': settings.AUTH_OPENID_CLIENT_SECRET,
|
||||
'grant_type': 'authorization_code',
|
||||
'code': code,
|
||||
'redirect_uri': build_absolute_uri(
|
||||
request, path=reverse(settings.AUTH_OPENID_AUTH_LOGIN_CALLBACK_URL_NAME)
|
||||
)
|
||||
}
|
||||
|
||||
# Prepares the token headers that will be used to request an authentication token to the
|
||||
# token endpoint of the OIDC provider.
|
||||
logger.debug(log_prompt.format('Prepares token headers'))
|
||||
basic_token = "{}:{}".format(settings.AUTH_OPENID_CLIENT_ID, settings.AUTH_OPENID_CLIENT_SECRET)
|
||||
headers = {"Authorization": "Basic {}".format(base64.b64encode(basic_token.encode()).decode())}
|
||||
if settings.AUTH_OPENID_CLIENT_AUTH_METHOD == 'client_secret_post':
|
||||
token_payload.update({
|
||||
'client_id': settings.AUTH_OPENID_CLIENT_ID,
|
||||
'client_secret': settings.AUTH_OPENID_CLIENT_SECRET,
|
||||
})
|
||||
headers = None
|
||||
else:
|
||||
# Prepares the token headers that will be used to request an authentication token to the
|
||||
# token endpoint of the OIDC provider.
|
||||
logger.debug(log_prompt.format('Prepares token headers'))
|
||||
basic_token = "{}:{}".format(
|
||||
settings.AUTH_OPENID_CLIENT_ID, settings.AUTH_OPENID_CLIENT_SECRET
|
||||
)
|
||||
headers = {
|
||||
"Authorization": "Basic {}".format(base64.b64encode(basic_token.encode()).decode())
|
||||
}
|
||||
|
||||
# Calls the token endpoint.
|
||||
logger.debug(log_prompt.format('Call the token endpoint'))
|
||||
|
|
|
@ -74,27 +74,37 @@ class PrepareRequestMixin:
|
|||
return idp_settings
|
||||
|
||||
@staticmethod
|
||||
def get_attribute_consuming_service():
|
||||
attr_mapping = settings.SAML2_RENAME_ATTRIBUTES
|
||||
if attr_mapping and isinstance(attr_mapping, dict):
|
||||
attr_list = [
|
||||
{
|
||||
"name": sp_key,
|
||||
"friendlyName": idp_key, "isRequired": True
|
||||
}
|
||||
for idp_key, sp_key in attr_mapping.items()
|
||||
]
|
||||
request_attribute_template = {
|
||||
"attributeConsumingService": {
|
||||
"isDefault": False,
|
||||
"serviceName": "JumpServer",
|
||||
"serviceDescription": "JumpServer",
|
||||
"requestedAttributes": attr_list
|
||||
}
|
||||
def get_request_attributes():
|
||||
attr_mapping = settings.SAML2_RENAME_ATTRIBUTES or {}
|
||||
attr_map_reverse = {v: k for k, v in attr_mapping.items()}
|
||||
need_attrs = (
|
||||
('username', 'username', True),
|
||||
('email', 'email', True),
|
||||
('name', 'name', False),
|
||||
('phone', 'phone', False),
|
||||
('comment', 'comment', False),
|
||||
)
|
||||
attr_list = []
|
||||
for name, friend_name, is_required in need_attrs:
|
||||
rename_name = attr_map_reverse.get(friend_name)
|
||||
name = rename_name if rename_name else name
|
||||
attr_list.append({
|
||||
"name": name, "isRequired": is_required,
|
||||
"friendlyName": friend_name,
|
||||
})
|
||||
return attr_list
|
||||
|
||||
def get_attribute_consuming_service(self):
|
||||
attr_list = self.get_request_attributes()
|
||||
request_attribute_template = {
|
||||
"attributeConsumingService": {
|
||||
"isDefault": False,
|
||||
"serviceName": "JumpServer",
|
||||
"serviceDescription": "JumpServer",
|
||||
"requestedAttributes": attr_list
|
||||
}
|
||||
return request_attribute_template
|
||||
else:
|
||||
return {}
|
||||
}
|
||||
return request_attribute_template
|
||||
|
||||
@staticmethod
|
||||
def get_advanced_settings():
|
||||
|
@ -167,11 +177,14 @@ class PrepareRequestMixin:
|
|||
|
||||
def get_attributes(self, saml_instance):
|
||||
user_attrs = {}
|
||||
attr_mapping = settings.SAML2_RENAME_ATTRIBUTES
|
||||
attrs = saml_instance.get_attributes()
|
||||
valid_attrs = ['username', 'name', 'email', 'comment', 'phone']
|
||||
|
||||
for attr, value in attrs.items():
|
||||
attr = attr.rsplit('/', 1)[-1]
|
||||
if attr_mapping and attr_mapping.get(attr):
|
||||
attr = attr_mapping.get(attr)
|
||||
if attr not in valid_attrs:
|
||||
continue
|
||||
user_attrs[attr] = self.value_to_str(value)
|
||||
|
|
|
@ -1,15 +1,25 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from captcha.fields import CaptchaField, CaptchaTextInput
|
||||
|
||||
from common.utils import get_logger, decrypt_password
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class EncryptedField(forms.CharField):
|
||||
def to_python(self, value):
|
||||
value = super().to_python(value)
|
||||
return decrypt_password(value)
|
||||
|
||||
|
||||
class UserLoginForm(forms.Form):
|
||||
days_auto_login = int(settings.SESSION_COOKIE_AGE / 3600 / 24)
|
||||
disable_days_auto_login = settings.SESSION_EXPIRE_AT_BROWSER_CLOSE_FORCE or days_auto_login < 1
|
||||
disable_days_auto_login = settings.SESSION_EXPIRE_AT_BROWSER_CLOSE_FORCE \
|
||||
or days_auto_login < 1
|
||||
|
||||
username = forms.CharField(
|
||||
label=_('Username'), max_length=100,
|
||||
|
@ -18,7 +28,7 @@ class UserLoginForm(forms.Form):
|
|||
'autofocus': 'autofocus'
|
||||
})
|
||||
)
|
||||
password = forms.CharField(
|
||||
password = EncryptedField(
|
||||
label=_('Password'), widget=forms.PasswordInput,
|
||||
max_length=1024, strip=False
|
||||
)
|
||||
|
|
|
@ -1,8 +1,12 @@
|
|||
import base64
|
||||
|
||||
from django.shortcuts import redirect, reverse
|
||||
from django.utils.deprecation import MiddlewareMixin
|
||||
from django.http import HttpResponse
|
||||
from django.conf import settings
|
||||
|
||||
from common.utils import gen_key_pair
|
||||
|
||||
|
||||
class MFAMiddleware:
|
||||
"""
|
||||
|
@ -48,3 +52,28 @@ class SessionCookieMiddleware(MiddlewareMixin):
|
|||
return response
|
||||
response.set_cookie(key, value)
|
||||
return response
|
||||
|
||||
|
||||
class EncryptedMiddleware:
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
|
||||
@staticmethod
|
||||
def check_key_pair(request, response):
|
||||
pub_key_name = settings.SESSION_RSA_PUBLIC_KEY_NAME
|
||||
public_key = request.session.get(pub_key_name)
|
||||
cookie_key = request.COOKIES.get(pub_key_name)
|
||||
if public_key and public_key == cookie_key:
|
||||
return
|
||||
|
||||
pri_key_name = settings.SESSION_RSA_PRIVATE_KEY_NAME
|
||||
private_key, public_key = gen_key_pair()
|
||||
public_key_decode = base64.b64encode(public_key.encode()).decode()
|
||||
request.session[pub_key_name] = public_key_decode
|
||||
request.session[pri_key_name] = private_key
|
||||
response.set_cookie(pub_key_name, public_key_decode)
|
||||
|
||||
def __call__(self, request):
|
||||
response = self.get_response(request)
|
||||
self.check_key_pair(request, response)
|
||||
return response
|
||||
|
|
|
@ -23,9 +23,7 @@ from acls.models import LoginACL
|
|||
from users.models import User
|
||||
from users.utils import LoginBlockUtil, MFABlockUtils, LoginIpBlockUtil
|
||||
from . import errors
|
||||
from .utils import rsa_decrypt, gen_key_pair
|
||||
from .signals import post_auth_success, post_auth_failed
|
||||
from .const import RSA_PRIVATE_KEY, RSA_PUBLIC_KEY
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
@ -58,6 +56,7 @@ def authenticate(request=None, **credentials):
|
|||
|
||||
for backend, backend_path in _get_backends(return_tuples=True):
|
||||
# 检查用户名是否允许认证 (预先检查,不浪费认证时间)
|
||||
logger.info('Try using auth backend: {}'.format(str(backend)))
|
||||
if not backend.username_allow_authenticate(username):
|
||||
continue
|
||||
|
||||
|
@ -91,46 +90,8 @@ def authenticate(request=None, **credentials):
|
|||
auth.authenticate = authenticate
|
||||
|
||||
|
||||
class PasswordEncryptionViewMixin:
|
||||
request = None
|
||||
|
||||
def get_decrypted_password(self, password=None, username=None):
|
||||
request = self.request
|
||||
if hasattr(request, 'data'):
|
||||
data = request.data
|
||||
else:
|
||||
data = request.POST
|
||||
|
||||
username = username or data.get('username')
|
||||
password = password or data.get('password')
|
||||
|
||||
password = self.decrypt_passwd(password)
|
||||
if not password:
|
||||
self.raise_password_decrypt_failed(username=username)
|
||||
return password
|
||||
|
||||
def raise_password_decrypt_failed(self, username):
|
||||
ip = self.get_request_ip()
|
||||
raise errors.CredentialError(
|
||||
error=errors.reason_password_decrypt_failed,
|
||||
username=username, ip=ip, request=self.request
|
||||
)
|
||||
|
||||
def decrypt_passwd(self, raw_passwd):
|
||||
# 获取解密密钥,对密码进行解密
|
||||
rsa_private_key = self.request.session.get(RSA_PRIVATE_KEY)
|
||||
if rsa_private_key is None:
|
||||
return raw_passwd
|
||||
|
||||
try:
|
||||
return rsa_decrypt(raw_passwd, rsa_private_key)
|
||||
except Exception as e:
|
||||
logger.error(e, exc_info=True)
|
||||
logger.error(
|
||||
f'Decrypt password failed: password[{raw_passwd}] '
|
||||
f'rsa_private_key[{rsa_private_key}]'
|
||||
)
|
||||
return None
|
||||
class CommonMixin:
|
||||
request: Request
|
||||
|
||||
def get_request_ip(self):
|
||||
ip = ''
|
||||
|
@ -139,26 +100,6 @@ class PasswordEncryptionViewMixin:
|
|||
ip = ip or get_request_ip(self.request)
|
||||
return ip
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
# 生成加解密密钥对,public_key传递给前端,private_key存入session中供解密使用
|
||||
rsa_public_key = self.request.session.get(RSA_PUBLIC_KEY)
|
||||
rsa_private_key = self.request.session.get(RSA_PRIVATE_KEY)
|
||||
if not all([rsa_private_key, rsa_public_key]):
|
||||
rsa_private_key, rsa_public_key = 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
|
||||
|
||||
kwargs.update({
|
||||
'rsa_public_key': rsa_public_key,
|
||||
})
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class CommonMixin(PasswordEncryptionViewMixin):
|
||||
request: Request
|
||||
get_request_ip: Callable
|
||||
|
||||
def raise_credential_error(self, error):
|
||||
raise self.partial_credential_error(error=error)
|
||||
|
||||
|
@ -193,20 +134,13 @@ class CommonMixin(PasswordEncryptionViewMixin):
|
|||
user.backend = self.request.session.get("auth_backend")
|
||||
return user
|
||||
|
||||
def get_auth_data(self, decrypt_passwd=False):
|
||||
def get_auth_data(self, data):
|
||||
request = self.request
|
||||
if hasattr(request, 'data'):
|
||||
data = request.data
|
||||
else:
|
||||
data = request.POST
|
||||
|
||||
items = ['username', 'password', 'challenge', 'public_key', 'auto_login']
|
||||
username, password, challenge, public_key, auto_login = bulk_get(data, items, default='')
|
||||
ip = self.get_request_ip()
|
||||
self._set_partial_credential_error(username=username, ip=ip, request=request)
|
||||
|
||||
if decrypt_passwd:
|
||||
password = self.get_decrypted_password()
|
||||
password = password + challenge.strip()
|
||||
return username, password, public_key, ip, auto_login
|
||||
|
||||
|
@ -482,10 +416,10 @@ class AuthMixin(CommonMixin, AuthPreCheckMixin, AuthACLMixin, MFAMixin, AuthPost
|
|||
need = cache.get(self.key_prefix_captcha.format(ip))
|
||||
return need
|
||||
|
||||
def check_user_auth(self, decrypt_passwd=False):
|
||||
def check_user_auth(self, valid_data=None):
|
||||
# pre check
|
||||
self.check_is_block()
|
||||
username, password, public_key, ip, auto_login = self.get_auth_data(decrypt_passwd)
|
||||
username, password, public_key, ip, auto_login = self.get_auth_data(valid_data)
|
||||
self._check_only_allow_exists_user_auth(username)
|
||||
|
||||
# check auth
|
||||
|
@ -537,11 +471,12 @@ class AuthMixin(CommonMixin, AuthPreCheckMixin, AuthACLMixin, MFAMixin, AuthPost
|
|||
self.mark_password_ok(user, False)
|
||||
return user
|
||||
|
||||
def check_user_auth_if_need(self, decrypt_passwd=False):
|
||||
def get_user_or_auth(self, valid_data):
|
||||
request = self.request
|
||||
if not request.session.get('auth_password'):
|
||||
return self.check_user_auth(decrypt_passwd=decrypt_passwd)
|
||||
return self.get_user_from_session()
|
||||
if request.session.get('auth_password'):
|
||||
return self.get_user_from_session()
|
||||
else:
|
||||
return self.check_user_auth(valid_data)
|
||||
|
||||
def clear_auth_mark(self):
|
||||
keys = ['auth_password', 'user_id', 'auth_confirm', 'auth_ticket_id']
|
||||
|
|
|
@ -13,24 +13,16 @@ __all__ = [
|
|||
'ConnectionTokenUserSerializer', 'ConnectionTokenFilterRuleSerializer',
|
||||
'ConnectionTokenAssetSerializer', 'ConnectionTokenSystemUserSerializer',
|
||||
'ConnectionTokenDomainSerializer', 'ConnectionTokenRemoteAppSerializer',
|
||||
'ConnectionTokenGatewaySerializer', 'ConnectionTokenSecretSerializer'
|
||||
'ConnectionTokenGatewaySerializer', 'ConnectionTokenSecretSerializer',
|
||||
'SuperConnectionTokenSerializer'
|
||||
]
|
||||
|
||||
|
||||
class ConnectionTokenSerializer(serializers.Serializer):
|
||||
user = serializers.CharField(max_length=128, required=False, allow_blank=True)
|
||||
system_user = serializers.CharField(max_length=128, required=True)
|
||||
asset = serializers.CharField(max_length=128, required=False)
|
||||
application = serializers.CharField(max_length=128, required=False)
|
||||
|
||||
@staticmethod
|
||||
def validate_user(user_id):
|
||||
from users.models import User
|
||||
user = User.objects.filter(id=user_id).first()
|
||||
if user is None:
|
||||
raise serializers.ValidationError('user id not exist')
|
||||
return user
|
||||
|
||||
@staticmethod
|
||||
def validate_system_user(system_user_id):
|
||||
from assets.models import SystemUser
|
||||
|
@ -65,6 +57,18 @@ class ConnectionTokenSerializer(serializers.Serializer):
|
|||
return super().validate(attrs)
|
||||
|
||||
|
||||
class SuperConnectionTokenSerializer(ConnectionTokenSerializer):
|
||||
user = serializers.CharField(max_length=128, required=False, allow_blank=True)
|
||||
|
||||
@staticmethod
|
||||
def validate_user(user_id):
|
||||
from users.models import User
|
||||
user = User.objects.filter(id=user_id).first()
|
||||
if user is None:
|
||||
raise serializers.ValidationError('user id not exist')
|
||||
return user
|
||||
|
||||
|
||||
class ConnectionTokenUserSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = User
|
||||
|
@ -114,7 +118,6 @@ class ConnectionTokenDomainSerializer(serializers.ModelSerializer):
|
|||
|
||||
|
||||
class ConnectionTokenFilterRuleSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = CommandFilterRule
|
||||
fields = [
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
#
|
||||
from rest_framework import serializers
|
||||
|
||||
from common.drf.fields import EncryptedField
|
||||
|
||||
|
||||
__all__ = [
|
||||
'OtpVerifySerializer', 'MFAChallengeSerializer', 'MFASelectTypeSerializer',
|
||||
|
@ -10,7 +12,7 @@ __all__ = [
|
|||
|
||||
|
||||
class PasswordVerifySerializer(serializers.Serializer):
|
||||
password = serializers.CharField()
|
||||
password = EncryptedField()
|
||||
|
||||
|
||||
class MFASelectTypeSerializer(serializers.Serializer):
|
||||
|
|
|
@ -161,6 +161,7 @@
|
|||
<span style="font-size: 21px;font-weight:400;color: #151515;letter-spacing: 0;">{{ JMS_TITLE }}</span>
|
||||
</div>
|
||||
<div class="contact-form col-md-10 col-md-offset-1">
|
||||
|
||||
<form id="login-form" action="" method="post" role="form" novalidate="novalidate">
|
||||
{% csrf_token %}
|
||||
<div style="line-height: 17px;margin-bottom: 20px;color: #999999;">
|
||||
|
@ -240,21 +241,13 @@
|
|||
</body>
|
||||
{% include '_foot_js.html' %}
|
||||
<script type="text/javascript" src="/static/js/plugins/jsencrypt/jsencrypt.min.js"></script>
|
||||
<script type="text/javascript" src="/static/js/plugins/cryptojs/crypto-js.min.js"></script>
|
||||
<script type="text/javascript" src="/static/js/plugins/buffer/buffer.min.js"></script>
|
||||
<script>
|
||||
function encryptLoginPassword(password, rsaPublicKey) {
|
||||
if (!password) {
|
||||
return ''
|
||||
}
|
||||
var jsencrypt = new JSEncrypt(); //加密对象
|
||||
jsencrypt.setPublicKey(rsaPublicKey); // 设置密钥
|
||||
return jsencrypt.encrypt(password); //加密
|
||||
}
|
||||
|
||||
function doLogin() {
|
||||
//公钥加密
|
||||
var rsaPublicKey = "{{ rsa_public_key }}"
|
||||
var password = $('#password').val(); //明文密码
|
||||
var passwordEncrypted = encryptLoginPassword(password, rsaPublicKey)
|
||||
var passwordEncrypted = encryptPassword(password)
|
||||
$('#password-hidden').val(passwordEncrypted); //返回给密码输入input
|
||||
$('#login-form').submit(); //post提交
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ router.register('access-keys', api.AccessKeyViewSet, 'access-key')
|
|||
router.register('sso', api.SSOViewSet, 'sso')
|
||||
router.register('temp-tokens', api.TempTokenViewSet, 'temp-token')
|
||||
router.register('connection-token', api.UserConnectionTokenViewSet, 'connection-token')
|
||||
router.register('super-connection-token', api.UserSuperConnectionTokenViewSet, 'super-connection-token')
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
|
|
|
@ -1,62 +1,22 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
import base64
|
||||
from Cryptodome.PublicKey import RSA
|
||||
from Cryptodome.Cipher import PKCS1_v1_5
|
||||
from Cryptodome import Random
|
||||
|
||||
from django.conf import settings
|
||||
from .notifications import DifferentCityLoginMessage
|
||||
from audits.models import UserLoginLog
|
||||
from audits.const import DEFAULT_CITY
|
||||
|
||||
from common.utils import validate_ip, get_ip_city, get_request_ip
|
||||
from common.utils import get_logger
|
||||
from audits.models import UserLoginLog
|
||||
from audits.const import DEFAULT_CITY
|
||||
from .notifications import DifferentCityLoginMessage
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
def gen_key_pair():
|
||||
""" 生成加密key
|
||||
用于登录页面提交用户名/密码时,对密码进行加密(前端)/解密(后端)
|
||||
"""
|
||||
random_generator = Random.new().read
|
||||
rsa = RSA.generate(1024, random_generator)
|
||||
rsa_private_key = rsa.exportKey().decode()
|
||||
rsa_public_key = rsa.publickey().exportKey().decode()
|
||||
return rsa_private_key, rsa_public_key
|
||||
|
||||
|
||||
def rsa_encrypt(message, rsa_public_key):
|
||||
""" 加密登录密码 """
|
||||
key = RSA.importKey(rsa_public_key)
|
||||
cipher = PKCS1_v1_5.new(key)
|
||||
cipher_text = base64.b64encode(cipher.encrypt(message.encode())).decode()
|
||||
return cipher_text
|
||||
|
||||
|
||||
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)
|
||||
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_different_city_login_if_need(user, request):
|
||||
if not settings.SECURITY_CHECK_DIFFERENT_CITY_LOGIN:
|
||||
return
|
||||
|
||||
ip = get_request_ip(request) or '0.0.0.0'
|
||||
|
||||
if not (ip and validate_ip(ip)):
|
||||
city = DEFAULT_CITY
|
||||
else:
|
||||
|
|
|
@ -21,6 +21,7 @@ from authentication.mixins import AuthMixin
|
|||
from common.sdk.im.dingtalk import DingTalk
|
||||
from common.utils.common import get_request_ip
|
||||
from authentication.notifications import OAuthBindMessage
|
||||
from .mixins import METAMixin
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
@ -200,14 +201,17 @@ class DingTalkEnableStartView(UserVerifyPasswordView):
|
|||
return success_url
|
||||
|
||||
|
||||
class DingTalkQRLoginView(DingTalkQRMixin, View):
|
||||
class DingTalkQRLoginView(DingTalkQRMixin, METAMixin, View):
|
||||
permission_classes = (AllowAny,)
|
||||
|
||||
def get(self, request: HttpRequest):
|
||||
redirect_url = request.GET.get('redirect_url')
|
||||
|
||||
redirect_uri = reverse('authentication:dingtalk-qr-login-callback', external=True)
|
||||
redirect_uri += '?' + urlencode({'redirect_url': redirect_url})
|
||||
redirect_uri += '?' + urlencode({
|
||||
'redirect_url': redirect_url,
|
||||
'next': self.get_next_url_from_meta()
|
||||
})
|
||||
|
||||
url = self.get_qr_url(redirect_uri)
|
||||
return HttpResponseRedirect(url)
|
||||
|
@ -305,4 +309,4 @@ class DingTalkOAuthLoginCallbackView(AuthMixin, DingTalkOAuthMixin, View):
|
|||
response = self.get_failed_response(login_url, title=msg, msg=msg)
|
||||
return response
|
||||
|
||||
return self.redirect_to_guard_view()
|
||||
return self.redirect_to_guard_view()
|
||||
|
|
|
@ -96,7 +96,7 @@ class UserLoginView(mixins.AuthMixin, FormView):
|
|||
self.request.session.delete_test_cookie()
|
||||
|
||||
try:
|
||||
self.check_user_auth(decrypt_passwd=True)
|
||||
self.check_user_auth(form.cleaned_data)
|
||||
except errors.AuthFailedError as e:
|
||||
form.add_error(None, e.msg)
|
||||
self.set_login_failed_mark()
|
||||
|
@ -219,7 +219,7 @@ class UserLoginGuardView(mixins.AuthMixin, RedirectView):
|
|||
|
||||
def get_redirect_url(self, *args, **kwargs):
|
||||
try:
|
||||
user = self.check_user_auth_if_need()
|
||||
user = self.get_user_from_session()
|
||||
self.check_user_mfa_if_need(user)
|
||||
self.check_user_login_confirm_if_need(user)
|
||||
except (errors.CredentialError, errors.SessionEmptyError) as e:
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
class METAMixin:
|
||||
def get_next_url_from_meta(self):
|
||||
request_meta = self.request.META or {}
|
||||
next_url = None
|
||||
referer = request_meta.get('HTTP_REFERER', '')
|
||||
next_url_item = referer.rsplit('next=', 1)
|
||||
if len(next_url_item) > 1:
|
||||
next_url = next_url_item[-1]
|
||||
return next_url
|
|
@ -21,6 +21,7 @@ from common.utils.common import get_request_ip
|
|||
from authentication import errors
|
||||
from authentication.mixins import AuthMixin
|
||||
from authentication.notifications import OAuthBindMessage
|
||||
from .mixins import METAMixin
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
@ -196,14 +197,17 @@ class WeComEnableStartView(UserVerifyPasswordView):
|
|||
return success_url
|
||||
|
||||
|
||||
class WeComQRLoginView(WeComQRMixin, View):
|
||||
class WeComQRLoginView(WeComQRMixin, METAMixin, View):
|
||||
permission_classes = (AllowAny,)
|
||||
|
||||
def get(self, request: HttpRequest):
|
||||
redirect_url = request.GET.get('redirect_url')
|
||||
|
||||
redirect_uri = reverse('authentication:wecom-qr-login-callback', external=True)
|
||||
redirect_uri += '?' + urlencode({'redirect_url': redirect_url})
|
||||
redirect_uri += '?' + urlencode({
|
||||
'redirect_url': redirect_url,
|
||||
'next': self.get_next_url_from_meta()
|
||||
})
|
||||
|
||||
url = self.get_qr_url(redirect_uri)
|
||||
return HttpResponseRedirect(url)
|
||||
|
@ -301,4 +305,4 @@ class WeComOAuthLoginCallbackView(AuthMixin, WeComOAuthMixin, View):
|
|||
response = self.get_failed_response(login_url, title=msg, msg=msg)
|
||||
return response
|
||||
|
||||
return self.redirect_to_guard_view()
|
||||
return self.redirect_to_guard_view()
|
||||
|
|
|
@ -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 django.core.validators import MinValueValidator, MaxValueValidator
|
||||
from ..utils import signer, crypto
|
||||
from common.utils import signer, crypto
|
||||
|
||||
|
||||
__all__ = [
|
|
@ -3,9 +3,10 @@
|
|||
|
||||
from rest_framework import serializers
|
||||
|
||||
from common.utils import decrypt_password
|
||||
|
||||
__all__ = [
|
||||
'ReadableHiddenField',
|
||||
'ReadableHiddenField', 'EncryptedField'
|
||||
]
|
||||
|
||||
|
||||
|
@ -23,3 +24,9 @@ class ReadableHiddenField(serializers.HiddenField):
|
|||
if hasattr(value, 'id'):
|
||||
return getattr(value, 'id')
|
||||
return value
|
||||
|
||||
|
||||
class EncryptedField(serializers.CharField):
|
||||
def to_internal_value(self, value):
|
||||
value = super().to_internal_value(value)
|
||||
return decrypt_password(value)
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from .model import *
|
|
@ -1,5 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.contrib.auth.mixins import UserPassesTestMixin
|
||||
from rest_framework import permissions
|
||||
from rest_framework.decorators import action
|
||||
|
@ -7,8 +8,10 @@ from rest_framework.request import Request
|
|||
from rest_framework.response import Response
|
||||
|
||||
from common.permissions import IsValidUser
|
||||
from audits.utils import create_operate_log
|
||||
from audits.models import OperateLog
|
||||
|
||||
__all__ = ["PermissionsMixin"]
|
||||
__all__ = ["PermissionsMixin", "RecordViewLogMixin"]
|
||||
|
||||
|
||||
class PermissionsMixin(UserPassesTestMixin):
|
||||
|
@ -24,3 +27,35 @@ class PermissionsMixin(UserPassesTestMixin):
|
|||
if not permission_class().has_permission(self.request, self):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
class RecordViewLogMixin:
|
||||
ACTION = OperateLog.ACTION_VIEW
|
||||
|
||||
@staticmethod
|
||||
def get_resource_display(request):
|
||||
query_params = dict(request.query_params)
|
||||
if query_params.get('format'):
|
||||
query_params.pop('format')
|
||||
spm_filter = query_params.pop('spm') if query_params.get('spm') else None
|
||||
if not query_params and not spm_filter:
|
||||
display_message = _('Export all')
|
||||
elif spm_filter:
|
||||
display_message = _('Export only selected items')
|
||||
else:
|
||||
query = ','.join(
|
||||
['%s=%s' % (key, value) for key, value in query_params.items()]
|
||||
)
|
||||
display_message = _('Export filtered: %s') % query
|
||||
return display_message
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
response = super().list(request, *args, **kwargs)
|
||||
resource = self.get_resource_display(request)
|
||||
create_operate_log(self.ACTION, self.model, resource)
|
||||
return response
|
||||
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
response = super().retrieve(request, *args, **kwargs)
|
||||
create_operate_log(self.ACTION, self.model, self.get_object())
|
||||
return response
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
import base64
|
||||
from Cryptodome.Cipher import AES
|
||||
import logging
|
||||
from Cryptodome.Cipher import AES, PKCS1_v1_5
|
||||
from Cryptodome.Util.Padding import pad
|
||||
from Cryptodome.Random import get_random_bytes
|
||||
from Cryptodome.PublicKey import RSA
|
||||
from Cryptodome import Random
|
||||
from gmssl.sm4 import CryptSM4, SM4_ENCRYPT, SM4_DECRYPT
|
||||
|
||||
from django.conf import settings
|
||||
|
@ -193,4 +196,66 @@ class Crypto:
|
|||
continue
|
||||
|
||||
|
||||
def gen_key_pair(length=1024):
|
||||
""" 生成加密key
|
||||
用于登录页面提交用户名/密码时,对密码进行加密(前端)/解密(后端)
|
||||
"""
|
||||
random_generator = Random.new().read
|
||||
rsa = RSA.generate(length, random_generator)
|
||||
rsa_private_key = rsa.exportKey().decode()
|
||||
rsa_public_key = rsa.publickey().exportKey().decode()
|
||||
return rsa_private_key, rsa_public_key
|
||||
|
||||
|
||||
def rsa_encrypt(message, rsa_public_key):
|
||||
""" 加密登录密码 """
|
||||
key = RSA.importKey(rsa_public_key)
|
||||
cipher = PKCS1_v1_5.new(key)
|
||||
cipher_text = base64.b64encode(cipher.encrypt(message.encode())).decode()
|
||||
return cipher_text
|
||||
|
||||
|
||||
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)
|
||||
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 rsa_decrypt_by_session_pkey(value):
|
||||
from jumpserver.utils import current_request
|
||||
private_key_name = settings.SESSION_RSA_PRIVATE_KEY_NAME
|
||||
private_key = current_request.session.get(private_key_name)
|
||||
|
||||
if not private_key or not value:
|
||||
return value
|
||||
|
||||
try:
|
||||
value = rsa_decrypt(value, private_key)
|
||||
except Exception as e:
|
||||
logging.error('Decrypt field error: {}'.format(e))
|
||||
return value
|
||||
|
||||
|
||||
def decrypt_password(value):
|
||||
cipher = value.split(':')
|
||||
if len(cipher) != 2:
|
||||
return value
|
||||
key_cipher, password_cipher = cipher
|
||||
aes_key = rsa_decrypt_by_session_pkey(key_cipher)
|
||||
aes = get_aes_crypto(aes_key, 'ECB')
|
||||
password = aes.decrypt(password_cipher)
|
||||
return password
|
||||
|
||||
|
||||
crypto = Crypto()
|
||||
|
|
|
@ -186,10 +186,27 @@ def make_signature(access_key_secret, date=None):
|
|||
return content_md5(data)
|
||||
|
||||
|
||||
def encrypt_password(password, salt=None):
|
||||
from passlib.hash import sha512_crypt
|
||||
if password:
|
||||
def encrypt_password(password, salt=None, algorithm='sha512'):
|
||||
from passlib.hash import sha512_crypt, des_crypt
|
||||
|
||||
def sha512():
|
||||
return sha512_crypt.using(rounds=5000).hash(password, salt=salt)
|
||||
|
||||
def des():
|
||||
return des_crypt.hash(password, salt=salt[:2])
|
||||
|
||||
support_algorithm = {
|
||||
'sha512': sha512, 'des': des
|
||||
}
|
||||
|
||||
if isinstance(algorithm, str):
|
||||
algorithm = algorithm.lower()
|
||||
|
||||
if algorithm not in support_algorithm.keys():
|
||||
algorithm = 'sha512'
|
||||
|
||||
if password and support_algorithm[algorithm]:
|
||||
return support_algorithm[algorithm]()
|
||||
return None
|
||||
|
||||
|
||||
|
|
|
@ -32,6 +32,10 @@ def local_now_display(fmt='%Y-%m-%d %H:%M:%S'):
|
|||
return local_now().strftime(fmt)
|
||||
|
||||
|
||||
def local_now_date_display(fmt='%Y-%m-%d'):
|
||||
return local_now().strftime(fmt)
|
||||
|
||||
|
||||
_rest_dt_field = DateTimeField()
|
||||
dt_parser = _rest_dt_field.to_internal_value
|
||||
dt_formatter = _rest_dt_field.to_representation
|
||||
|
|
|
@ -187,6 +187,8 @@ class Config(dict):
|
|||
'BASE_SITE_URL': None,
|
||||
'AUTH_OPENID_CLIENT_ID': 'client-id',
|
||||
'AUTH_OPENID_CLIENT_SECRET': 'client-secret',
|
||||
# https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication
|
||||
'AUTH_OPENID_CLIENT_AUTH_METHOD': 'client_secret_basic',
|
||||
'AUTH_OPENID_SHARE_SESSION': True,
|
||||
'AUTH_OPENID_IGNORE_SSL_VERIFICATION': True,
|
||||
|
||||
|
|
|
@ -55,6 +55,7 @@ AUTH_OPENID = CONFIG.AUTH_OPENID
|
|||
BASE_SITE_URL = CONFIG.BASE_SITE_URL
|
||||
AUTH_OPENID_CLIENT_ID = CONFIG.AUTH_OPENID_CLIENT_ID
|
||||
AUTH_OPENID_CLIENT_SECRET = CONFIG.AUTH_OPENID_CLIENT_SECRET
|
||||
AUTH_OPENID_CLIENT_AUTH_METHOD = CONFIG.AUTH_OPENID_CLIENT_AUTH_METHOD
|
||||
AUTH_OPENID_PROVIDER_ENDPOINT = CONFIG.AUTH_OPENID_PROVIDER_ENDPOINT
|
||||
AUTH_OPENID_PROVIDER_AUTHORIZATION_ENDPOINT = CONFIG.AUTH_OPENID_PROVIDER_AUTHORIZATION_ENDPOINT
|
||||
AUTH_OPENID_PROVIDER_TOKEN_ENDPOINT = CONFIG.AUTH_OPENID_PROVIDER_TOKEN_ENDPOINT
|
||||
|
|
|
@ -95,6 +95,7 @@ MIDDLEWARE = [
|
|||
'authentication.backends.cas.middleware.CASMiddleware',
|
||||
'authentication.middleware.MFAMiddleware',
|
||||
'authentication.middleware.SessionCookieMiddleware',
|
||||
'authentication.middleware.EncryptedMiddleware',
|
||||
'simple_history.middleware.HistoryRequestMiddleware',
|
||||
]
|
||||
|
||||
|
|
|
@ -169,3 +169,6 @@ ANNOUNCEMENT = CONFIG.ANNOUNCEMENT
|
|||
# help
|
||||
HELP_DOCUMENT_URL = CONFIG.HELP_DOCUMENT_URL
|
||||
HELP_SUPPORT_URL = CONFIG.HELP_SUPPORT_URL
|
||||
|
||||
SESSION_RSA_PRIVATE_KEY_NAME = 'jms_private_key'
|
||||
SESSION_RSA_PUBLIC_KEY_NAME = 'jms_public_key'
|
||||
|
|
|
@ -31,6 +31,7 @@ api_v1 = [
|
|||
app_view_patterns = [
|
||||
path('auth/', include('authentication.urls.view_urls'), name='auth'),
|
||||
path('ops/', include('ops.urls.view_urls'), name='ops'),
|
||||
path('tickets/', include('tickets.urls.view_urls'), name='tickets'),
|
||||
path('common/', include('common.urls.view_urls'), name='common'),
|
||||
re_path(r'flower/(?P<path>.*)', views.celery_flower_view, name='flower-view'),
|
||||
path('download/', views.ResourceDownload.as_view(), name='download'),
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f2c88ade4bfae213bdcdafad656af73f764e3b1b3f2b0c59aa39626e967730ca
|
||||
size 125911
|
||||
oid sha256:90a70c14fd3b546cb1ef6a96da4cd7a2acde947128bbb773527ed1845510511c
|
||||
size 127420
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c75e0a1f2a047dac1374916c630bc0e8ef5ad5eea7518ffc21e93f747fc1235e
|
||||
size 104165
|
||||
oid sha256:f181a41eb4dd8a30a576f7903e5c9f519da2042e5e095ac27146d7b4002ba3df
|
||||
size 105303
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,6 +1,6 @@
|
|||
# Generated by Django 2.2.7 on 2019-12-17 09:58
|
||||
|
||||
import common.fields.model
|
||||
import common.db.fields
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
|
@ -18,17 +18,17 @@ class Migration(migrations.Migration):
|
|||
migrations.AlterField(
|
||||
model_name='adhoc',
|
||||
name='_become',
|
||||
field=common.fields.model.EncryptJsonDictCharField(blank=True, null=True, default='', max_length=1024, verbose_name='Become'),
|
||||
field=common.db.fields.EncryptJsonDictCharField(blank=True, null=True, default='', max_length=1024, verbose_name='Become'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='adhoc',
|
||||
name='_options',
|
||||
field=common.fields.model.JsonDictCharField(default='', max_length=1024, verbose_name='Options'),
|
||||
field=common.db.fields.JsonDictCharField(default='', max_length=1024, verbose_name='Options'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='adhoc',
|
||||
name='_tasks',
|
||||
field=common.fields.model.JsonListTextField(verbose_name='Tasks'),
|
||||
field=common.db.fields.JsonListTextField(verbose_name='Tasks'),
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name='adhoc',
|
||||
|
@ -48,12 +48,12 @@ class Migration(migrations.Migration):
|
|||
migrations.AlterField(
|
||||
model_name='adhocrunhistory',
|
||||
name='_result',
|
||||
field=common.fields.model.JsonDictTextField(blank=True, null=True, verbose_name='Adhoc raw result'),
|
||||
field=common.db.fields.JsonDictTextField(blank=True, null=True, verbose_name='Adhoc raw result'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='adhocrunhistory',
|
||||
name='_summary',
|
||||
field=common.fields.model.JsonDictTextField(blank=True, null=True, verbose_name='Adhoc result summary'),
|
||||
field=common.db.fields.JsonDictTextField(blank=True, null=True, verbose_name='Adhoc result summary'),
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name='adhocrunhistory',
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# Generated by Django 2.2.7 on 2020-01-06 07:34
|
||||
|
||||
import common.fields.model
|
||||
import common.db.fields
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
|
@ -14,6 +14,6 @@ class Migration(migrations.Migration):
|
|||
migrations.AlterField(
|
||||
model_name='adhoc',
|
||||
name='become',
|
||||
field=common.fields.model.EncryptJsonDictCharField(blank=True, default='', max_length=1024, null=True, verbose_name='Become'),
|
||||
field=common.db.fields.EncryptJsonDictCharField(blank=True, default='', max_length=1024, null=True, verbose_name='Become'),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -9,11 +9,11 @@ from celery import current_task
|
|||
from django.db import models
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import ugettext_lazy as _, gettext
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from common.utils import get_logger, lazyproperty
|
||||
from common.utils.translate import translate_value
|
||||
from common.fields.model import (
|
||||
from common.db.fields import (
|
||||
JsonListTextField, JsonDictCharField, EncryptJsonDictCharField,
|
||||
JsonDictTextField,
|
||||
)
|
||||
|
|
|
@ -8,6 +8,7 @@ from ..filters import RoleFilter
|
|||
from ..serializers import RoleSerializer, RoleUserSerializer
|
||||
from ..models import Role, SystemRole, OrgRole
|
||||
from .permission import PermissionViewSet
|
||||
from common.mixins.api import PaginatedResponseMixin
|
||||
|
||||
__all__ = [
|
||||
'RoleViewSet', 'SystemRoleViewSet', 'OrgRoleViewSet',
|
||||
|
@ -15,7 +16,7 @@ __all__ = [
|
|||
]
|
||||
|
||||
|
||||
class RoleViewSet(JMSModelViewSet):
|
||||
class RoleViewSet(PaginatedResponseMixin, JMSModelViewSet):
|
||||
queryset = Role.objects.all()
|
||||
serializer_classes = {
|
||||
'default': RoleSerializer,
|
||||
|
@ -54,7 +55,7 @@ class RoleViewSet(JMSModelViewSet):
|
|||
def users(self, *args, **kwargs):
|
||||
role = self.get_object()
|
||||
queryset = role.users
|
||||
return self.get_paginated_response_with_query_set(queryset)
|
||||
return self.get_paginated_response_from_queryset(queryset)
|
||||
|
||||
|
||||
class SystemRoleViewSet(RoleViewSet):
|
||||
|
|
|
@ -91,7 +91,7 @@ exclude_permissions = (
|
|||
|
||||
|
||||
only_system_permissions = (
|
||||
('assets', 'platform', '*', '*'),
|
||||
('assets', 'platform', 'add,change,delete', 'platform'),
|
||||
('users', 'user', 'delete', 'user'),
|
||||
('rbac', 'role', 'delete,add,change', 'role'),
|
||||
('rbac', 'systemrole', '*', '*'),
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
from django.utils.translation import gettext_lazy as _
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from rest_framework.serializers import ValidationError
|
||||
|
||||
|
@ -111,10 +112,13 @@ class RoleBinding(JMSModel):
|
|||
system_bindings = [b for b in bindings if b.scope == Role.Scope.system.value]
|
||||
# 工作台仅限于自己加入的组织
|
||||
if perm == 'rbac.view_workbench':
|
||||
all_orgs = user.orgs.all()
|
||||
all_orgs = user.orgs.all().distinct()
|
||||
else:
|
||||
all_orgs = Organization.objects.all()
|
||||
|
||||
if not settings.XPACK_ENABLED:
|
||||
all_orgs = all_orgs.filter(id=Organization.DEFAULT_ID)
|
||||
|
||||
# 有系统级别的绑定,就代表在所有组织有这个权限
|
||||
if system_bindings:
|
||||
orgs = all_orgs
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
from rest_framework import generics
|
||||
from rest_framework.permissions import AllowAny
|
||||
from rest_framework.permissions import AllowAny, IsAuthenticated
|
||||
from django.conf import settings
|
||||
|
||||
from jumpserver.utils import has_valid_xpack_license, get_xpack_license_info
|
||||
|
@ -9,10 +9,10 @@ from ..utils import get_interface_setting
|
|||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
__all__ = ['PublicSettingApi']
|
||||
__all__ = ['PublicSettingApi', 'OpenPublicSettingApi']
|
||||
|
||||
|
||||
class PublicSettingApi(generics.RetrieveAPIView):
|
||||
class OpenPublicSettingApi(generics.RetrieveAPIView):
|
||||
permission_classes = (AllowAny,)
|
||||
serializer_class = serializers.PublicSettingSerializer
|
||||
|
||||
|
@ -28,48 +28,40 @@ class PublicSettingApi(generics.RetrieveAPIView):
|
|||
return interface['login_title']
|
||||
|
||||
def get_object(self):
|
||||
instance = {
|
||||
"data": {
|
||||
# Security
|
||||
"WINDOWS_SKIP_ALL_MANUAL_PASSWORD": settings.WINDOWS_SKIP_ALL_MANUAL_PASSWORD,
|
||||
"OLD_PASSWORD_HISTORY_LIMIT_COUNT": settings.OLD_PASSWORD_HISTORY_LIMIT_COUNT,
|
||||
"SECURITY_MAX_IDLE_TIME": settings.SECURITY_MAX_IDLE_TIME,
|
||||
"SECURITY_VIEW_AUTH_NEED_MFA": settings.SECURITY_VIEW_AUTH_NEED_MFA,
|
||||
"SECURITY_MFA_VERIFY_TTL": settings.SECURITY_MFA_VERIFY_TTL,
|
||||
"SECURITY_COMMAND_EXECUTION": settings.SECURITY_COMMAND_EXECUTION,
|
||||
"SECURITY_PASSWORD_EXPIRATION_TIME": settings.SECURITY_PASSWORD_EXPIRATION_TIME,
|
||||
"SECURITY_LUNA_REMEMBER_AUTH": settings.SECURITY_LUNA_REMEMBER_AUTH,
|
||||
"PASSWORD_RULE": {
|
||||
'SECURITY_PASSWORD_MIN_LENGTH': settings.SECURITY_PASSWORD_MIN_LENGTH,
|
||||
'SECURITY_ADMIN_USER_PASSWORD_MIN_LENGTH': settings.SECURITY_ADMIN_USER_PASSWORD_MIN_LENGTH,
|
||||
'SECURITY_PASSWORD_UPPER_CASE': settings.SECURITY_PASSWORD_UPPER_CASE,
|
||||
'SECURITY_PASSWORD_LOWER_CASE': settings.SECURITY_PASSWORD_LOWER_CASE,
|
||||
'SECURITY_PASSWORD_NUMBER': settings.SECURITY_PASSWORD_NUMBER,
|
||||
'SECURITY_PASSWORD_SPECIAL_CHAR': settings.SECURITY_PASSWORD_SPECIAL_CHAR,
|
||||
},
|
||||
'SECURITY_WATERMARK_ENABLED': settings.SECURITY_WATERMARK_ENABLED,
|
||||
'SECURITY_SESSION_SHARE': settings.SECURITY_SESSION_SHARE,
|
||||
# XPACK
|
||||
"XPACK_ENABLED": settings.XPACK_ENABLED,
|
||||
"XPACK_LICENSE_IS_VALID": has_valid_xpack_license(),
|
||||
"XPACK_LICENSE_INFO": get_xpack_license_info(),
|
||||
# Performance
|
||||
"LOGIN_TITLE": self.get_login_title(),
|
||||
"LOGO_URLS": self.get_logo_urls(),
|
||||
"HELP_DOCUMENT_URL": settings.HELP_DOCUMENT_URL,
|
||||
"HELP_SUPPORT_URL": settings.HELP_SUPPORT_URL,
|
||||
# Auth
|
||||
"AUTH_WECOM": settings.AUTH_WECOM,
|
||||
"AUTH_DINGTALK": settings.AUTH_DINGTALK,
|
||||
"AUTH_FEISHU": settings.AUTH_FEISHU,
|
||||
# Terminal
|
||||
"XRDP_ENABLED": settings.XRDP_ENABLED,
|
||||
"TERMINAL_MAGNUS_ENABLED": settings.TERMINAL_MAGNUS_ENABLED,
|
||||
"TERMINAL_KOKO_SSH_ENABLED": settings.TERMINAL_KOKO_SSH_ENABLED,
|
||||
# Announcement
|
||||
"ANNOUNCEMENT_ENABLED": settings.ANNOUNCEMENT_ENABLED,
|
||||
"ANNOUNCEMENT": settings.ANNOUNCEMENT,
|
||||
"AUTH_TEMP_TOKEN": settings.AUTH_TEMP_TOKEN,
|
||||
}
|
||||
return {
|
||||
"XPACK_ENABLED": settings.XPACK_ENABLED,
|
||||
"LOGIN_TITLE": self.get_login_title(),
|
||||
"LOGO_URLS": self.get_logo_urls(),
|
||||
}
|
||||
return instance
|
||||
|
||||
|
||||
class PublicSettingApi(OpenPublicSettingApi):
|
||||
permission_classes = (IsAuthenticated,)
|
||||
serializer_class = serializers.PrivateSettingSerializer
|
||||
|
||||
def get_object(self):
|
||||
values = super().get_object()
|
||||
values.update({
|
||||
"XPACK_LICENSE_IS_VALID": has_valid_xpack_license(),
|
||||
"XPACK_LICENSE_INFO": get_xpack_license_info(),
|
||||
"PASSWORD_RULE": {
|
||||
'SECURITY_PASSWORD_MIN_LENGTH': settings.SECURITY_PASSWORD_MIN_LENGTH,
|
||||
'SECURITY_ADMIN_USER_PASSWORD_MIN_LENGTH': settings.SECURITY_ADMIN_USER_PASSWORD_MIN_LENGTH,
|
||||
'SECURITY_PASSWORD_UPPER_CASE': settings.SECURITY_PASSWORD_UPPER_CASE,
|
||||
'SECURITY_PASSWORD_LOWER_CASE': settings.SECURITY_PASSWORD_LOWER_CASE,
|
||||
'SECURITY_PASSWORD_NUMBER': settings.SECURITY_PASSWORD_NUMBER,
|
||||
'SECURITY_PASSWORD_SPECIAL_CHAR': settings.SECURITY_PASSWORD_SPECIAL_CHAR,
|
||||
},
|
||||
})
|
||||
|
||||
serializer = self.serializer_class()
|
||||
field_names = list(serializer.fields.keys())
|
||||
for name in field_names:
|
||||
if name in values:
|
||||
continue
|
||||
# 提前把异常爆出来
|
||||
values[name] = getattr(settings, name)
|
||||
return values
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from common.drf.fields import EncryptedField
|
||||
|
||||
__all__ = [
|
||||
'LDAPTestConfigSerializer', 'LDAPUserSerializer', 'LDAPTestLoginSerializer',
|
||||
'LDAPSettingSerializer',
|
||||
|
@ -20,7 +22,7 @@ class LDAPTestConfigSerializer(serializers.Serializer):
|
|||
|
||||
class LDAPTestLoginSerializer(serializers.Serializer):
|
||||
username = serializers.CharField(max_length=1024, required=True)
|
||||
password = serializers.CharField(max_length=2014, required=True)
|
||||
password = EncryptedField(max_length=2014, required=True)
|
||||
|
||||
|
||||
class LDAPUserSerializer(serializers.Serializer):
|
||||
|
@ -28,6 +30,7 @@ class LDAPUserSerializer(serializers.Serializer):
|
|||
username = serializers.CharField()
|
||||
name = serializers.CharField()
|
||||
email = serializers.CharField()
|
||||
groups = serializers.ListField(child=serializers.CharField(), default=[])
|
||||
existing = serializers.BooleanField(read_only=True)
|
||||
|
||||
|
||||
|
|
|
@ -17,6 +17,14 @@ class CommonSettingSerializer(serializers.Serializer):
|
|||
AUTH_OPENID_CLIENT_SECRET = serializers.CharField(
|
||||
required=False, max_length=1024, write_only=True, label=_('Client Secret')
|
||||
)
|
||||
AUTH_OPENID_CLIENT_AUTH_METHOD = serializers.ChoiceField(
|
||||
default='client_secret_basic',
|
||||
choices=(
|
||||
('client_secret_basic', 'Client Secret Basic'),
|
||||
('client_secret_post', 'Client Secret Post')
|
||||
),
|
||||
label=_('Client authentication method')
|
||||
)
|
||||
AUTH_OPENID_SHARE_SESSION = serializers.BooleanField(required=False, label=_('Share session'))
|
||||
AUTH_OPENID_IGNORE_SSL_VERIFICATION = serializers.BooleanField(
|
||||
required=False, label=_('Ignore ssl verification')
|
||||
|
|
|
@ -1,10 +1,43 @@
|
|||
# coding: utf-8
|
||||
#
|
||||
#
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
__all__ = ['PublicSettingSerializer']
|
||||
__all__ = ['PublicSettingSerializer', 'PrivateSettingSerializer']
|
||||
|
||||
|
||||
class PublicSettingSerializer(serializers.Serializer):
|
||||
data = serializers.DictField(read_only=True)
|
||||
XPACK_ENABLED = serializers.BooleanField()
|
||||
LOGIN_TITLE = serializers.CharField()
|
||||
LOGO_URLS = serializers.DictField()
|
||||
|
||||
|
||||
class PrivateSettingSerializer(PublicSettingSerializer):
|
||||
WINDOWS_SKIP_ALL_MANUAL_PASSWORD = serializers.BooleanField()
|
||||
OLD_PASSWORD_HISTORY_LIMIT_COUNT = serializers.IntegerField()
|
||||
SECURITY_MAX_IDLE_TIME = serializers.IntegerField()
|
||||
SECURITY_VIEW_AUTH_NEED_MFA = serializers.BooleanField()
|
||||
SECURITY_MFA_VERIFY_TTL = serializers.IntegerField()
|
||||
SECURITY_COMMAND_EXECUTION = serializers.BooleanField()
|
||||
SECURITY_PASSWORD_EXPIRATION_TIME = serializers.IntegerField()
|
||||
SECURITY_LUNA_REMEMBER_AUTH = serializers.BooleanField()
|
||||
SECURITY_WATERMARK_ENABLED = serializers.BooleanField()
|
||||
SESSION_EXPIRE_AT_BROWSER_CLOSE = serializers.BooleanField()
|
||||
PASSWORD_RULE = serializers.DictField()
|
||||
SECURITY_SESSION_SHARE = serializers.BooleanField()
|
||||
XPACK_LICENSE_IS_VALID = serializers.BooleanField()
|
||||
XPACK_LICENSE_INFO = serializers.DictField()
|
||||
HELP_DOCUMENT_URL = serializers.CharField()
|
||||
HELP_SUPPORT_URL = serializers.CharField()
|
||||
|
||||
AUTH_WECOM = serializers.BooleanField()
|
||||
AUTH_DINGTALK = serializers.BooleanField()
|
||||
AUTH_FEISHU = serializers.BooleanField()
|
||||
AUTH_TEMP_TOKEN = serializers.BooleanField()
|
||||
|
||||
XRDP_ENABLED = serializers.BooleanField()
|
||||
TERMINAL_MAGNUS_ENABLED = serializers.BooleanField()
|
||||
TERMINAL_KOKO_SSH_ENABLED = serializers.BooleanField()
|
||||
|
||||
ANNOUNCEMENT_ENABLED = serializers.BooleanField()
|
||||
ANNOUNCEMENT = serializers.CharField()
|
||||
|
|
|
@ -35,4 +35,4 @@ class TerminalSettingSerializer(serializers.Serializer):
|
|||
)
|
||||
TERMINAL_MAGNUS_ENABLED = serializers.BooleanField(label=_("Enable database proxy"))
|
||||
XRDP_ENABLED = serializers.BooleanField(label=_("Enable XRDP"))
|
||||
TERMINAL_KOKO_SSH_ENABLED = serializers.BooleanField(label=_("Enable KoKo SSH"))
|
||||
TERMINAL_KOKO_SSH_ENABLED = serializers.BooleanField(label=_("Enable SSH Client"))
|
||||
|
|
|
@ -22,4 +22,5 @@ urlpatterns = [
|
|||
|
||||
path('setting/', api.SettingsApi.as_view(), name='settings-setting'),
|
||||
path('public/', api.PublicSettingApi.as_view(), name='public-setting'),
|
||||
path('public/open/', api.OpenPublicSettingApi.as_view(), name='open-public-setting'),
|
||||
]
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
# coding: utf-8
|
||||
#
|
||||
|
||||
import os
|
||||
import json
|
||||
from ldap3 import Server, Connection, SIMPLE
|
||||
from ldap3.core.exceptions import (
|
||||
|
@ -21,12 +22,14 @@ from django.conf import settings
|
|||
from django.core.cache import cache
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from copy import deepcopy
|
||||
from collections import defaultdict
|
||||
from orgs.utils import tmp_to_org
|
||||
|
||||
from common.const import LDAP_AD_ACCOUNT_DISABLE
|
||||
from common.utils import timeit, get_logger
|
||||
from common.db.utils import close_old_connections
|
||||
from users.utils import construct_user_email
|
||||
from users.models import User
|
||||
from users.models import User, UserGroup
|
||||
from authentication.backends.ldap import LDAPAuthorizationBackend, LDAPUser
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
@ -185,6 +188,12 @@ class LDAPServerUtil(object):
|
|||
if attr == 'is_active' and mapping.lower() == 'useraccountcontrol' \
|
||||
and value:
|
||||
value = int(value) & LDAP_AD_ACCOUNT_DISABLE != LDAP_AD_ACCOUNT_DISABLE
|
||||
if attr == 'groups' and mapping.lower() == 'memberof':
|
||||
# AD: {'groups': 'memberOf'}
|
||||
if isinstance(value, str) and value:
|
||||
value = [value]
|
||||
if not isinstance(value, list):
|
||||
value = []
|
||||
user[attr] = value.strip() if isinstance(value, str) else value
|
||||
return user
|
||||
|
||||
|
@ -244,10 +253,13 @@ class LDAPCacheUtil(object):
|
|||
if user['username'] in self.search_users
|
||||
]
|
||||
elif self.search_value:
|
||||
filter_users = [
|
||||
user for user in users
|
||||
if self.search_value.lower() in ','.join(user.values()).lower()
|
||||
]
|
||||
filter_users = []
|
||||
for u in users:
|
||||
search_value = self.search_value.lower()
|
||||
user_all_attr_value = [v for v in u.values() if isinstance(v, str)]
|
||||
if search_value not in ','.join(user_all_attr_value).lower():
|
||||
continue
|
||||
filter_users.append(u)
|
||||
else:
|
||||
filter_users = users
|
||||
return filter_users
|
||||
|
@ -345,6 +357,7 @@ class LDAPSyncUtil(object):
|
|||
|
||||
|
||||
class LDAPImportUtil(object):
|
||||
user_group_name_prefix = 'AD '
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
@ -365,23 +378,58 @@ class LDAPImportUtil(object):
|
|||
)
|
||||
return obj, created
|
||||
|
||||
def get_user_group_names(self, groups) -> list:
|
||||
if not isinstance(groups, list):
|
||||
logger.error('Groups type not list')
|
||||
return []
|
||||
group_names = []
|
||||
for group in groups:
|
||||
if not group:
|
||||
continue
|
||||
if not isinstance(group, str):
|
||||
continue
|
||||
# get group name for AD, Such as: CN=Users,CN=Builtin,DC=jms,DC=com
|
||||
group_name = group.split(',')[0].split('=')[-1]
|
||||
group_name = f'{self.user_group_name_prefix}{group_name}'.strip()
|
||||
group_names.append(group_name)
|
||||
return group_names
|
||||
|
||||
def perform_import(self, users, org=None):
|
||||
logger.info('Start perform import ldap users, count: {}'.format(len(users)))
|
||||
errors = []
|
||||
objs = []
|
||||
group_users_mapper = defaultdict(set)
|
||||
for user in users:
|
||||
groups = user.pop('groups', [])
|
||||
try:
|
||||
obj, created = self.update_or_create(user)
|
||||
objs.append(obj)
|
||||
except Exception as e:
|
||||
errors.append({user['username']: str(e)})
|
||||
logger.error(e)
|
||||
continue
|
||||
try:
|
||||
group_names = self.get_user_group_names(groups)
|
||||
for group_name in group_names:
|
||||
group_users_mapper[group_name].add(obj)
|
||||
except Exception as e:
|
||||
errors.append({user['username']: str(e)})
|
||||
logger.error(e)
|
||||
continue
|
||||
if not org:
|
||||
return
|
||||
if org.is_root():
|
||||
return
|
||||
# add user to org
|
||||
for obj in objs:
|
||||
org.add_member(obj)
|
||||
# add user to group
|
||||
with tmp_to_org(org):
|
||||
for group_name, users in group_users_mapper.items():
|
||||
group, created = UserGroup.objects.get_or_create(
|
||||
name=group_name, defaults={'name': group_name}
|
||||
)
|
||||
group.users.add(*users)
|
||||
logger.info('End perform import ldap users')
|
||||
return errors
|
||||
|
||||
|
|
|
@ -1501,3 +1501,90 @@ function getStatusIcon(status, mapping, title) {
|
|||
}
|
||||
return icon;
|
||||
}
|
||||
|
||||
|
||||
function fillKey(key) {
|
||||
let keySize = 128
|
||||
// 如果超过 key 16 位, 最大取 32 位,需要更改填充
|
||||
if (key.length > 16) {
|
||||
key = key.slice(0, 32)
|
||||
keySize = keySize * 2
|
||||
}
|
||||
const filledKeyLength = keySize / 8
|
||||
if (key.length >= filledKeyLength) {
|
||||
return key.slice(0, filledKeyLength)
|
||||
}
|
||||
const filledKey = Buffer.alloc(keySize / 8)
|
||||
const keys = Buffer.from(key)
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
filledKey[i] = keys[i]
|
||||
}
|
||||
return filledKey
|
||||
}
|
||||
|
||||
function aesEncrypt(text, originKey) {
|
||||
const key = CryptoJS.enc.Utf8.parse(fillKey(originKey));
|
||||
return CryptoJS.AES.encrypt(text, key, {
|
||||
mode: CryptoJS.mode.ECB,
|
||||
padding: CryptoJS.pad.ZeroPadding
|
||||
}).toString();
|
||||
}
|
||||
|
||||
function rsaEncrypt(text, pubKey) {
|
||||
if (!text) {
|
||||
return text
|
||||
}
|
||||
const jsEncrypt = new JSEncrypt();
|
||||
jsEncrypt.setPublicKey(pubKey);
|
||||
return jsEncrypt.encrypt(text);
|
||||
}
|
||||
|
||||
function rsaDecrypt(cipher, pkey) {
|
||||
const jsEncrypt = new JSEncrypt();
|
||||
jsEncrypt.setPrivateKey(pkey);
|
||||
return jsEncrypt.decrypt(cipher)
|
||||
}
|
||||
|
||||
|
||||
window.rsaEncrypt = rsaEncrypt
|
||||
window.rsaDecrypt = rsaDecrypt
|
||||
|
||||
function encryptPassword(password) {
|
||||
if (!password) {
|
||||
return ''
|
||||
}
|
||||
const aesKey = (Math.random() + 1).toString(36).substring(2)
|
||||
// public key 是 base64 存储的
|
||||
const rsaPublicKeyText = getCookie('jms_public_key')
|
||||
.replaceAll('"', '')
|
||||
const rsaPublicKey = atob(rsaPublicKeyText)
|
||||
const keyCipher = rsaEncrypt(aesKey, rsaPublicKey)
|
||||
const passwordCipher = aesEncrypt(password, aesKey)
|
||||
return `${keyCipher}:${passwordCipher}`
|
||||
}
|
||||
|
||||
|
||||
function randomString(length) {
|
||||
const characters ='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
let result = '';
|
||||
const charactersLength = characters.length;
|
||||
for ( let i = 0; i < length; i++ ) {
|
||||
result += characters.charAt(Math.floor(Math.random() * charactersLength));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function testEncrypt() {
|
||||
const radio = []
|
||||
const len2 = []
|
||||
for (let i=1;i<4096;i++) {
|
||||
const password = randomString(i)
|
||||
const cipher = encryptPassword(password)
|
||||
len2.push([password.length, cipher.length])
|
||||
radio.push(cipher.length/password.length)
|
||||
}
|
||||
return radio
|
||||
}
|
||||
|
||||
window.encryptPassword = encryptPassword
|
||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -1 +0,0 @@
|
|||
{"version":3,"file":"fit.js","sources":["../../../src/addons/fit/fit.ts","../../../node_modules/browser-pack/_prelude.js"],"sourcesContent":["/**\n * Copyright (c) 2014 The xterm.js authors. All rights reserved.\n * @license MIT\n *\n * Fit terminal columns and rows to the dimensions of its DOM element.\n *\n * ## Approach\n *\n * Rows: Truncate the division of the terminal parent element height by the\n * terminal row height.\n * Columns: Truncate the division of the terminal parent element width by the\n * terminal character width (apply display: inline at the terminal\n * row and truncate its width with the current number of columns).\n */\n\nimport { Terminal } from 'xterm';\n\nexport interface IGeometry {\n rows: number;\n cols: number;\n}\n\nexport function proposeGeometry(term: Terminal): IGeometry {\n if (!term.element.parentElement) {\n return null;\n }\n const parentElementStyle = window.getComputedStyle(term.element.parentElement);\n const parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height'));\n const parentElementWidth = Math.max(0, parseInt(parentElementStyle.getPropertyValue('width')));\n const elementStyle = window.getComputedStyle(term.element);\n const elementPadding = {\n top: parseInt(elementStyle.getPropertyValue('padding-top')),\n bottom: parseInt(elementStyle.getPropertyValue('padding-bottom')),\n right: parseInt(elementStyle.getPropertyValue('padding-right')),\n left: parseInt(elementStyle.getPropertyValue('padding-left'))\n };\n const elementPaddingVer = elementPadding.top + elementPadding.bottom;\n const elementPaddingHor = elementPadding.right + elementPadding.left;\n const availableHeight = parentElementHeight - elementPaddingVer;\n const availableWidth = parentElementWidth - elementPaddingHor - (<any>term)._core.viewport.scrollBarWidth;\n const geometry = {\n cols: Math.floor(availableWidth / (<any>term)._core.renderer.dimensions.actualCellWidth),\n rows: Math.floor(availableHeight / (<any>term)._core.renderer.dimensions.actualCellHeight)\n };\n return geometry;\n}\n\nexport function fit(term: Terminal): void {\n const geometry = proposeGeometry(term);\n if (geometry) {\n // Force a full render\n if (term.rows !== geometry.rows || term.cols !== geometry.cols) {\n (<any>term)._core.renderer.clear();\n term.resize(geometry.cols, geometry.rows);\n }\n }\n}\n\nexport function apply(terminalConstructor: typeof Terminal): void {\n (<any>terminalConstructor.prototype).proposeGeometry = function (): IGeometry {\n return proposeGeometry(this);\n };\n\n (<any>terminalConstructor.prototype).fit = function (): void {\n fit(this);\n };\n}\n",null],"names":[],"mappings":"ACAA;;;ADsBA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAvBA;AAyBA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AATA;AAWA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AARA;"}
|
|
@ -1 +0,0 @@
|
|||
{"version":3,"file":"fullscreen.js","sources":["../../../src/addons/fullscreen/fullscreen.ts","../../../node_modules/browser-pack/_prelude.js"],"sourcesContent":["/**\n * Copyright (c) 2014 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { Terminal } from 'xterm';\n\n/**\n * Toggle the given terminal's fullscreen mode.\n * @param term The terminal to toggle full screen mode\n * @param fullscreen Toggle fullscreen on (true) or off (false)\n */\nexport function toggleFullScreen(term: Terminal, fullscreen: boolean): void {\n let fn: string;\n\n if (typeof fullscreen === 'undefined') {\n fn = (term.element.classList.contains('fullscreen')) ? 'remove' : 'add';\n } else if (!fullscreen) {\n fn = 'remove';\n } else {\n fn = 'add';\n }\n\n term.element.classList[fn]('fullscreen');\n}\n\nexport function apply(terminalConstructor: typeof Terminal): void {\n (<any>terminalConstructor.prototype).toggleFullScreen = function (fullscreen: boolean): void {\n toggleFullScreen(this, fullscreen);\n };\n}\n",null],"names":[],"mappings":"ACAA;;;ADYA;AACA;AAEA;AACA;AACA;AAAA;AACA;AACA;AAAA;AACA;AACA;AAEA;AACA;AAZA;AAcA;AACA;AACA;AACA;AACA;AAJA;"}
|
File diff suppressed because one or more lines are too long
|
@ -1 +0,0 @@
|
|||
{"version":3,"file":"terminado.js","sources":["../../../src/addons/terminado/terminado.ts","../../../node_modules/browser-pack/_prelude.js"],"sourcesContent":["/**\n * Copyright (c) 2016 The xterm.js authors. All rights reserved.\n * @license MIT\n *\n * This module provides methods for attaching a terminal to a terminado\n * WebSocket stream.\n */\n\nimport { Terminal } from 'xterm';\nimport { ITerminadoAddonTerminal } from './Interfaces';\n\n/**\n * Attaches the given terminal to the given socket.\n *\n * @param term The terminal to be attached to the given socket.\n * @param socket The socket to attach the current terminal.\n * @param bidirectional Whether the terminal should send data to the socket as well.\n * @param buffered Whether the rendering of incoming data should happen instantly or at a maximum\n * frequency of 1 rendering per 10ms.\n */\nexport function terminadoAttach(term: Terminal, socket: WebSocket, bidirectional: boolean, buffered: boolean): void {\n const addonTerminal = <ITerminadoAddonTerminal>term;\n bidirectional = (typeof bidirectional === 'undefined') ? true : bidirectional;\n addonTerminal.__socket = socket;\n\n addonTerminal.__flushBuffer = () => {\n addonTerminal.write(addonTerminal.__attachSocketBuffer);\n addonTerminal.__attachSocketBuffer = null;\n };\n\n addonTerminal.__pushToBuffer = (data: string) => {\n if (addonTerminal.__attachSocketBuffer) {\n addonTerminal.__attachSocketBuffer += data;\n } else {\n addonTerminal.__attachSocketBuffer = data;\n setTimeout(addonTerminal.__flushBuffer, 10);\n }\n };\n\n addonTerminal.__getMessage = (ev: MessageEvent) => {\n const data = JSON.parse(ev.data);\n if (data[0] === 'stdout') {\n if (buffered) {\n addonTerminal.__pushToBuffer(data[1]);\n } else {\n addonTerminal.write(data[1]);\n }\n }\n };\n\n addonTerminal.__sendData = (data: string) => {\n socket.send(JSON.stringify(['stdin', data]));\n };\n\n addonTerminal.__setSize = (size: {rows: number, cols: number}) => {\n socket.send(JSON.stringify(['set_size', size.rows, size.cols]));\n };\n\n socket.addEventListener('message', addonTerminal.__getMessage);\n\n if (bidirectional) {\n addonTerminal.on('data', addonTerminal.__sendData);\n }\n addonTerminal.on('resize', addonTerminal.__setSize);\n\n socket.addEventListener('close', () => terminadoDetach(addonTerminal, socket));\n socket.addEventListener('error', () => terminadoDetach(addonTerminal, socket));\n}\n\n/**\n * Detaches the given terminal from the given socket\n *\n * @param term The terminal to be detached from the given socket.\n * @param socket The socket from which to detach the current terminal.\n */\nexport function terminadoDetach(term: Terminal, socket: WebSocket): void {\n const addonTerminal = <ITerminadoAddonTerminal>term;\n addonTerminal.off('data', addonTerminal.__sendData);\n\n socket = (typeof socket === 'undefined') ? addonTerminal.__socket : socket;\n\n if (socket) {\n socket.removeEventListener('message', addonTerminal.__getMessage);\n }\n\n delete addonTerminal.__socket;\n}\n\nexport function apply(terminalConstructor: typeof Terminal): void {\n /**\n * Attaches the current terminal to the given socket\n *\n * @param socket - The socket to attach the current terminal.\n * @param bidirectional - Whether the terminal should send data to the socket as well.\n * @param buffered - Whether the rendering of incoming data should happen instantly or at a\n * maximum frequency of 1 rendering per 10ms.\n */\n (<any>terminalConstructor.prototype).terminadoAttach = function (socket: WebSocket, bidirectional: boolean, buffered: boolean): void {\n return terminadoAttach(this, socket, bidirectional, buffered);\n };\n\n /**\n * Detaches the current terminal from the given socket.\n *\n * @param socket The socket from which to detach the current terminal.\n */\n (<any>terminalConstructor.prototype).terminadoDetach = function (socket: WebSocket): void {\n return terminadoDetach(this, socket);\n };\n}\n",null],"names":[],"mappings":"ACAA;;;ADoBA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AAAA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AAAA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AAEA;AACA;AACA;AAEA;AAEA;AACA;AACA;AACA;AAEA;AACA;AACA;AA/CA;AAuDA;AACA;AACA;AAEA;AAEA;AACA;AACA;AAEA;AACA;AAXA;AAaA;AASA;AACA;AACA;AAOA;AACA;AACA;AACA;AArBA;"}
|
|
@ -1 +0,0 @@
|
|||
{"version":3,"file":"webLinks.js","sources":["../../../src/addons/webLinks/webLinks.ts","../../../node_modules/browser-pack/_prelude.js"],"sourcesContent":["/**\n * Copyright (c) 2017 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { Terminal, ILinkMatcherOptions } from 'xterm';\n\nconst protocolClause = '(https?:\\\\/\\\\/)';\nconst domainCharacterSet = '[\\\\da-z\\\\.-]+';\nconst negatedDomainCharacterSet = '[^\\\\da-z\\\\.-]+';\nconst domainBodyClause = '(' + domainCharacterSet + ')';\nconst tldClause = '([a-z\\\\.]{2,6})';\nconst ipClause = '((\\\\d{1,3}\\\\.){3}\\\\d{1,3})';\nconst localHostClause = '(localhost)';\nconst portClause = '(:\\\\d{1,5})';\nconst hostClause = '((' + domainBodyClause + '\\\\.' + tldClause + ')|' + ipClause + '|' + localHostClause + ')' + portClause + '?';\nconst pathClause = '(\\\\/[\\\\/\\\\w\\\\.\\\\-%~]*)*';\nconst queryStringHashFragmentCharacterSet = '[0-9\\\\w\\\\[\\\\]\\\\(\\\\)\\\\/\\\\?\\\\!#@$%&\\'*+,:;~\\\\=\\\\.\\\\-]*';\nconst queryStringClause = '(\\\\?' + queryStringHashFragmentCharacterSet + ')?';\nconst hashFragmentClause = '(#' + queryStringHashFragmentCharacterSet + ')?';\nconst negatedPathCharacterSet = '[^\\\\/\\\\w\\\\.\\\\-%]+';\nconst bodyClause = hostClause + pathClause + queryStringClause + hashFragmentClause;\nconst start = '(?:^|' + negatedDomainCharacterSet + ')(';\nconst end = ')($|' + negatedPathCharacterSet + ')';\nconst strictUrlRegex = new RegExp(start + protocolClause + bodyClause + end);\n\nfunction handleLink(event: MouseEvent, uri: string): void {\n window.open(uri, '_blank');\n}\n\n/**\n * Initialize the web links addon, registering the link matcher.\n * @param term The terminal to use web links within.\n * @param handler A custom handler to use.\n * @param options Custom options to use, matchIndex will always be ignored.\n */\nexport function webLinksInit(term: Terminal, handler: (event: MouseEvent, uri: string) => void = handleLink, options: ILinkMatcherOptions = {}): void {\n options.matchIndex = 1;\n term.registerLinkMatcher(strictUrlRegex, handler, options);\n}\n\nexport function apply(terminalConstructor: typeof Terminal): void {\n (<any>terminalConstructor.prototype).webLinksInit = function (handler?: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions): void {\n webLinksInit(this, handler, options);\n };\n}\n",null],"names":[],"mappings":"ACAA;;;ADOA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AAQA;AAAA;AAAA;AACA;AACA;AACA;AAHA;AAKA;AACA;AACA;AACA;AACA;AAJA;"}
|
|
@ -1 +0,0 @@
|
|||
{"version":3,"file":"winptyCompat.js","sources":["../../../src/addons/winptyCompat/winptyCompat.ts","../../../node_modules/browser-pack/_prelude.js"],"sourcesContent":["/**\n * Copyright (c) 2017 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { Terminal } from 'xterm';\nimport { IWinptyCompatAddonTerminal } from './Interfaces';\n\nexport function winptyCompatInit(terminal: Terminal): void {\n const addonTerminal = <IWinptyCompatAddonTerminal>terminal;\n\n // Don't do anything when the platform is not Windows\n const isWindows = ['Windows', 'Win16', 'Win32', 'WinCE'].indexOf(navigator.platform) >= 0;\n if (!isWindows) {\n return;\n }\n\n // Winpty does not support wraparound mode which means that lines will never\n // be marked as wrapped. This causes issues for things like copying a line\n // retaining the wrapped new line characters or if consumers are listening\n // in on the data stream.\n //\n // The workaround for this is to listen to every incoming line feed and mark\n // the line as wrapped if the last character in the previous line is not a\n // space. This is certainly not without its problems, but generally on\n // Windows when text reaches the end of the terminal it's likely going to be\n // wrapped.\n addonTerminal.on('linefeed', () => {\n const line = addonTerminal._core.buffer.lines.get(addonTerminal._core.buffer.ybase + addonTerminal._core.buffer.y - 1);\n const lastChar = line[addonTerminal.cols - 1];\n\n if (lastChar[3] !== 32 /* ' ' */) {\n const nextLine = addonTerminal._core.buffer.lines.get(addonTerminal._core.buffer.ybase + addonTerminal._core.buffer.y);\n (<any>nextLine).isWrapped = true;\n }\n });\n}\n\nexport function apply(terminalConstructor: typeof Terminal): void {\n (<any>terminalConstructor.prototype).winptyCompatInit = function (): void {\n winptyCompatInit(this);\n };\n}\n",null],"names":[],"mappings":"ACAA;;;ADQA;AACA;AAGA;AACA;AACA;AACA;AAYA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AA5BA;AA8BA;AACA;AACA;AACA;AACA;AAJA;"}
|
|
@ -1 +0,0 @@
|
|||
{"version":3,"file":"zmodem.js","sources":["../../../src/addons/zmodem/zmodem.ts","../../../node_modules/browser-pack/_prelude.js"],"sourcesContent":["/**\n * Copyright (c) 2017 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { Terminal } from 'xterm';\n\n/**\n *\n * Allow xterm.js to handle ZMODEM uploads and downloads.\n *\n * This addon is a wrapper around zmodem.js. It adds the following to the\n * Terminal class:\n *\n * - function `zmodemAttach(<WebSocket>, <Object>)` - creates a Zmodem.Sentry\n * on the passed WebSocket object. The Object passed is optional and\n * can contain:\n * - noTerminalWriteOutsideSession: Suppress writes from the Sentry\n * object to the Terminal while there is no active Session. This\n * is necessary for compatibility with, for example, the\n * `attach.js` addon.\n *\n * - event `zmodemDetect` - fired on Zmodem.Sentry’s `on_detect` callback.\n * Passes the zmodem.js Detection object.\n *\n * - event `zmodemRetract` - fired on Zmodem.Sentry’s `on_retract` callback.\n *\n * You’ll need to provide logic to handle uploads and downloads.\n * See zmodem.js’s documentation for more details.\n *\n * **IMPORTANT:** After you confirm() a zmodem.js Detection, if you have\n * used the `attach` or `terminado` addons, you’ll need to suspend their\n * operation for the duration of the ZMODEM session. (The demo does this\n * via `detach()` and a re-`attach()`.)\n */\n\nlet zmodem;\n\nexport interface IZmodemOptions {\n noTerminalWriteOutsideSession?: boolean;\n}\n\nfunction zmodemAttach(ws: WebSocket, opts: IZmodemOptions = {}): void {\n const term = this;\n const senderFunc = (octets: ArrayLike<number>) => ws.send(new Uint8Array(octets));\n\n let zsentry;\n\n function shouldWrite(): boolean {\n return !!zsentry.get_confirmed_session() || !opts.noTerminalWriteOutsideSession;\n }\n\n zsentry = new zmodem.Sentry({\n to_terminal: (octets: ArrayLike<number>) => {\n if (shouldWrite()) {\n term.write(\n String.fromCharCode.apply(String, octets)\n );\n }\n },\n sender: senderFunc,\n on_retract: () => (<any>term).emit('zmodemRetract'),\n on_detect: (detection: any) => (<any>term).emit('zmodemDetect', detection)\n });\n\n function handleWSMessage(evt: MessageEvent): void {\n\n // In testing with xterm.js’s demo the first message was\n // always text even if the rest were binary. While that\n // may be specific to xterm.js’s demo, ultimately we\n // should reject anything that isn’t binary.\n if (typeof evt.data === 'string') {\n if (shouldWrite()) {\n term.write(evt.data);\n }\n }\n else {\n zsentry.consume(evt.data);\n }\n }\n\n ws.binaryType = 'arraybuffer';\n ws.addEventListener('message', handleWSMessage);\n}\n\nexport function apply(terminalConstructor: typeof Terminal): void {\n zmodem = (typeof window === 'object') ? (<any>window).Zmodem : {Browser: null}; // Nullify browser for tests\n\n (<any>terminalConstructor.prototype).zmodemAttach = zmodemAttach;\n (<any>terminalConstructor.prototype).zmodemBrowser = zmodem.Browser;\n}\n",null],"names":[],"mappings":"ACAA;;;ADoCA;AAMA;AAAA;AACA;AACA;AAEA;AAEA;AACA;AACA;AAEA;AACA;AACA;AACA;AAGA;AACA;AACA;AACA;AACA;AACA;AAEA;AAMA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AAEA;AACA;AAEA;AACA;AACA;AALA;"}
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,41 @@
|
|||
{% load static %}
|
||||
{% load i18n %}
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="shortcut icon" href="{{ FAVICON_URL }}" type="image/x-icon">
|
||||
<title>{% block html_title %}{% endblock %}</title>
|
||||
|
||||
{% include '_head_css_js.html' %}
|
||||
<link href="{% static "css/jumpserver.css" %}" rel="stylesheet">
|
||||
<script src="{% static "js/jumpserver.js" %}"></script>
|
||||
<style>
|
||||
.outerBox {
|
||||
margin: 0 auto;
|
||||
padding: 100px 20px 20px 20px;
|
||||
}
|
||||
</style>
|
||||
{% block custom_head_css_js %} {% endblock %}
|
||||
</head>
|
||||
|
||||
<body class="gray-bg">
|
||||
<div class="outerBox animated fadeInDown">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
{% block content %} {% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
<hr/>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
{% include '_copyright.html' %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
{% include '_foot_js.html' %}
|
||||
{% block custom_foot_js %} {% endblock %}
|
||||
</html>
|
|
@ -15,45 +15,33 @@ p {
|
|||
</style>
|
||||
<div style="margin: 0 200px">
|
||||
<div class="group">
|
||||
<h2>JumpServer {% trans 'Client' %}</h2>
|
||||
<h2>JumpServer {% trans 'Client' %} v1.1.5</h2>
|
||||
<p>
|
||||
{% trans 'JumpServer Client, currently used to launch the client, now only support launch RDP SSH client, The Telnet client will next' %}
|
||||
{# //JumpServer 客户端,支持 RDP 的本地拉起,后续会支持拉起 ssh。#}
|
||||
</p>
|
||||
<ul>
|
||||
<li> <a href="/download/JumpServer-Client-Installer.msi">Windows {% trans 'Client' %}</a></li>
|
||||
<li> <a href="/download/JumpServer-Client-Installer.dmg">macOS {% trans 'Client' %}</a></li>
|
||||
<li> <a href="/download/JumpServer-Client-Installer-x86_64.msi">jumpserver-client-windows-x86_64.msi</a></li>
|
||||
<li> <a href="/download/JumpServer-Client-Installer-arm64.msi">jumpserver-client-windows-arm64.msi</a></li>
|
||||
<li> <a href="/download/JumpServer-Client-Installer.dmg">jumpserver-client-darwin.dmg</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="group">
|
||||
<h2>{% trans 'Microsoft' %} RDP {% trans 'Official' %}{% trans 'Client' %}</h2>
|
||||
<h2>{% trans 'Microsoft' %} RDP {% trans 'Official' %}{% trans 'Client' %} v10.6.7</h2>
|
||||
<p>
|
||||
{% trans 'macOS needs to download the client to connect RDP asset, which comes with Windows' %}
|
||||
</p>
|
||||
<ul>
|
||||
<li><a href="/download/Microsoft_Remote_Desktop_10.6.7_installer.pkg">Microsoft_Remote_Desktop_10.6.7_installer.pkg</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="group">
|
||||
<h2>SSH {% trans 'Client' %}</h2>
|
||||
<p>
|
||||
{% trans 'Windows needs to download the client to connect SSH assets, and the MacOS system uses its own terminal' %}
|
||||
</p>
|
||||
<ul>
|
||||
<li><a href="/download/putty/w64/putty.exe">64-bit x86: Putty.exe</a></li>
|
||||
<li><a href="/download/putty/wa64/putty.exe">64-bit Arm: Putty.exe</a></li>
|
||||
<li><a href="/download/putty/w32/putty.exe">32-bit x86: Putty.exe</a></li>
|
||||
<li><a href="/download/Microsoft_Remote_Desktop_10.6.7_installer.pkg">microsoft-remote-desktop-installer.pkg</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{% if XPACK_ENABLED %}
|
||||
<div class="group">
|
||||
<h2>{% trans 'Windows Remote application publisher tools' %}</h2>
|
||||
<h2>{% trans 'Windows Remote application publisher tools' %} v2.0</h2>
|
||||
<p>{% trans 'Jmservisor is the program used to pull up remote applications in Windows Remote Application publisher' %}</p>
|
||||
<ul>
|
||||
<li><a href="/download/Jmservisor.msi">Jmservisor</a></li>
|
||||
<li><a href="/download/Jmservisor.msi">jmservisor.msi</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
import pytz
|
||||
import inspect
|
||||
|
||||
from datetime import datetime
|
||||
from functools import reduce, partial
|
||||
from itertools import groupby
|
||||
import pytz
|
||||
from uuid import UUID
|
||||
import inspect
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.db.models import QuerySet as DJQuerySet
|
||||
|
@ -15,6 +16,7 @@ from elasticsearch.exceptions import RequestError, NotFoundError
|
|||
|
||||
from common.utils.common import lazyproperty
|
||||
from common.utils import get_logger
|
||||
from common.utils.timezone import local_now_date_display, utc_now
|
||||
from common.exceptions import JMSException
|
||||
from .models import AbstractSessionCommand
|
||||
|
||||
|
@ -28,12 +30,13 @@ class InvalidElasticsearch(JMSException):
|
|||
|
||||
class CommandStore(object):
|
||||
def __init__(self, config):
|
||||
hosts = config.get("HOSTS")
|
||||
kwargs = config.get("OTHER", {})
|
||||
self.index = config.get("INDEX") or 'jumpserver'
|
||||
self.doc_type = config.get("DOC_TYPE") or '_doc'
|
||||
self.index_prefix = config.get('INDEX') or 'jumpserver'
|
||||
self.is_index_by_date = bool(config.get('INDEX_BY_DATE'))
|
||||
self.exact_fields = {}
|
||||
self.match_fields = {}
|
||||
hosts = config.get("HOSTS")
|
||||
kwargs = config.get("OTHER", {})
|
||||
|
||||
ignore_verify_certs = kwargs.pop('IGNORE_VERIFY_CERTS', False)
|
||||
if ignore_verify_certs:
|
||||
|
@ -50,6 +53,17 @@ class CommandStore(object):
|
|||
else:
|
||||
self.match_fields.update(may_exact_fields)
|
||||
|
||||
self.init_index(config)
|
||||
|
||||
def init_index(self, config):
|
||||
if self.is_index_by_date:
|
||||
date = local_now_date_display()
|
||||
self.index = '%s-%s' % (self.index_prefix, date)
|
||||
self.query_index = '%s-alias' % self.index_prefix
|
||||
else:
|
||||
self.index = config.get("INDEX") or 'jumpserver'
|
||||
self.query_index = config.get("INDEX") or 'jumpserver'
|
||||
|
||||
def is_new_index_type(self):
|
||||
if not self.ping(timeout=3):
|
||||
return False
|
||||
|
@ -101,11 +115,18 @@ class CommandStore(object):
|
|||
else:
|
||||
mappings = {'mappings': {'properties': properties}}
|
||||
|
||||
if self.is_index_by_date:
|
||||
mappings['aliases'] = {
|
||||
self.query_index: {}
|
||||
}
|
||||
try:
|
||||
self.es.indices.create(self.index, body=mappings)
|
||||
return
|
||||
except RequestError as e:
|
||||
logger.exception(e)
|
||||
if e.error == 'resource_already_exists_exception':
|
||||
logger.warning(e)
|
||||
else:
|
||||
logger.exception(e)
|
||||
|
||||
@staticmethod
|
||||
def make_data(command):
|
||||
|
@ -141,7 +162,7 @@ class CommandStore(object):
|
|||
body = self.get_query_body(**query)
|
||||
|
||||
data = self.es.search(
|
||||
index=self.index, doc_type=self.doc_type, body=body, from_=from_, size=size,
|
||||
index=self.query_index, doc_type=self.doc_type, body=body, from_=from_, size=size,
|
||||
sort=sort
|
||||
)
|
||||
source_data = []
|
||||
|
@ -154,7 +175,7 @@ class CommandStore(object):
|
|||
|
||||
def count(self, **query):
|
||||
body = self.get_query_body(**query)
|
||||
data = self.es.count(index=self.index, doc_type=self.doc_type, body=body)
|
||||
data = self.es.count(index=self.query_index, doc_type=self.doc_type, body=body)
|
||||
return data["count"]
|
||||
|
||||
def __getattr__(self, item):
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# Generated by Django 2.2.5 on 2019-11-22 10:07
|
||||
|
||||
import common.fields.model
|
||||
import common.db.fields
|
||||
from django.db import migrations, models
|
||||
import uuid
|
||||
|
||||
|
@ -21,7 +21,7 @@ class Migration(migrations.Migration):
|
|||
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
|
||||
('name', models.CharField(max_length=32, unique=True, verbose_name='Name')),
|
||||
('type', models.CharField(choices=[('null', 'Null'), ('server', 'Server'), ('es', 'Elasticsearch')], default='server', max_length=16, verbose_name='Type')),
|
||||
('meta', common.fields.model.EncryptJsonDictTextField(default={})),
|
||||
('meta', common.db.fields.EncryptJsonDictTextField(default={})),
|
||||
('comment', models.TextField(blank=True, default='', max_length=128, verbose_name='Comment')),
|
||||
],
|
||||
options={
|
||||
|
@ -37,7 +37,7 @@ class Migration(migrations.Migration):
|
|||
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
|
||||
('name', models.CharField(max_length=32, unique=True, verbose_name='Name')),
|
||||
('type', models.CharField(choices=[('null', 'Null'), ('server', 'Server'), ('s3', 'S3'), ('ceph', 'Ceph'), ('swift', 'Swift'), ('oss', 'OSS'), ('azure', 'Azure')], default='server', max_length=16, verbose_name='Type')),
|
||||
('meta', common.fields.model.EncryptJsonDictTextField(default={})),
|
||||
('meta', common.db.fields.EncryptJsonDictTextField(default={})),
|
||||
('comment', models.TextField(blank=True, default='', max_length=128, verbose_name='Comment')),
|
||||
],
|
||||
options={
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
# Generated by Django 3.1.14 on 2022-04-12 07:39
|
||||
|
||||
import copy
|
||||
import common.fields.model
|
||||
import common.db.fields
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
@ -83,13 +82,13 @@ class Migration(migrations.Migration):
|
|||
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=128, unique=True, verbose_name='Name')),
|
||||
('host', models.CharField(max_length=256, verbose_name='Host', blank=True)),
|
||||
('https_port', common.fields.model.PortField(default=443, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(65535)], verbose_name='HTTPS Port')),
|
||||
('http_port', common.fields.model.PortField(default=80, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(65535)], verbose_name='HTTP Port')),
|
||||
('ssh_port', common.fields.model.PortField(default=2222, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(65535)], verbose_name='SSH Port')),
|
||||
('rdp_port', common.fields.model.PortField(default=3389, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(65535)], verbose_name='RDP Port')),
|
||||
('mysql_port', common.fields.model.PortField(default=33060, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(65535)], verbose_name='MySQL Port')),
|
||||
('mariadb_port', common.fields.model.PortField(default=33061, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(65535)], verbose_name='MariaDB Port')),
|
||||
('postgresql_port', common.fields.model.PortField(default=54320, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(65535)], verbose_name='PostgreSQL Port')),
|
||||
('https_port', common.db.fields.PortField(default=443, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(65535)], verbose_name='HTTPS Port')),
|
||||
('http_port', common.db.fields.PortField(default=80, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(65535)], verbose_name='HTTP Port')),
|
||||
('ssh_port', common.db.fields.PortField(default=2222, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(65535)], verbose_name='SSH Port')),
|
||||
('rdp_port', common.db.fields.PortField(default=3389, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(65535)], verbose_name='RDP Port')),
|
||||
('mysql_port', common.db.fields.PortField(default=33060, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(65535)], verbose_name='MySQL Port')),
|
||||
('mariadb_port', common.db.fields.PortField(default=33061, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(65535)], verbose_name='MariaDB Port')),
|
||||
('postgresql_port', common.db.fields.PortField(default=54320, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(65535)], verbose_name='PostgreSQL Port')),
|
||||
('comment', models.TextField(blank=True, default='', verbose_name='Comment')),
|
||||
],
|
||||
options={
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
# Generated by Django 3.1.14 on 2022-05-12 06:35
|
||||
|
||||
import common.db.fields
|
||||
import django.core.validators
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('terminal', '0048_endpoint_endpointrule'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='endpoint',
|
||||
name='redis_port',
|
||||
field=common.db.fields.PortField(default=63790, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(65535)], verbose_name='Redis Port'),
|
||||
),
|
||||
]
|
|
@ -2,7 +2,7 @@ from django.db import models
|
|||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.core.validators import MinValueValidator, MaxValueValidator
|
||||
from common.db.models import JMSModel
|
||||
from common.fields.model import PortField
|
||||
from common.db.fields import PortField
|
||||
from common.utils.ip import contains_ip
|
||||
|
||||
|
||||
|
@ -17,6 +17,7 @@ class Endpoint(JMSModel):
|
|||
mysql_port = PortField(default=33060, verbose_name=_('MySQL Port'))
|
||||
mariadb_port = PortField(default=33061, verbose_name=_('MariaDB Port'))
|
||||
postgresql_port = PortField(default=54320, verbose_name=_('PostgreSQL Port'))
|
||||
redis_port = PortField(default=63790, verbose_name=_('Redis Port'))
|
||||
comment = models.TextField(default='', blank=True, verbose_name=_('Comment'))
|
||||
|
||||
default_id = '00000000-0000-0000-0000-000000000001'
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
import os
|
||||
|
||||
from importlib import import_module
|
||||
|
||||
import jms_storage
|
||||
|
@ -9,7 +10,8 @@ from django.utils.translation import ugettext_lazy as _
|
|||
from django.conf import settings
|
||||
from common.mixins import CommonModelMixin
|
||||
from common.utils import get_logger
|
||||
from common.fields.model import EncryptJsonDictTextField
|
||||
from common.db.fields import EncryptJsonDictTextField
|
||||
from common.utils.timezone import local_now_date_display
|
||||
from terminal.backends import TYPE_ENGINE_MAPPING
|
||||
from .terminal import Terminal
|
||||
from .command import Command
|
||||
|
@ -63,6 +65,10 @@ class CommandStorage(CommonStorageModelMixin, CommonModelMixin):
|
|||
def type_server(self):
|
||||
return self.type == const.CommandStorageTypeChoices.server.value
|
||||
|
||||
@property
|
||||
def type_es(self):
|
||||
return self.type == const.CommandStorageTypeChoices.es.value
|
||||
|
||||
@property
|
||||
def type_null_or_server(self):
|
||||
return self.type_null or self.type_server
|
||||
|
@ -73,6 +79,18 @@ class CommandStorage(CommonStorageModelMixin, CommonModelMixin):
|
|||
config.update({'TYPE': self.type})
|
||||
return config
|
||||
|
||||
@property
|
||||
def valid_config(self):
|
||||
config = self.config
|
||||
if self.type_es and config.get('INDEX_BY_DATE'):
|
||||
engine_mod = import_module(TYPE_ENGINE_MAPPING[self.type])
|
||||
store = engine_mod.CommandStore(config)
|
||||
store._ensure_index_exists()
|
||||
index_prefix = config.get('INDEX') or 'jumpserver'
|
||||
date = local_now_date_display()
|
||||
config['INDEX'] = '%s-%s' % (index_prefix, date)
|
||||
return config
|
||||
|
||||
def is_valid(self):
|
||||
if self.type_null_or_server:
|
||||
return True
|
||||
|
@ -89,16 +107,20 @@ class CommandStorage(CommonStorageModelMixin, CommonModelMixin):
|
|||
return Terminal.objects.filter(command_storage=self.name, is_deleted=False).exists()
|
||||
|
||||
def get_command_queryset(self):
|
||||
if self.type_null:
|
||||
return Command.objects.none()
|
||||
|
||||
if self.type_server:
|
||||
qs = Command.objects.all()
|
||||
else:
|
||||
if self.type not in TYPE_ENGINE_MAPPING:
|
||||
logger.error(f'Command storage `{self.type}` not support')
|
||||
return Command.objects.none()
|
||||
return Command.objects.all()
|
||||
|
||||
if self.type in TYPE_ENGINE_MAPPING:
|
||||
engine_mod = import_module(TYPE_ENGINE_MAPPING[self.type])
|
||||
qs = engine_mod.QuerySet(self.config)
|
||||
qs.model = Command
|
||||
return qs
|
||||
return qs
|
||||
|
||||
logger.error(f'Command storage `{self.type}` not support')
|
||||
return Command.objects.none()
|
||||
|
||||
def save(self, force_insert=False, force_update=False, using=None,
|
||||
update_fields=None):
|
||||
|
|
|
@ -68,7 +68,7 @@ class StorageMixin:
|
|||
def get_command_storage_config(self):
|
||||
s = self.get_command_storage()
|
||||
if s:
|
||||
config = s.config
|
||||
config = s.valid_config
|
||||
else:
|
||||
config = settings.DEFAULT_TERMINAL_COMMAND_STORAGE
|
||||
return config
|
||||
|
|
|
@ -16,7 +16,7 @@ class EndpointSerializer(BulkModelSerializer):
|
|||
'host',
|
||||
'https_port', 'http_port', 'ssh_port',
|
||||
'rdp_port', 'mysql_port', 'mariadb_port',
|
||||
'postgresql_port',
|
||||
'postgresql_port', 'redis_port',
|
||||
]
|
||||
fields = fields_mini + fields_small + [
|
||||
'comment', 'date_created', 'date_updated', 'created_by'
|
||||
|
@ -29,6 +29,7 @@ class EndpointSerializer(BulkModelSerializer):
|
|||
'mysql_port': {'default': 33060},
|
||||
'mariadb_port': {'default': 33061},
|
||||
'postgresql_port': {'default': 54320},
|
||||
'redis_port': {'default': 63790},
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -155,6 +155,10 @@ class CommandStorageTypeESSerializer(serializers.Serializer):
|
|||
child=serializers.CharField(validators=[command_storage_es_host_format_validator]),
|
||||
label=_('Hosts'), help_text=_(hosts_help_text), allow_null=True
|
||||
)
|
||||
INDEX_BY_DATE = serializers.BooleanField(
|
||||
default=False, label=_('Index by date'),
|
||||
help_text=_('Whether to create an index by date')
|
||||
)
|
||||
INDEX = serializers.CharField(
|
||||
max_length=1024, default='jumpserver', label=_('Index'), allow_null=True
|
||||
)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# Generated by Django 2.2.5 on 2019-11-15 06:57
|
||||
|
||||
import common.fields.model
|
||||
import common.db.fields
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
@ -26,7 +26,7 @@ class Migration(migrations.Migration):
|
|||
('user_display', models.CharField(max_length=128, verbose_name='User display name')),
|
||||
('title', models.CharField(max_length=256, verbose_name='Title')),
|
||||
('body', models.TextField(verbose_name='Body')),
|
||||
('meta', common.fields.model.JsonDictTextField(default='{}', verbose_name='Meta')),
|
||||
('meta', common.db.fields.JsonDictTextField(default='{}', verbose_name='Meta')),
|
||||
('assignee_display', models.CharField(blank=True, max_length=128, null=True, verbose_name='Assignee display name')),
|
||||
('assignees_display', models.CharField(blank=True, max_length=128, verbose_name='Assignees display name')),
|
||||
('type', models.CharField(choices=[('general', 'General'), ('login_confirm', 'Login confirm')], default='general', max_length=16, verbose_name='Type')),
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
from urllib.parse import urljoin
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
from django.shortcuts import reverse
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from . import const
|
||||
from notifications.notifications import UserMessage
|
||||
from common.utils import get_logger
|
||||
from common.utils import get_logger, random_string
|
||||
from .models import Ticket
|
||||
from . import const
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
@ -57,6 +59,13 @@ class TicketAppliedToAssignee(BaseTicketMessage):
|
|||
def __init__(self, user, ticket):
|
||||
self.ticket = ticket
|
||||
super().__init__(user)
|
||||
self._token = None
|
||||
|
||||
@property
|
||||
def token(self):
|
||||
if self._token is None:
|
||||
self._token = random_string(32)
|
||||
return self._token
|
||||
|
||||
@property
|
||||
def content_title(self):
|
||||
|
@ -71,6 +80,29 @@ class TicketAppliedToAssignee(BaseTicketMessage):
|
|||
)
|
||||
return title
|
||||
|
||||
def get_ticket_approval_url(self):
|
||||
url = reverse('tickets:direct-approve', kwargs={'token': self.token})
|
||||
return urljoin(settings.SITE_URL, url)
|
||||
|
||||
def get_html_msg(self) -> dict:
|
||||
body = self.ticket.body.replace('\n', '<br/>')
|
||||
context = dict(
|
||||
title=self.content_title,
|
||||
ticket_detail_url=self.ticket_detail_url,
|
||||
body=body,
|
||||
)
|
||||
|
||||
ticket_approval_url = self.get_ticket_approval_url()
|
||||
context.update({'ticket_approval_url': ticket_approval_url})
|
||||
message = render_to_string('tickets/_msg_ticket.html', context)
|
||||
cache.set(self.token, {
|
||||
'body': body, 'ticket_id': self.ticket.id
|
||||
}, 3600)
|
||||
return {
|
||||
'subject': self.subject,
|
||||
'message': message
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def gen_test_msg(cls):
|
||||
from .models import Ticket
|
||||
|
|
|
@ -9,7 +9,13 @@
|
|||
<br>
|
||||
<div>
|
||||
<a href="{{ ticket_detail_url }}" target="_blank">
|
||||
{% trans 'Click here to review' %}
|
||||
{% trans 'View details' %}
|
||||
</a>
|
||||
<br>
|
||||
{% if ticket_approval_url %}
|
||||
<a href="{{ ticket_approval_url }}" target="_blank">
|
||||
{% trans 'Direct approval' %}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
{% extends '_base_double_screen.html' %}
|
||||
{% load bootstrap3 %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
<div class="col-lg-12">
|
||||
<div class="col-lg-6">
|
||||
<div class="ibox-content">
|
||||
<h2 class="font-bold" style="display: inline">{% trans 'Ticket information' %}</h2>
|
||||
<h1></h1>
|
||||
<div style="word-break: break-all">{{ ticket_info | safe }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<div class="ibox-content">
|
||||
<img src="{{ LOGO_URL }}" style="margin: auto" width="50" height="50">
|
||||
<h2 class="font-bold" style="display: inline">{% trans 'Ticket approval' %}</h2>
|
||||
<h1></h1>
|
||||
<div class="ibox-content">
|
||||
<p>
|
||||
{% trans 'Hello' %} {{ user.name }},
|
||||
</p>
|
||||
<p style="text-indent: 3em">
|
||||
{{ prompt_msg }}
|
||||
</p>
|
||||
<br>
|
||||
<form id="approve-form" action="" method="post" role="form" novalidate="novalidate">
|
||||
{% csrf_token %}
|
||||
<div class="form-group" style="">
|
||||
{% if user.is_authenticated %}
|
||||
<button class="btn btn-primary block full-width m-b" name="action" value="approve"
|
||||
type="submit">
|
||||
{% trans 'Ticket direct approval' %}
|
||||
</button>
|
||||
<button class="btn btn-primary block full-width m-b" name="action" value="reject"
|
||||
type="submit">
|
||||
{% trans 'Ticket direct reject' %}
|
||||
</button>
|
||||
{% else %}
|
||||
<a id='login_button' class="btn btn-primary block full-width m-b"
|
||||
href="{{ login_url }}">
|
||||
{% trans 'Go Login' %}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<style>
|
||||
|
||||
</style>
|
||||
{% endblock %}
|
|
@ -0,0 +1,12 @@
|
|||
# coding:utf-8
|
||||
#
|
||||
|
||||
from django.urls import path
|
||||
|
||||
from .. import views
|
||||
|
||||
app_name = 'tickets'
|
||||
|
||||
urlpatterns = [
|
||||
path('direct-approve/<str:token>/', views.TicketDirectApproveView.as_view(), name='direct-approve'),
|
||||
]
|
|
@ -0,0 +1 @@
|
|||
from .approve import *
|
|
@ -0,0 +1,98 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from __future__ import unicode_literals
|
||||
from django.views.generic.base import TemplateView
|
||||
from django.shortcuts import redirect, reverse
|
||||
from django.core.cache import cache
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from tickets.models import Ticket
|
||||
from tickets.errors import AlreadyClosed
|
||||
from common.utils import get_logger, FlashMessageUtil
|
||||
|
||||
logger = get_logger(__name__)
|
||||
__all__ = ['TicketDirectApproveView']
|
||||
|
||||
|
||||
class TicketDirectApproveView(TemplateView):
|
||||
template_name = 'tickets/approve_check_password.html'
|
||||
redirect_field_name = 'next'
|
||||
|
||||
@property
|
||||
def message_data(self):
|
||||
return {
|
||||
'title': _('Ticket approval'),
|
||||
'error': _("This ticket does not exist, "
|
||||
"the process has ended, or this link has expired"),
|
||||
'redirect_url': self.login_url,
|
||||
'auto_redirect': False
|
||||
}
|
||||
|
||||
@property
|
||||
def login_url(self):
|
||||
return reverse('authentication:login') + '?admin=1'
|
||||
|
||||
def redirect_message_response(self, **kwargs):
|
||||
message_data = self.message_data
|
||||
for key, value in kwargs.items():
|
||||
if isinstance(value, str):
|
||||
message_data[key] = value
|
||||
if message_data.get('message'):
|
||||
message_data.pop('error')
|
||||
redirect_url = FlashMessageUtil.gen_message_url(message_data)
|
||||
return redirect(redirect_url)
|
||||
|
||||
@staticmethod
|
||||
def clear(token):
|
||||
cache.delete(token)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
# 放入工单信息
|
||||
token = kwargs.get('token')
|
||||
ticket_info = cache.get(token, {}).get('body', '')
|
||||
if self.request.user.is_authenticated:
|
||||
prompt_msg = _('Click the button below to approve or reject')
|
||||
else:
|
||||
prompt_msg = _('After successful authentication, this ticket can be approved directly')
|
||||
kwargs.update({
|
||||
'ticket_info': ticket_info, 'prompt_msg': prompt_msg,
|
||||
'login_url': '%s&next=%s' % (
|
||||
self.login_url,
|
||||
reverse('tickets:direct-approve', kwargs={'token': token})
|
||||
),
|
||||
})
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
token = kwargs.get('token')
|
||||
ticket_info = cache.get(token)
|
||||
if not ticket_info:
|
||||
return self.redirect_message_response(redirect_url=self.login_url)
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
def post(self, request, **kwargs):
|
||||
user = request.user
|
||||
token = kwargs.get('token')
|
||||
action = request.POST.get('action')
|
||||
if action not in ['approve', 'reject']:
|
||||
msg = _('Illegal approval action')
|
||||
return self.redirect_message_response(error=str(msg))
|
||||
|
||||
ticket_info = cache.get(token)
|
||||
if not ticket_info:
|
||||
return self.redirect_message_response(redirect_url=self.login_url)
|
||||
try:
|
||||
ticket_id = ticket_info.get('ticket_id')
|
||||
ticket = Ticket.all().get(id=ticket_id)
|
||||
if not ticket.has_current_assignee(user):
|
||||
raise Exception(_("This user is not authorized to approve this ticket"))
|
||||
getattr(ticket, action)(user)
|
||||
except AlreadyClosed as e:
|
||||
self.clear(token)
|
||||
return self.redirect_message_response(error=str(e), redirect_url=self.login_url)
|
||||
except Exception as e:
|
||||
return self.redirect_message_response(error=str(e), redirect_url=self.login_url)
|
||||
|
||||
self.clear(token)
|
||||
return self.redirect_message_response(message=_("Success"), redirect_url=self.login_url)
|
|
@ -5,6 +5,7 @@ from django.utils.translation import gettext_lazy as _
|
|||
from captcha.fields import CaptchaField
|
||||
|
||||
from common.utils import validate_ssh_public_key
|
||||
from authentication.forms import EncryptedField
|
||||
from ..models import User
|
||||
|
||||
|
||||
|
@ -17,7 +18,7 @@ __all__ = [
|
|||
|
||||
|
||||
class UserCheckPasswordForm(forms.Form):
|
||||
password = forms.CharField(
|
||||
password = EncryptedField(
|
||||
label=_('Password'), widget=forms.PasswordInput,
|
||||
max_length=1024, strip=False
|
||||
)
|
||||
|
@ -77,12 +78,12 @@ UserFirstLoginFinishForm.verbose_name = _("Finish")
|
|||
|
||||
|
||||
class UserTokenResetPasswordForm(forms.Form):
|
||||
new_password = forms.CharField(
|
||||
new_password = EncryptedField(
|
||||
min_length=5, max_length=128,
|
||||
widget=forms.PasswordInput,
|
||||
label=_("New password")
|
||||
)
|
||||
confirm_password = forms.CharField(
|
||||
confirm_password = EncryptedField(
|
||||
min_length=5, max_length=128,
|
||||
widget=forms.PasswordInput,
|
||||
label=_("Confirm password")
|
||||
|
@ -103,7 +104,7 @@ class UserForgotPasswordForm(forms.Form):
|
|||
|
||||
|
||||
class UserPasswordForm(UserTokenResetPasswordForm):
|
||||
old_password = forms.CharField(
|
||||
old_password = EncryptedField(
|
||||
max_length=128, widget=forms.PasswordInput,
|
||||
label=_("Old password")
|
||||
)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# Generated by Django 2.1.7 on 2019-06-25 03:04
|
||||
|
||||
import common.fields.model
|
||||
import common.db.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
|
@ -14,17 +14,17 @@ class Migration(migrations.Migration):
|
|||
migrations.AlterField(
|
||||
model_name='user',
|
||||
name='_otp_secret_key',
|
||||
field=common.fields.model.EncryptCharField(blank=True, max_length=128, null=True),
|
||||
field=common.db.fields.EncryptCharField(blank=True, max_length=128, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='user',
|
||||
name='_private_key',
|
||||
field=common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='Private key'),
|
||||
field=common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Private key'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='user',
|
||||
name='_public_key',
|
||||
field=common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='Public key'),
|
||||
field=common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Public key'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='user',
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue