Merge pull request #8229 from jumpserver/dev

v2.22.0 rc1
pull/8289/head
Jiangjie.Bai 2022-05-12 17:02:01 +08:00 committed by GitHub
commit 7f52675bd3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
107 changed files with 2091 additions and 1280 deletions

View File

@ -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$"]'

View File

@ -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 FROM python:3.8-slim
ARG PIP_MIRROR=https://pypi.douban.com/simple MAINTAINER JumpServer Team <ibuler@qq.com>
ENV PIP_MIRROR=$PIP_MIRROR
ARG PIP_JMS_MIRROR=https://pypi.douban.com/simple
ENV PIP_JMS_MIRROR=$PIP_JMS_MIRROR
WORKDIR /opt/jumpserver
ARG BUILD_DEPENDENCIES=" \ ARG BUILD_DEPENDENCIES=" \
g++ \ 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 \ && mv /bin/sh /bin/sh.bak \
&& ln -s /bin/bash /bin/sh && 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 \ && 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/ \ && tar xf instantclient-basiclite-linux.x64-21.1.0.0.0.tar -C /opt/oracle/ \
&& echo "/opt/jumpserver/oracle/instantclient_21_1" > /etc/ld.so.conf.d/oracle-instantclient.conf \ && echo "/opt/oracle/instantclient_21_1" > /etc/ld.so.conf.d/oracle-instantclient.conf \
&& ldconfig \ && ldconfig \
&& rm -f instantclient-basiclite-linux.x64-21.1.0.0.0.tar && 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 \ ARG PIP_MIRROR=https://mirrors.aliyun.com/pypi/simple/
&& pip install --upgrade pip==20.2.4 setuptools==49.6.0 wheel==0.34.2 -i ${PIP_MIRROR} \ 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 $(grep -E 'jms|jumpserver' requirements/requirements.txt) -i ${PIP_JMS_MIRROR} \
&& pip install --no-cache-dir -r requirements/requirements.txt -i ${PIP_MIRROR} \ && pip install --no-cache-dir -r requirements/requirements.txt -i ${PIP_MIRROR} \
&& rm -rf ~/.cache/pip && 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/data
VOLUME /opt/jumpserver/logs VOLUME /opt/jumpserver/logs

View File

@ -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 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. 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.

View File

@ -6,6 +6,7 @@ from django.db.models import F, Q
from common.drf.filters import BaseFilterSet from common.drf.filters import BaseFilterSet
from common.drf.api import JMSBulkModelViewSet from common.drf.api import JMSBulkModelViewSet
from common.mixins import RecordViewLogMixin
from rbac.permissions import RBACPermission from rbac.permissions import RBACPermission
from assets.models import SystemUser from assets.models import SystemUser
from ..models import Account from ..models import Account
@ -54,7 +55,7 @@ class SystemUserAppRelationViewSet(ApplicationAccountViewSet):
perm_model = SystemUser perm_model = SystemUser
class ApplicationAccountSecretViewSet(ApplicationAccountViewSet): class ApplicationAccountSecretViewSet(RecordViewLogMixin, ApplicationAccountViewSet):
serializer_class = serializers.AppAccountSecretSerializer serializer_class = serializers.AppAccountSecretSerializer
permission_classes = [RBACPermission, NeedMFAVerify] permission_classes = [RBACPermission, NeedMFAVerify]
http_method_names = ['get', 'options'] http_method_names = ['get', 'options']

View File

@ -1,6 +1,6 @@
# Generated by Django 2.1.7 on 2019-05-20 11:04 # 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 from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
import uuid import uuid
@ -23,7 +23,7 @@ class Migration(migrations.Migration):
('name', models.CharField(max_length=128, verbose_name='Name')), ('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')), ('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')), ('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')), ('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')), ('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')), ('comment', models.TextField(blank=True, default='', max_length=128, verbose_name='Comment')),

View File

@ -1,7 +1,7 @@
# Generated by Django 3.1.12 on 2021-08-26 09:07 # Generated by Django 3.1.12 on 2021-08-26 09:07
import assets.models.base import assets.models.base
import common.fields.model import common.db.fields
from django.conf import settings from django.conf import settings
import django.core.validators import django.core.validators
from django.db import migrations, models from django.db import migrations, models
@ -26,9 +26,9 @@ class Migration(migrations.Migration):
('id', models.UUIDField(db_index=True, default=uuid.uuid4)), ('id', models.UUIDField(db_index=True, default=uuid.uuid4)),
('name', models.CharField(max_length=128, verbose_name='Name')), ('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')), ('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')), ('password', common.db.fields.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')), ('private_key', common.db.fields.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')), ('public_key', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH public key')),
('comment', models.TextField(blank=True, verbose_name='Comment')), ('comment', models.TextField(blank=True, verbose_name='Comment')),
('date_created', models.DateTimeField(blank=True, editable=False, verbose_name='Date created')), ('date_created', models.DateTimeField(blank=True, editable=False, verbose_name='Date created')),
('date_updated', models.DateTimeField(blank=True, editable=False, verbose_name='Date updated')), ('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)), ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('name', models.CharField(max_length=128, verbose_name='Name')), ('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')), ('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')), ('password', common.db.fields.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')), ('private_key', common.db.fields.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')), ('public_key', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH public key')),
('comment', models.TextField(blank=True, verbose_name='Comment')), ('comment', models.TextField(blank=True, verbose_name='Comment')),
('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date created')), ('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date created')),
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),

View File

@ -8,6 +8,7 @@ from rest_framework.generics import CreateAPIView
from orgs.mixins.api import OrgBulkModelViewSet from orgs.mixins.api import OrgBulkModelViewSet
from rbac.permissions import RBACPermission from rbac.permissions import RBACPermission
from common.drf.filters import BaseFilterSet from common.drf.filters import BaseFilterSet
from common.mixins import RecordViewLogMixin
from common.permissions import NeedMFAVerify from common.permissions import NeedMFAVerify
from ..tasks.account_connectivity import test_accounts_connectivity_manual from ..tasks.account_connectivity import test_accounts_connectivity_manual
from ..models import AuthBook, Node from ..models import AuthBook, Node
@ -79,7 +80,7 @@ class AccountViewSet(OrgBulkModelViewSet):
return Response(data={'task': task.id}) return Response(data={'task': task.id})
class AccountSecretsViewSet(AccountViewSet): class AccountSecretsViewSet(RecordViewLogMixin, AccountViewSet):
""" """
因为可能要导出所有账号所以单独建立了一个 viewset 因为可能要导出所有账号所以单独建立了一个 viewset
""" """

View File

@ -4,7 +4,6 @@ from rest_framework.response import Response
from rest_framework.decorators import action from rest_framework.decorators import action
from common.utils import get_logger, get_object_or_none 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.permissions import IsValidUser
from common.mixins.api import SuggestionMixin from common.mixins.api import SuggestionMixin
from orgs.mixins.api import OrgBulkModelViewSet from orgs.mixins.api import OrgBulkModelViewSet
@ -102,27 +101,17 @@ class SystemUserTempAuthInfoApi(generics.CreateAPIView):
permission_classes = (IsValidUser,) permission_classes = (IsValidUser,)
serializer_class = SystemUserTempAuthSerializer 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): def create(self, request, *args, **kwargs):
serializer = super().get_serializer(data=request.data) serializer = super().get_serializer(data=request.data)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
pk = kwargs.get('pk') pk = kwargs.get('pk')
data = self.decrypt_data_if_need(serializer.validated_data) data = serializer.validated_data
instance_id = data.get('instance_id') asset_or_app_id = data.get('instance_id')
with tmp_to_root_org(): with tmp_to_root_org():
instance = get_object_or_404(SystemUser, pk=pk) 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) return Response(serializer.data, status=201)

View File

@ -1,7 +1,7 @@
# Generated by Django 2.1.7 on 2019-06-24 13:08 # Generated by Django 2.1.7 on 2019-06-24 13:08
import assets.models.utils import assets.models.utils
import common.fields.model import common.db.fields
from django.db import migrations from django.db import migrations
@ -15,61 +15,61 @@ class Migration(migrations.Migration):
migrations.AlterField( migrations.AlterField(
model_name='adminuser', model_name='adminuser',
name='_password', 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( migrations.AlterField(
model_name='adminuser', model_name='adminuser',
name='_private_key', 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( migrations.AlterField(
model_name='adminuser', model_name='adminuser',
name='_public_key', 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( migrations.AlterField(
model_name='authbook', model_name='authbook',
name='_password', 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( migrations.AlterField(
model_name='authbook', model_name='authbook',
name='_private_key', 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( migrations.AlterField(
model_name='authbook', model_name='authbook',
name='_public_key', 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( migrations.AlterField(
model_name='gateway', model_name='gateway',
name='_password', 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( migrations.AlterField(
model_name='gateway', model_name='gateway',
name='_private_key', 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( migrations.AlterField(
model_name='gateway', model_name='gateway',
name='_public_key', 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( migrations.AlterField(
model_name='systemuser', model_name='systemuser',
name='_password', 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( migrations.AlterField(
model_name='systemuser', model_name='systemuser',
name='_private_key', 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( migrations.AlterField(
model_name='systemuser', model_name='systemuser',
name='_public_key', 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'),
), ),
] ]

View File

@ -1,6 +1,6 @@
# Generated by Django 2.1.7 on 2019-07-11 12:18 # 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 from django.db import migrations
@ -14,21 +14,21 @@ class Migration(migrations.Migration):
migrations.AlterField( migrations.AlterField(
model_name='adminuser', model_name='adminuser',
name='private_key', 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( migrations.AlterField(
model_name='authbook', model_name='authbook',
name='private_key', 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( migrations.AlterField(
model_name='gateway', model_name='gateway',
name='private_key', 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( migrations.AlterField(
model_name='systemuser', model_name='systemuser',
name='private_key', 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'),
), ),
] ]

View File

@ -1,6 +1,6 @@
# Generated by Django 2.2.7 on 2019-12-06 07:26 # 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 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')), ('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')), ('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')), ('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')), ('internal', models.BooleanField(default=False, verbose_name='Internal')),
('comment', models.TextField(blank=True, null=True, verbose_name='Comment')), ('comment', models.TextField(blank=True, null=True, verbose_name='Comment')),
], ],

View File

@ -1,6 +1,6 @@
# Generated by Django 3.1.6 on 2021-06-05 16:10 # 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 from django.conf import settings
import django.core.validators import django.core.validators
from django.db import migrations, models from django.db import migrations, models
@ -58,9 +58,9 @@ class Migration(migrations.Migration):
('id', models.UUIDField(db_index=True, default=uuid.uuid4)), ('id', models.UUIDField(db_index=True, default=uuid.uuid4)),
('name', models.CharField(max_length=128, verbose_name='Name')), ('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')), ('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')), ('password', common.db.fields.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')), ('private_key', common.db.fields.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')), ('public_key', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH public key')),
('comment', models.TextField(blank=True, verbose_name='Comment')), ('comment', models.TextField(blank=True, verbose_name='Comment')),
('date_created', models.DateTimeField(blank=True, editable=False, verbose_name='Date created')), ('date_created', models.DateTimeField(blank=True, editable=False, verbose_name='Date created')),
('date_updated', models.DateTimeField(blank=True, editable=False, verbose_name='Date updated')), ('date_updated', models.DateTimeField(blank=True, editable=False, verbose_name='Date updated')),

View File

@ -3,6 +3,20 @@
from django.db import migrations, models 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): class Migration(migrations.Migration):
dependencies = [ dependencies = [
@ -15,4 +29,5 @@ class Migration(migrations.Migration):
name='number', name='number',
field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Asset number'), field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Asset number'),
), ),
migrations.RunPython(create_internal_platform)
] ]

View File

@ -11,7 +11,7 @@ from django.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from rest_framework.exceptions import ValidationError from rest_framework.exceptions import ValidationError
from common.fields.model import JsonDictTextField from common.db.fields import JsonDictTextField
from common.utils import lazyproperty from common.utils import lazyproperty
from orgs.mixins.models import OrgModelMixin, OrgManager from orgs.mixins.models import OrgModelMixin, OrgManager
@ -301,7 +301,7 @@ class Asset(AbsConnectivity, AbsHardwareInfo, ProtocolsMixin, NodesRelationMixin
'private_key': auth_user.private_key_file 'private_key': auth_user.private_key_file
} }
if not with_become: if not with_become or self.is_windows():
return info return info
if become_user: if become_user:

View File

@ -19,7 +19,7 @@ from common.utils import (
) )
from common.utils.encode import ssh_pubkey_gen from common.utils.encode import ssh_pubkey_gen
from common.validators import alphanumeric from common.validators import alphanumeric
from common import fields from common.db import fields
from orgs.mixins.models import OrgModelMixin from orgs.mixins.models import OrgModelMixin

View File

@ -6,12 +6,13 @@ from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers from rest_framework import serializers
from common.utils import ssh_pubkey_gen, ssh_private_key_gen, validate_ssh_private_key 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 from assets.models import Type
class AuthSerializer(serializers.ModelSerializer): class AuthSerializer(serializers.ModelSerializer):
password = serializers.CharField(required=False, allow_blank=True, allow_null=True, max_length=1024) password = EncryptedField(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) private_key = EncryptedField(required=False, allow_blank=True, allow_null=True, max_length=4096)
def gen_keys(self, private_key=None, password=None): def gen_keys(self, private_key=None, password=None):
if private_key is None: if private_key is None:
@ -31,6 +32,8 @@ class AuthSerializer(serializers.ModelSerializer):
class AuthSerializerMixin(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( passphrase = serializers.CharField(
allow_blank=True, allow_null=True, required=False, max_length=512, allow_blank=True, allow_null=True, required=False, max_length=512,
write_only=True, label=_('Key password') write_only=True, label=_('Key password')

View File

@ -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]) 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 comment = system_user.name
algorithm = kwargs.get('algorithm')
if username is None: if username is None:
username = system_user.username username = system_user.username
@ -104,7 +104,7 @@ def get_push_unixlike_system_user_tasks(system_user, username=None):
'module': 'user', 'module': 'user',
'args': 'name={} shell={} state=present password={}'.format( 'args': 'name={} shell={} state=present password={}'.format(
username, system_user.shell, 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 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: if username is None:
username = system_user.username username = system_user.username
password = system_user.password password = system_user.password
@ -176,7 +176,7 @@ def get_push_windows_system_user_tasks(system_user: SystemUser, username=None):
return tasks 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 命令跟资产无关 获取推送系统用户的 ansible 命令跟资产无关
:param system_user: :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) get_tasks = get_task_map.get(platform, get_push_unixlike_system_user_tasks)
if not system_user.username_same_with_user: if not system_user.username_same_with_user:
return get_tasks(system_user) return get_tasks(system_user, algorithm=algorithm)
tasks = [] tasks = []
# 仅推送这个username # 仅推送这个username
if username is not None: if username is not None:
tasks.extend(get_tasks(system_user, username)) tasks.extend(get_tasks(system_user, username, algorithm=algorithm))
return tasks return tasks
users = system_user.users.all().values_list('username', flat=True) users = system_user.users.all().values_list('username', flat=True)
print(_("System user is dynamic: {}").format(list(users))) print(_("System user is dynamic: {}").format(list(users)))
for _username in users: for _username in users:
tasks.extend(get_tasks(system_user, _username)) tasks.extend(get_tasks(system_user, _username, algorithm=algorithm))
return tasks return tasks
@ -244,7 +244,11 @@ def push_system_user_util(system_user, assets, task_name, username=None):
for u in usernames: for u in usernames:
for a in _assets: for a in _assets:
system_user.load_asset_special_auth(a, u) 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]) run_task(tasks, [a])

View File

@ -3,3 +3,23 @@
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
DEFAULT_CITY = _("Unknown") 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',
)

View File

@ -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'),
),
]

View File

@ -49,10 +49,12 @@ class FTPLog(OrgModelMixin):
class OperateLog(OrgModelMixin): class OperateLog(OrgModelMixin):
ACTION_CREATE = 'create' ACTION_CREATE = 'create'
ACTION_VIEW = 'view'
ACTION_UPDATE = 'update' ACTION_UPDATE = 'update'
ACTION_DELETE = 'delete' ACTION_DELETE = 'delete'
ACTION_CHOICES = ( ACTION_CHOICES = (
(ACTION_CREATE, _("Create")), (ACTION_CREATE, _("Create")),
(ACTION_VIEW, _("View")),
(ACTION_UPDATE, _("Update")), (ACTION_UPDATE, _("Update")),
(ACTION_DELETE, _("Delete")) (ACTION_DELETE, _("Delete"))
) )

View File

@ -21,7 +21,7 @@ from jumpserver.utils import current_request
from users.models import User from users.models import User
from users.signals import post_user_change_password from users.signals import post_user_change_password
from terminal.models import Session, Command 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 . import models, serializers
from .models import OperateLog from .models import OperateLog
from orgs.utils import current_org from orgs.utils import current_org
@ -36,26 +36,6 @@ logger = get_logger(__name__)
sys_logger = get_syslogger(__name__) sys_logger = get_syslogger(__name__)
json_render = JSONRenderer() 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): class AuthBackendLabelMapping(LazyObject):
@staticmethod @staticmethod
@ -80,28 +60,6 @@ class AuthBackendLabelMapping(LazyObject):
AUTH_BACKEND_LABEL_MAPPING = AuthBackendLabelMapping() 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 = { M2M_NEED_RECORD = {
User.groups.through._meta.object_name: ( User.groups.through._meta.object_name: (
_('User and Group'), _('User and Group'),

View File

@ -1,9 +1,17 @@
import csv import csv
import codecs import codecs
from django.http import HttpResponse
from .const import DEFAULT_CITY from django.http import HttpResponse
from common.utils import validate_ip, get_ip_city 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): def get_excel_response(filename):
@ -36,3 +44,25 @@ def write_login_log(*args, **kwargs):
city = get_ip_city(ip) or DEFAULT_CITY city = get_ip_city(ip) or DEFAULT_CITY
kwargs.update({'ip': ip, 'city': city}) kwargs.update({'ip': ip, 'city': city})
UserLoginLog.objects.create(**kwargs) 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))

View File

@ -7,7 +7,6 @@ import os
import base64 import base64
import ctypes import ctypes
from django.conf import settings
from django.core.cache import cache from django.core.cache import cache
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.http import HttpResponse 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 common.const.http import PATCH
from terminal.models import EndpointRule from terminal.models import EndpointRule
from ..serializers import ( from ..serializers import (
ConnectionTokenSerializer, ConnectionTokenSecretSerializer, ConnectionTokenSerializer, ConnectionTokenSecretSerializer, SuperConnectionTokenSerializer
) )
logger = get_logger(__name__) logger = get_logger(__name__)
__all__ = ['UserConnectionTokenViewSet', 'TokenCacheMixin'] __all__ = ['UserConnectionTokenViewSet', 'UserSuperConnectionTokenViewSet', 'TokenCacheMixin']
class ClientProtocolMixin: class ClientProtocolMixin:
@ -70,8 +69,7 @@ class ClientProtocolMixin:
system_user = serializer.validated_data['system_user'] system_user = serializer.validated_data['system_user']
user = serializer.validated_data.get('user') user = serializer.validated_data.get('user')
if not user or not self.request.user.is_superuser: user = user if user else self.request.user
user = self.request.user
return asset, application, system_user, user return asset, application, system_user, user
@staticmethod @staticmethod
@ -105,7 +103,7 @@ class ClientProtocolMixin:
'bookmarktype:i': '3', 'bookmarktype:i': '3',
'use redirection server name:i': '0', 'use redirection server name:i': '0',
'smart sizing:i': '1', 'smart sizing:i': '1',
#'drivestoredirect:s': '*', # 'drivestoredirect:s': '*',
# 'domain:s': '' # 'domain:s': ''
# 'alternate shell:s:': '||MySQLWorkbench', # 'alternate shell:s:': '||MySQLWorkbench',
# 'remoteapplicationname:s': 'Firefox', # 'remoteapplicationname:s': 'Firefox',
@ -206,21 +204,6 @@ class ClientProtocolMixin:
rst = rst.decode('ascii') rst = rst.decode('ascii')
return rst 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): def get_valid_serializer(self):
if self.request.method == 'GET': if self.request.method == 'GET':
data = self.request.query_params data = self.request.query_params
@ -252,6 +235,21 @@ class ClientProtocolMixin:
} }
return data 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') @action(methods=['POST', 'GET'], detail=False, url_path='client-url')
def get_client_protocol_url(self, request, *args, **kwargs): def get_client_protocol_url(self, request, *args, **kwargs):
serializer = self.get_valid_serializer() serializer = self.get_valid_serializer()
@ -370,7 +368,7 @@ class TokenCacheMixin:
key = self.get_token_cache_key(token) key = self.get_token_cache_key(token)
return cache.ttl(key) 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) key = self.get_token_cache_key(token)
cache.set(key, value, timeout=ttl) cache.set(key, value, timeout=ttl)
@ -379,7 +377,7 @@ class TokenCacheMixin:
value = cache.get(key, None) value = cache.get(key, None)
return value 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) value = self.get_token_from_cache(token)
if value: if value:
pre_ttl = self.get_token_ttl(token) pre_ttl = self.get_token_ttl(token)
@ -397,22 +395,10 @@ class TokenCacheMixin:
return data return data
class UserConnectionTokenViewSet( class BaseUserConnectionTokenViewSet(
RootOrgViewMixin, SerializerMixin, ClientProtocolMixin, 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 @staticmethod
def check_resource_permission(user, asset, application, system_user): def check_resource_permission(user, asset, application, system_user):
@ -429,22 +415,7 @@ class UserConnectionTokenViewSet(
raise PermissionDenied(error) raise PermissionDenied(error)
return True return True
@action(methods=[PATCH], detail=False) def create_token(self, user, asset, application, system_user, ttl=5 * 60):
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')
self.check_resource_permission(user, asset, application, system_user) self.check_resource_permission(user, asset, application, system_user)
token = random_string(36) token = random_string(36)
secret = random_string(16) secret = random_string(16)
@ -489,6 +460,20 @@ class UserConnectionTokenViewSet(
} }
return Response(data, status=201) 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): def valid_token(self, token):
from users.models import User from users.models import User
from assets.models import SystemUser, Asset from assets.models import SystemUser, Asset
@ -526,3 +511,23 @@ class UserConnectionTokenViewSet(
if not value: if not value:
return Response('', status=404) return Response('', status=404)
return Response(value) 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)

View File

@ -27,8 +27,10 @@ class TokenCreateApi(AuthMixin, CreateAPIView):
def create(self, request, *args, **kwargs): def create(self, request, *args, **kwargs):
self.create_session_if_need() self.create_session_if_need()
# 如果认证没有过,检查账号密码 # 如果认证没有过,检查账号密码
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
try: 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_mfa_if_need(user)
self.check_user_login_confirm_if_need(user) self.check_user_login_confirm_if_need(user)
self.send_auth_signal(success=True, user=user) self.send_auth_signal(success=True, user=user)

View File

@ -103,21 +103,44 @@ class OIDCAuthCodeBackend(OIDCBaseBackend):
# Prepares the token payload that will be used to request an authentication token to the # Prepares the token payload that will be used to request an authentication token to the
# token endpoint of the OIDC provider. # token endpoint of the OIDC provider.
logger.debug(log_prompt.format('Prepares token payload')) 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 = { token_payload = {
'client_id': settings.AUTH_OPENID_CLIENT_ID,
'client_secret': settings.AUTH_OPENID_CLIENT_SECRET,
'grant_type': 'authorization_code', 'grant_type': 'authorization_code',
'code': code, 'code': code,
'redirect_uri': build_absolute_uri( 'redirect_uri': build_absolute_uri(
request, path=reverse(settings.AUTH_OPENID_AUTH_LOGIN_CALLBACK_URL_NAME) request, path=reverse(settings.AUTH_OPENID_AUTH_LOGIN_CALLBACK_URL_NAME)
) )
} }
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 # Prepares the token headers that will be used to request an authentication token to the
# token endpoint of the OIDC provider. # token endpoint of the OIDC provider.
logger.debug(log_prompt.format('Prepares token headers')) logger.debug(log_prompt.format('Prepares token headers'))
basic_token = "{}:{}".format(settings.AUTH_OPENID_CLIENT_ID, settings.AUTH_OPENID_CLIENT_SECRET) basic_token = "{}:{}".format(
headers = {"Authorization": "Basic {}".format(base64.b64encode(basic_token.encode()).decode())} settings.AUTH_OPENID_CLIENT_ID, settings.AUTH_OPENID_CLIENT_SECRET
)
headers = {
"Authorization": "Basic {}".format(base64.b64encode(basic_token.encode()).decode())
}
# Calls the token endpoint. # Calls the token endpoint.
logger.debug(log_prompt.format('Call the token endpoint')) logger.debug(log_prompt.format('Call the token endpoint'))

View File

@ -74,16 +74,28 @@ class PrepareRequestMixin:
return idp_settings return idp_settings
@staticmethod @staticmethod
def get_attribute_consuming_service(): def get_request_attributes():
attr_mapping = settings.SAML2_RENAME_ATTRIBUTES attr_mapping = settings.SAML2_RENAME_ATTRIBUTES or {}
if attr_mapping and isinstance(attr_mapping, dict): attr_map_reverse = {v: k for k, v in attr_mapping.items()}
attr_list = [ need_attrs = (
{ ('username', 'username', True),
"name": sp_key, ('email', 'email', True),
"friendlyName": idp_key, "isRequired": True ('name', 'name', False),
} ('phone', 'phone', False),
for idp_key, sp_key in attr_mapping.items() ('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 = { request_attribute_template = {
"attributeConsumingService": { "attributeConsumingService": {
"isDefault": False, "isDefault": False,
@ -93,8 +105,6 @@ class PrepareRequestMixin:
} }
} }
return request_attribute_template return request_attribute_template
else:
return {}
@staticmethod @staticmethod
def get_advanced_settings(): def get_advanced_settings():
@ -167,11 +177,14 @@ class PrepareRequestMixin:
def get_attributes(self, saml_instance): def get_attributes(self, saml_instance):
user_attrs = {} user_attrs = {}
attr_mapping = settings.SAML2_RENAME_ATTRIBUTES
attrs = saml_instance.get_attributes() attrs = saml_instance.get_attributes()
valid_attrs = ['username', 'name', 'email', 'comment', 'phone'] valid_attrs = ['username', 'name', 'email', 'comment', 'phone']
for attr, value in attrs.items(): for attr, value in attrs.items():
attr = attr.rsplit('/', 1)[-1] attr = attr.rsplit('/', 1)[-1]
if attr_mapping and attr_mapping.get(attr):
attr = attr_mapping.get(attr)
if attr not in valid_attrs: if attr not in valid_attrs:
continue continue
user_attrs[attr] = self.value_to_str(value) user_attrs[attr] = self.value_to_str(value)

View File

@ -1,15 +1,25 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from django import forms from django import forms
from django.conf import settings from django.conf import settings
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from captcha.fields import CaptchaField, CaptchaTextInput 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): class UserLoginForm(forms.Form):
days_auto_login = int(settings.SESSION_COOKIE_AGE / 3600 / 24) 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( username = forms.CharField(
label=_('Username'), max_length=100, label=_('Username'), max_length=100,
@ -18,7 +28,7 @@ class UserLoginForm(forms.Form):
'autofocus': 'autofocus' 'autofocus': 'autofocus'
}) })
) )
password = forms.CharField( password = EncryptedField(
label=_('Password'), widget=forms.PasswordInput, label=_('Password'), widget=forms.PasswordInput,
max_length=1024, strip=False max_length=1024, strip=False
) )

View File

@ -1,8 +1,12 @@
import base64
from django.shortcuts import redirect, reverse from django.shortcuts import redirect, reverse
from django.utils.deprecation import MiddlewareMixin from django.utils.deprecation import MiddlewareMixin
from django.http import HttpResponse from django.http import HttpResponse
from django.conf import settings from django.conf import settings
from common.utils import gen_key_pair
class MFAMiddleware: class MFAMiddleware:
""" """
@ -48,3 +52,28 @@ class SessionCookieMiddleware(MiddlewareMixin):
return response return response
response.set_cookie(key, value) response.set_cookie(key, value)
return response 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

View File

@ -23,9 +23,7 @@ from acls.models import LoginACL
from users.models import User from users.models import User
from users.utils import LoginBlockUtil, MFABlockUtils, LoginIpBlockUtil from users.utils import LoginBlockUtil, MFABlockUtils, LoginIpBlockUtil
from . import errors from . import errors
from .utils import rsa_decrypt, gen_key_pair
from .signals import post_auth_success, post_auth_failed from .signals import post_auth_success, post_auth_failed
from .const import RSA_PRIVATE_KEY, RSA_PUBLIC_KEY
logger = get_logger(__name__) logger = get_logger(__name__)
@ -58,6 +56,7 @@ def authenticate(request=None, **credentials):
for backend, backend_path in _get_backends(return_tuples=True): 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): if not backend.username_allow_authenticate(username):
continue continue
@ -91,46 +90,8 @@ def authenticate(request=None, **credentials):
auth.authenticate = authenticate auth.authenticate = authenticate
class PasswordEncryptionViewMixin: class CommonMixin:
request = None request: Request
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
def get_request_ip(self): def get_request_ip(self):
ip = '' ip = ''
@ -139,26 +100,6 @@ class PasswordEncryptionViewMixin:
ip = ip or get_request_ip(self.request) ip = ip or get_request_ip(self.request)
return ip 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): def raise_credential_error(self, error):
raise self.partial_credential_error(error=error) raise self.partial_credential_error(error=error)
@ -193,20 +134,13 @@ class CommonMixin(PasswordEncryptionViewMixin):
user.backend = self.request.session.get("auth_backend") user.backend = self.request.session.get("auth_backend")
return user return user
def get_auth_data(self, decrypt_passwd=False): def get_auth_data(self, data):
request = self.request request = self.request
if hasattr(request, 'data'):
data = request.data
else:
data = request.POST
items = ['username', 'password', 'challenge', 'public_key', 'auto_login'] items = ['username', 'password', 'challenge', 'public_key', 'auto_login']
username, password, challenge, public_key, auto_login = bulk_get(data, items, default='') username, password, challenge, public_key, auto_login = bulk_get(data, items, default='')
ip = self.get_request_ip() ip = self.get_request_ip()
self._set_partial_credential_error(username=username, ip=ip, request=request) self._set_partial_credential_error(username=username, ip=ip, request=request)
if decrypt_passwd:
password = self.get_decrypted_password()
password = password + challenge.strip() password = password + challenge.strip()
return username, password, public_key, ip, auto_login 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)) need = cache.get(self.key_prefix_captcha.format(ip))
return need return need
def check_user_auth(self, decrypt_passwd=False): def check_user_auth(self, valid_data=None):
# pre check # pre check
self.check_is_block() 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) self._check_only_allow_exists_user_auth(username)
# check auth # check auth
@ -537,11 +471,12 @@ class AuthMixin(CommonMixin, AuthPreCheckMixin, AuthACLMixin, MFAMixin, AuthPost
self.mark_password_ok(user, False) self.mark_password_ok(user, False)
return user return user
def check_user_auth_if_need(self, decrypt_passwd=False): def get_user_or_auth(self, valid_data):
request = self.request request = self.request
if not request.session.get('auth_password'): if request.session.get('auth_password'):
return self.check_user_auth(decrypt_passwd=decrypt_passwd)
return self.get_user_from_session() return self.get_user_from_session()
else:
return self.check_user_auth(valid_data)
def clear_auth_mark(self): def clear_auth_mark(self):
keys = ['auth_password', 'user_id', 'auth_confirm', 'auth_ticket_id'] keys = ['auth_password', 'user_id', 'auth_confirm', 'auth_ticket_id']

View File

@ -13,24 +13,16 @@ __all__ = [
'ConnectionTokenUserSerializer', 'ConnectionTokenFilterRuleSerializer', 'ConnectionTokenUserSerializer', 'ConnectionTokenFilterRuleSerializer',
'ConnectionTokenAssetSerializer', 'ConnectionTokenSystemUserSerializer', 'ConnectionTokenAssetSerializer', 'ConnectionTokenSystemUserSerializer',
'ConnectionTokenDomainSerializer', 'ConnectionTokenRemoteAppSerializer', 'ConnectionTokenDomainSerializer', 'ConnectionTokenRemoteAppSerializer',
'ConnectionTokenGatewaySerializer', 'ConnectionTokenSecretSerializer' 'ConnectionTokenGatewaySerializer', 'ConnectionTokenSecretSerializer',
'SuperConnectionTokenSerializer'
] ]
class ConnectionTokenSerializer(serializers.Serializer): class ConnectionTokenSerializer(serializers.Serializer):
user = serializers.CharField(max_length=128, required=False, allow_blank=True)
system_user = serializers.CharField(max_length=128, required=True) system_user = serializers.CharField(max_length=128, required=True)
asset = serializers.CharField(max_length=128, required=False) asset = serializers.CharField(max_length=128, required=False)
application = 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 @staticmethod
def validate_system_user(system_user_id): def validate_system_user(system_user_id):
from assets.models import SystemUser from assets.models import SystemUser
@ -65,6 +57,18 @@ class ConnectionTokenSerializer(serializers.Serializer):
return super().validate(attrs) 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 ConnectionTokenUserSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = User model = User
@ -114,7 +118,6 @@ class ConnectionTokenDomainSerializer(serializers.ModelSerializer):
class ConnectionTokenFilterRuleSerializer(serializers.ModelSerializer): class ConnectionTokenFilterRuleSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = CommandFilterRule model = CommandFilterRule
fields = [ fields = [

View File

@ -2,6 +2,8 @@
# #
from rest_framework import serializers from rest_framework import serializers
from common.drf.fields import EncryptedField
__all__ = [ __all__ = [
'OtpVerifySerializer', 'MFAChallengeSerializer', 'MFASelectTypeSerializer', 'OtpVerifySerializer', 'MFAChallengeSerializer', 'MFASelectTypeSerializer',
@ -10,7 +12,7 @@ __all__ = [
class PasswordVerifySerializer(serializers.Serializer): class PasswordVerifySerializer(serializers.Serializer):
password = serializers.CharField() password = EncryptedField()
class MFASelectTypeSerializer(serializers.Serializer): class MFASelectTypeSerializer(serializers.Serializer):

View File

@ -161,6 +161,7 @@
<span style="font-size: 21px;font-weight:400;color: #151515;letter-spacing: 0;">{{ JMS_TITLE }}</span> <span style="font-size: 21px;font-weight:400;color: #151515;letter-spacing: 0;">{{ JMS_TITLE }}</span>
</div> </div>
<div class="contact-form col-md-10 col-md-offset-1"> <div class="contact-form col-md-10 col-md-offset-1">
<form id="login-form" action="" method="post" role="form" novalidate="novalidate"> <form id="login-form" action="" method="post" role="form" novalidate="novalidate">
{% csrf_token %} {% csrf_token %}
<div style="line-height: 17px;margin-bottom: 20px;color: #999999;"> <div style="line-height: 17px;margin-bottom: 20px;color: #999999;">
@ -240,21 +241,13 @@
</body> </body>
{% include '_foot_js.html' %} {% 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/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> <script>
function encryptLoginPassword(password, rsaPublicKey) {
if (!password) {
return ''
}
var jsencrypt = new JSEncrypt(); //加密对象
jsencrypt.setPublicKey(rsaPublicKey); // 设置密钥
return jsencrypt.encrypt(password); //加密
}
function doLogin() { function doLogin() {
//公钥加密 //公钥加密
var rsaPublicKey = "{{ rsa_public_key }}"
var password = $('#password').val(); //明文密码 var password = $('#password').val(); //明文密码
var passwordEncrypted = encryptLoginPassword(password, rsaPublicKey) var passwordEncrypted = encryptPassword(password)
$('#password-hidden').val(passwordEncrypted); //返回给密码输入input $('#password-hidden').val(passwordEncrypted); //返回给密码输入input
$('#login-form').submit(); //post提交 $('#login-form').submit(); //post提交
} }

View File

@ -11,6 +11,7 @@ router.register('access-keys', api.AccessKeyViewSet, 'access-key')
router.register('sso', api.SSOViewSet, 'sso') router.register('sso', api.SSOViewSet, 'sso')
router.register('temp-tokens', api.TempTokenViewSet, 'temp-token') router.register('temp-tokens', api.TempTokenViewSet, 'temp-token')
router.register('connection-token', api.UserConnectionTokenViewSet, 'connection-token') router.register('connection-token', api.UserConnectionTokenViewSet, 'connection-token')
router.register('super-connection-token', api.UserSuperConnectionTokenViewSet, 'super-connection-token')
urlpatterns = [ urlpatterns = [

View File

@ -1,62 +1,22 @@
# -*- coding: utf-8 -*- # -*- 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 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 validate_ip, get_ip_city, get_request_ip
from common.utils import get_logger 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__) 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): def check_different_city_login_if_need(user, request):
if not settings.SECURITY_CHECK_DIFFERENT_CITY_LOGIN: if not settings.SECURITY_CHECK_DIFFERENT_CITY_LOGIN:
return return
ip = get_request_ip(request) or '0.0.0.0' ip = get_request_ip(request) or '0.0.0.0'
if not (ip and validate_ip(ip)): if not (ip and validate_ip(ip)):
city = DEFAULT_CITY city = DEFAULT_CITY
else: else:

View File

@ -21,6 +21,7 @@ from authentication.mixins import AuthMixin
from common.sdk.im.dingtalk import DingTalk from common.sdk.im.dingtalk import DingTalk
from common.utils.common import get_request_ip from common.utils.common import get_request_ip
from authentication.notifications import OAuthBindMessage from authentication.notifications import OAuthBindMessage
from .mixins import METAMixin
logger = get_logger(__file__) logger = get_logger(__file__)
@ -200,14 +201,17 @@ class DingTalkEnableStartView(UserVerifyPasswordView):
return success_url return success_url
class DingTalkQRLoginView(DingTalkQRMixin, View): class DingTalkQRLoginView(DingTalkQRMixin, METAMixin, View):
permission_classes = (AllowAny,) permission_classes = (AllowAny,)
def get(self, request: HttpRequest): def get(self, request: HttpRequest):
redirect_url = request.GET.get('redirect_url') redirect_url = request.GET.get('redirect_url')
redirect_uri = reverse('authentication:dingtalk-qr-login-callback', external=True) 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) url = self.get_qr_url(redirect_uri)
return HttpResponseRedirect(url) return HttpResponseRedirect(url)

View File

@ -96,7 +96,7 @@ class UserLoginView(mixins.AuthMixin, FormView):
self.request.session.delete_test_cookie() self.request.session.delete_test_cookie()
try: try:
self.check_user_auth(decrypt_passwd=True) self.check_user_auth(form.cleaned_data)
except errors.AuthFailedError as e: except errors.AuthFailedError as e:
form.add_error(None, e.msg) form.add_error(None, e.msg)
self.set_login_failed_mark() self.set_login_failed_mark()
@ -219,7 +219,7 @@ class UserLoginGuardView(mixins.AuthMixin, RedirectView):
def get_redirect_url(self, *args, **kwargs): def get_redirect_url(self, *args, **kwargs):
try: try:
user = self.check_user_auth_if_need() user = self.get_user_from_session()
self.check_user_mfa_if_need(user) self.check_user_mfa_if_need(user)
self.check_user_login_confirm_if_need(user) self.check_user_login_confirm_if_need(user)
except (errors.CredentialError, errors.SessionEmptyError) as e: except (errors.CredentialError, errors.SessionEmptyError) as e:

View File

@ -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

View File

@ -21,6 +21,7 @@ from common.utils.common import get_request_ip
from authentication import errors from authentication import errors
from authentication.mixins import AuthMixin from authentication.mixins import AuthMixin
from authentication.notifications import OAuthBindMessage from authentication.notifications import OAuthBindMessage
from .mixins import METAMixin
logger = get_logger(__file__) logger = get_logger(__file__)
@ -196,14 +197,17 @@ class WeComEnableStartView(UserVerifyPasswordView):
return success_url return success_url
class WeComQRLoginView(WeComQRMixin, View): class WeComQRLoginView(WeComQRMixin, METAMixin, View):
permission_classes = (AllowAny,) permission_classes = (AllowAny,)
def get(self, request: HttpRequest): def get(self, request: HttpRequest):
redirect_url = request.GET.get('redirect_url') redirect_url = request.GET.get('redirect_url')
redirect_uri = reverse('authentication:wecom-qr-login-callback', external=True) 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) url = self.get_qr_url(redirect_uri)
return HttpResponseRedirect(url) return HttpResponseRedirect(url)

View File

@ -5,7 +5,7 @@ from django.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.utils.encoding import force_text from django.utils.encoding import force_text
from django.core.validators import MinValueValidator, MaxValueValidator from django.core.validators import MinValueValidator, MaxValueValidator
from ..utils import signer, crypto from common.utils import signer, crypto
__all__ = [ __all__ = [

View File

@ -3,9 +3,10 @@
from rest_framework import serializers from rest_framework import serializers
from common.utils import decrypt_password
__all__ = [ __all__ = [
'ReadableHiddenField', 'ReadableHiddenField', 'EncryptedField'
] ]
@ -23,3 +24,9 @@ class ReadableHiddenField(serializers.HiddenField):
if hasattr(value, 'id'): if hasattr(value, 'id'):
return getattr(value, 'id') return getattr(value, 'id')
return value return value
class EncryptedField(serializers.CharField):
def to_internal_value(self, value):
value = super().to_internal_value(value)
return decrypt_password(value)

View File

@ -1,4 +0,0 @@
# -*- coding: utf-8 -*-
#
from .model import *

View File

@ -1,5 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from django.utils.translation import ugettext_lazy as _
from django.contrib.auth.mixins import UserPassesTestMixin from django.contrib.auth.mixins import UserPassesTestMixin
from rest_framework import permissions from rest_framework import permissions
from rest_framework.decorators import action from rest_framework.decorators import action
@ -7,8 +8,10 @@ from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from common.permissions import IsValidUser 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): class PermissionsMixin(UserPassesTestMixin):
@ -24,3 +27,35 @@ class PermissionsMixin(UserPassesTestMixin):
if not permission_class().has_permission(self.request, self): if not permission_class().has_permission(self.request, self):
return False return False
return True 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

View File

@ -1,7 +1,10 @@
import base64 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.Util.Padding import pad
from Cryptodome.Random import get_random_bytes 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 gmssl.sm4 import CryptSM4, SM4_ENCRYPT, SM4_DECRYPT
from django.conf import settings from django.conf import settings
@ -193,4 +196,66 @@ class Crypto:
continue 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() crypto = Crypto()

View File

@ -186,10 +186,27 @@ def make_signature(access_key_secret, date=None):
return content_md5(data) return content_md5(data)
def encrypt_password(password, salt=None): def encrypt_password(password, salt=None, algorithm='sha512'):
from passlib.hash import sha512_crypt from passlib.hash import sha512_crypt, des_crypt
if password:
def sha512():
return sha512_crypt.using(rounds=5000).hash(password, salt=salt) 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 return None

View File

@ -32,6 +32,10 @@ def local_now_display(fmt='%Y-%m-%d %H:%M:%S'):
return local_now().strftime(fmt) return local_now().strftime(fmt)
def local_now_date_display(fmt='%Y-%m-%d'):
return local_now().strftime(fmt)
_rest_dt_field = DateTimeField() _rest_dt_field = DateTimeField()
dt_parser = _rest_dt_field.to_internal_value dt_parser = _rest_dt_field.to_internal_value
dt_formatter = _rest_dt_field.to_representation dt_formatter = _rest_dt_field.to_representation

View File

@ -187,6 +187,8 @@ class Config(dict):
'BASE_SITE_URL': None, 'BASE_SITE_URL': None,
'AUTH_OPENID_CLIENT_ID': 'client-id', 'AUTH_OPENID_CLIENT_ID': 'client-id',
'AUTH_OPENID_CLIENT_SECRET': 'client-secret', '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_SHARE_SESSION': True,
'AUTH_OPENID_IGNORE_SSL_VERIFICATION': True, 'AUTH_OPENID_IGNORE_SSL_VERIFICATION': True,

View File

@ -55,6 +55,7 @@ AUTH_OPENID = CONFIG.AUTH_OPENID
BASE_SITE_URL = CONFIG.BASE_SITE_URL BASE_SITE_URL = CONFIG.BASE_SITE_URL
AUTH_OPENID_CLIENT_ID = CONFIG.AUTH_OPENID_CLIENT_ID AUTH_OPENID_CLIENT_ID = CONFIG.AUTH_OPENID_CLIENT_ID
AUTH_OPENID_CLIENT_SECRET = CONFIG.AUTH_OPENID_CLIENT_SECRET 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_ENDPOINT = CONFIG.AUTH_OPENID_PROVIDER_ENDPOINT
AUTH_OPENID_PROVIDER_AUTHORIZATION_ENDPOINT = CONFIG.AUTH_OPENID_PROVIDER_AUTHORIZATION_ENDPOINT AUTH_OPENID_PROVIDER_AUTHORIZATION_ENDPOINT = CONFIG.AUTH_OPENID_PROVIDER_AUTHORIZATION_ENDPOINT
AUTH_OPENID_PROVIDER_TOKEN_ENDPOINT = CONFIG.AUTH_OPENID_PROVIDER_TOKEN_ENDPOINT AUTH_OPENID_PROVIDER_TOKEN_ENDPOINT = CONFIG.AUTH_OPENID_PROVIDER_TOKEN_ENDPOINT

View File

@ -95,6 +95,7 @@ MIDDLEWARE = [
'authentication.backends.cas.middleware.CASMiddleware', 'authentication.backends.cas.middleware.CASMiddleware',
'authentication.middleware.MFAMiddleware', 'authentication.middleware.MFAMiddleware',
'authentication.middleware.SessionCookieMiddleware', 'authentication.middleware.SessionCookieMiddleware',
'authentication.middleware.EncryptedMiddleware',
'simple_history.middleware.HistoryRequestMiddleware', 'simple_history.middleware.HistoryRequestMiddleware',
] ]

View File

@ -169,3 +169,6 @@ ANNOUNCEMENT = CONFIG.ANNOUNCEMENT
# help # help
HELP_DOCUMENT_URL = CONFIG.HELP_DOCUMENT_URL HELP_DOCUMENT_URL = CONFIG.HELP_DOCUMENT_URL
HELP_SUPPORT_URL = CONFIG.HELP_SUPPORT_URL HELP_SUPPORT_URL = CONFIG.HELP_SUPPORT_URL
SESSION_RSA_PRIVATE_KEY_NAME = 'jms_private_key'
SESSION_RSA_PUBLIC_KEY_NAME = 'jms_public_key'

View File

@ -31,6 +31,7 @@ api_v1 = [
app_view_patterns = [ app_view_patterns = [
path('auth/', include('authentication.urls.view_urls'), name='auth'), path('auth/', include('authentication.urls.view_urls'), name='auth'),
path('ops/', include('ops.urls.view_urls'), name='ops'), 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'), path('common/', include('common.urls.view_urls'), name='common'),
re_path(r'flower/(?P<path>.*)', views.celery_flower_view, name='flower-view'), re_path(r'flower/(?P<path>.*)', views.celery_flower_view, name='flower-view'),
path('download/', views.ResourceDownload.as_view(), name='download'), path('download/', views.ResourceDownload.as_view(), name='download'),

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:f2c88ade4bfae213bdcdafad656af73f764e3b1b3f2b0c59aa39626e967730ca oid sha256:90a70c14fd3b546cb1ef6a96da4cd7a2acde947128bbb773527ed1845510511c
size 125911 size 127420

File diff suppressed because it is too large Load Diff

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:c75e0a1f2a047dac1374916c630bc0e8ef5ad5eea7518ffc21e93f747fc1235e oid sha256:f181a41eb4dd8a30a576f7903e5c9f519da2042e5e095ac27146d7b4002ba3df
size 104165 size 105303

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
# Generated by Django 2.2.7 on 2019-12-17 09:58 # 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 from django.db import migrations
@ -18,17 +18,17 @@ class Migration(migrations.Migration):
migrations.AlterField( migrations.AlterField(
model_name='adhoc', model_name='adhoc',
name='_become', 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( migrations.AlterField(
model_name='adhoc', model_name='adhoc',
name='_options', 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( migrations.AlterField(
model_name='adhoc', model_name='adhoc',
name='_tasks', name='_tasks',
field=common.fields.model.JsonListTextField(verbose_name='Tasks'), field=common.db.fields.JsonListTextField(verbose_name='Tasks'),
), ),
migrations.RenameField( migrations.RenameField(
model_name='adhoc', model_name='adhoc',
@ -48,12 +48,12 @@ class Migration(migrations.Migration):
migrations.AlterField( migrations.AlterField(
model_name='adhocrunhistory', model_name='adhocrunhistory',
name='_result', 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( migrations.AlterField(
model_name='adhocrunhistory', model_name='adhocrunhistory',
name='_summary', 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( migrations.RenameField(
model_name='adhocrunhistory', model_name='adhocrunhistory',

View File

@ -1,6 +1,6 @@
# Generated by Django 2.2.7 on 2020-01-06 07:34 # 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 from django.db import migrations
@ -14,6 +14,6 @@ class Migration(migrations.Migration):
migrations.AlterField( migrations.AlterField(
model_name='adhoc', model_name='adhoc',
name='become', 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'),
), ),
] ]

View File

@ -9,11 +9,11 @@ from celery import current_task
from django.db import models from django.db import models
from django.conf import settings from django.conf import settings
from django.utils import timezone 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 import get_logger, lazyproperty
from common.utils.translate import translate_value from common.utils.translate import translate_value
from common.fields.model import ( from common.db.fields import (
JsonListTextField, JsonDictCharField, EncryptJsonDictCharField, JsonListTextField, JsonDictCharField, EncryptJsonDictCharField,
JsonDictTextField, JsonDictTextField,
) )

View File

@ -8,6 +8,7 @@ from ..filters import RoleFilter
from ..serializers import RoleSerializer, RoleUserSerializer from ..serializers import RoleSerializer, RoleUserSerializer
from ..models import Role, SystemRole, OrgRole from ..models import Role, SystemRole, OrgRole
from .permission import PermissionViewSet from .permission import PermissionViewSet
from common.mixins.api import PaginatedResponseMixin
__all__ = [ __all__ = [
'RoleViewSet', 'SystemRoleViewSet', 'OrgRoleViewSet', 'RoleViewSet', 'SystemRoleViewSet', 'OrgRoleViewSet',
@ -15,7 +16,7 @@ __all__ = [
] ]
class RoleViewSet(JMSModelViewSet): class RoleViewSet(PaginatedResponseMixin, JMSModelViewSet):
queryset = Role.objects.all() queryset = Role.objects.all()
serializer_classes = { serializer_classes = {
'default': RoleSerializer, 'default': RoleSerializer,
@ -54,7 +55,7 @@ class RoleViewSet(JMSModelViewSet):
def users(self, *args, **kwargs): def users(self, *args, **kwargs):
role = self.get_object() role = self.get_object()
queryset = role.users queryset = role.users
return self.get_paginated_response_with_query_set(queryset) return self.get_paginated_response_from_queryset(queryset)
class SystemRoleViewSet(RoleViewSet): class SystemRoleViewSet(RoleViewSet):

View File

@ -91,7 +91,7 @@ exclude_permissions = (
only_system_permissions = ( only_system_permissions = (
('assets', 'platform', '*', '*'), ('assets', 'platform', 'add,change,delete', 'platform'),
('users', 'user', 'delete', 'user'), ('users', 'user', 'delete', 'user'),
('rbac', 'role', 'delete,add,change', 'role'), ('rbac', 'role', 'delete,add,change', 'role'),
('rbac', 'systemrole', '*', '*'), ('rbac', 'systemrole', '*', '*'),

View File

@ -1,6 +1,7 @@
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.db import models from django.db import models
from django.db.models import Q from django.db.models import Q
from django.conf import settings
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from rest_framework.serializers 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] system_bindings = [b for b in bindings if b.scope == Role.Scope.system.value]
# 工作台仅限于自己加入的组织 # 工作台仅限于自己加入的组织
if perm == 'rbac.view_workbench': if perm == 'rbac.view_workbench':
all_orgs = user.orgs.all() all_orgs = user.orgs.all().distinct()
else: else:
all_orgs = Organization.objects.all() all_orgs = Organization.objects.all()
if not settings.XPACK_ENABLED:
all_orgs = all_orgs.filter(id=Organization.DEFAULT_ID)
# 有系统级别的绑定,就代表在所有组织有这个权限 # 有系统级别的绑定,就代表在所有组织有这个权限
if system_bindings: if system_bindings:
orgs = all_orgs orgs = all_orgs

View File

@ -1,5 +1,5 @@
from rest_framework import generics from rest_framework import generics
from rest_framework.permissions import AllowAny from rest_framework.permissions import AllowAny, IsAuthenticated
from django.conf import settings from django.conf import settings
from jumpserver.utils import has_valid_xpack_license, get_xpack_license_info 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__) logger = get_logger(__name__)
__all__ = ['PublicSettingApi'] __all__ = ['PublicSettingApi', 'OpenPublicSettingApi']
class PublicSettingApi(generics.RetrieveAPIView): class OpenPublicSettingApi(generics.RetrieveAPIView):
permission_classes = (AllowAny,) permission_classes = (AllowAny,)
serializer_class = serializers.PublicSettingSerializer serializer_class = serializers.PublicSettingSerializer
@ -28,17 +28,22 @@ class PublicSettingApi(generics.RetrieveAPIView):
return interface['login_title'] return interface['login_title']
def get_object(self): def get_object(self):
instance = { return {
"data": { "XPACK_ENABLED": settings.XPACK_ENABLED,
# Security "LOGIN_TITLE": self.get_login_title(),
"WINDOWS_SKIP_ALL_MANUAL_PASSWORD": settings.WINDOWS_SKIP_ALL_MANUAL_PASSWORD, "LOGO_URLS": self.get_logo_urls(),
"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, class PublicSettingApi(OpenPublicSettingApi):
"SECURITY_COMMAND_EXECUTION": settings.SECURITY_COMMAND_EXECUTION, permission_classes = (IsAuthenticated,)
"SECURITY_PASSWORD_EXPIRATION_TIME": settings.SECURITY_PASSWORD_EXPIRATION_TIME, serializer_class = serializers.PrivateSettingSerializer
"SECURITY_LUNA_REMEMBER_AUTH": settings.SECURITY_LUNA_REMEMBER_AUTH,
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": { "PASSWORD_RULE": {
'SECURITY_PASSWORD_MIN_LENGTH': settings.SECURITY_PASSWORD_MIN_LENGTH, 'SECURITY_PASSWORD_MIN_LENGTH': settings.SECURITY_PASSWORD_MIN_LENGTH,
'SECURITY_ADMIN_USER_PASSWORD_MIN_LENGTH': settings.SECURITY_ADMIN_USER_PASSWORD_MIN_LENGTH, 'SECURITY_ADMIN_USER_PASSWORD_MIN_LENGTH': settings.SECURITY_ADMIN_USER_PASSWORD_MIN_LENGTH,
@ -47,29 +52,16 @@ class PublicSettingApi(generics.RetrieveAPIView):
'SECURITY_PASSWORD_NUMBER': settings.SECURITY_PASSWORD_NUMBER, 'SECURITY_PASSWORD_NUMBER': settings.SECURITY_PASSWORD_NUMBER,
'SECURITY_PASSWORD_SPECIAL_CHAR': settings.SECURITY_PASSWORD_SPECIAL_CHAR, 'SECURITY_PASSWORD_SPECIAL_CHAR': settings.SECURITY_PASSWORD_SPECIAL_CHAR,
}, },
'SECURITY_WATERMARK_ENABLED': settings.SECURITY_WATERMARK_ENABLED, })
'SECURITY_SESSION_SHARE': settings.SECURITY_SESSION_SHARE,
# XPACK serializer = self.serializer_class()
"XPACK_ENABLED": settings.XPACK_ENABLED, field_names = list(serializer.fields.keys())
"XPACK_LICENSE_IS_VALID": has_valid_xpack_license(), for name in field_names:
"XPACK_LICENSE_INFO": get_xpack_license_info(), if name in values:
# Performance continue
"LOGIN_TITLE": self.get_login_title(), # 提前把异常爆出来
"LOGO_URLS": self.get_logo_urls(), values[name] = getattr(settings, name)
"HELP_DOCUMENT_URL": settings.HELP_DOCUMENT_URL, return values
"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 instance

View File

@ -1,6 +1,8 @@
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers from rest_framework import serializers
from common.drf.fields import EncryptedField
__all__ = [ __all__ = [
'LDAPTestConfigSerializer', 'LDAPUserSerializer', 'LDAPTestLoginSerializer', 'LDAPTestConfigSerializer', 'LDAPUserSerializer', 'LDAPTestLoginSerializer',
'LDAPSettingSerializer', 'LDAPSettingSerializer',
@ -20,7 +22,7 @@ class LDAPTestConfigSerializer(serializers.Serializer):
class LDAPTestLoginSerializer(serializers.Serializer): class LDAPTestLoginSerializer(serializers.Serializer):
username = serializers.CharField(max_length=1024, required=True) 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): class LDAPUserSerializer(serializers.Serializer):
@ -28,6 +30,7 @@ class LDAPUserSerializer(serializers.Serializer):
username = serializers.CharField() username = serializers.CharField()
name = serializers.CharField() name = serializers.CharField()
email = serializers.CharField() email = serializers.CharField()
groups = serializers.ListField(child=serializers.CharField(), default=[])
existing = serializers.BooleanField(read_only=True) existing = serializers.BooleanField(read_only=True)

View File

@ -17,6 +17,14 @@ class CommonSettingSerializer(serializers.Serializer):
AUTH_OPENID_CLIENT_SECRET = serializers.CharField( AUTH_OPENID_CLIENT_SECRET = serializers.CharField(
required=False, max_length=1024, write_only=True, label=_('Client Secret') 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_SHARE_SESSION = serializers.BooleanField(required=False, label=_('Share session'))
AUTH_OPENID_IGNORE_SSL_VERIFICATION = serializers.BooleanField( AUTH_OPENID_IGNORE_SSL_VERIFICATION = serializers.BooleanField(
required=False, label=_('Ignore ssl verification') required=False, label=_('Ignore ssl verification')

View File

@ -3,8 +3,41 @@
from rest_framework import serializers from rest_framework import serializers
__all__ = ['PublicSettingSerializer'] __all__ = ['PublicSettingSerializer', 'PrivateSettingSerializer']
class PublicSettingSerializer(serializers.Serializer): 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()

View File

@ -35,4 +35,4 @@ class TerminalSettingSerializer(serializers.Serializer):
) )
TERMINAL_MAGNUS_ENABLED = serializers.BooleanField(label=_("Enable database proxy")) TERMINAL_MAGNUS_ENABLED = serializers.BooleanField(label=_("Enable database proxy"))
XRDP_ENABLED = serializers.BooleanField(label=_("Enable XRDP")) 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"))

View File

@ -22,4 +22,5 @@ urlpatterns = [
path('setting/', api.SettingsApi.as_view(), name='settings-setting'), path('setting/', api.SettingsApi.as_view(), name='settings-setting'),
path('public/', api.PublicSettingApi.as_view(), name='public-setting'), path('public/', api.PublicSettingApi.as_view(), name='public-setting'),
path('public/open/', api.OpenPublicSettingApi.as_view(), name='open-public-setting'),
] ]

View File

@ -1,6 +1,7 @@
# coding: utf-8 # coding: utf-8
# #
import os
import json import json
from ldap3 import Server, Connection, SIMPLE from ldap3 import Server, Connection, SIMPLE
from ldap3.core.exceptions import ( from ldap3.core.exceptions import (
@ -21,12 +22,14 @@ from django.conf import settings
from django.core.cache import cache from django.core.cache import cache
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from copy import deepcopy 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.const import LDAP_AD_ACCOUNT_DISABLE
from common.utils import timeit, get_logger from common.utils import timeit, get_logger
from common.db.utils import close_old_connections from common.db.utils import close_old_connections
from users.utils import construct_user_email 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 from authentication.backends.ldap import LDAPAuthorizationBackend, LDAPUser
logger = get_logger(__file__) logger = get_logger(__file__)
@ -185,6 +188,12 @@ class LDAPServerUtil(object):
if attr == 'is_active' and mapping.lower() == 'useraccountcontrol' \ if attr == 'is_active' and mapping.lower() == 'useraccountcontrol' \
and value: and value:
value = int(value) & LDAP_AD_ACCOUNT_DISABLE != LDAP_AD_ACCOUNT_DISABLE 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 user[attr] = value.strip() if isinstance(value, str) else value
return user return user
@ -244,10 +253,13 @@ class LDAPCacheUtil(object):
if user['username'] in self.search_users if user['username'] in self.search_users
] ]
elif self.search_value: elif self.search_value:
filter_users = [ filter_users = []
user for user in users for u in users:
if self.search_value.lower() in ','.join(user.values()).lower() 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: else:
filter_users = users filter_users = users
return filter_users return filter_users
@ -345,6 +357,7 @@ class LDAPSyncUtil(object):
class LDAPImportUtil(object): class LDAPImportUtil(object):
user_group_name_prefix = 'AD '
def __init__(self): def __init__(self):
pass pass
@ -365,23 +378,58 @@ class LDAPImportUtil(object):
) )
return obj, created 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): def perform_import(self, users, org=None):
logger.info('Start perform import ldap users, count: {}'.format(len(users))) logger.info('Start perform import ldap users, count: {}'.format(len(users)))
errors = [] errors = []
objs = [] objs = []
group_users_mapper = defaultdict(set)
for user in users: for user in users:
groups = user.pop('groups', [])
try: try:
obj, created = self.update_or_create(user) obj, created = self.update_or_create(user)
objs.append(obj) objs.append(obj)
except Exception as e: except Exception as e:
errors.append({user['username']: str(e)}) errors.append({user['username']: str(e)})
logger.error(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: if not org:
return return
if org.is_root(): if org.is_root():
return return
# add user to org
for obj in objs: for obj in objs:
org.add_member(obj) 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') logger.info('End perform import ldap users')
return errors return errors

View File

@ -1501,3 +1501,90 @@ function getStatusIcon(status, mapping, title) {
} }
return icon; 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

View File

@ -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;"}

View File

@ -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

View File

@ -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;"}

View File

@ -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;"}

View File

@ -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;"}

View File

@ -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.Sentrys `on_detect` callback.\n * Passes the zmodem.js Detection object.\n *\n * - event `zmodemRetract` - fired on Zmodem.Sentrys `on_retract` callback.\n *\n * Youll need to provide logic to handle uploads and downloads.\n * See zmodem.jss documentation for more details.\n *\n * **IMPORTANT:** After you confirm() a zmodem.js Detection, if you have\n * used the `attach` or `terminado` addons, youll 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.jss demo the first message was\n // always text even if the rest were binary. While that\n // may be specific to xterm.jss demo, ultimately we\n // should reject anything that isnt 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

View File

@ -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>

View File

@ -15,45 +15,33 @@ p {
</style> </style>
<div style="margin: 0 200px"> <div style="margin: 0 200px">
<div class="group"> <div class="group">
<h2>JumpServer {% trans 'Client' %}</h2> <h2>JumpServer {% trans 'Client' %} v1.1.5</h2>
<p> <p>
{% trans 'JumpServer Client, currently used to launch the client, now only support launch RDP SSH client, The Telnet client will next' %} {% 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> </p>
<ul> <ul>
<li> <a href="/download/JumpServer-Client-Installer.msi">Windows {% 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.dmg">macOS {% trans 'Client' %}</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> </ul>
</div> </div>
<div class="group"> <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> <p>
{% trans 'macOS needs to download the client to connect RDP asset, which comes with Windows' %} {% trans 'macOS needs to download the client to connect RDP asset, which comes with Windows' %}
</p> </p>
<ul> <ul>
<li><a href="/download/Microsoft_Remote_Desktop_10.6.7_installer.pkg">Microsoft_Remote_Desktop_10.6.7_installer.pkg</a></li> <li><a href="/download/Microsoft_Remote_Desktop_10.6.7_installer.pkg">microsoft-remote-desktop-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>
</ul> </ul>
</div> </div>
{% if XPACK_ENABLED %} {% if XPACK_ENABLED %}
<div class="group"> <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> <p>{% trans 'Jmservisor is the program used to pull up remote applications in Windows Remote Application publisher' %}</p>
<ul> <ul>
<li><a href="/download/Jmservisor.msi">Jmservisor</a></li> <li><a href="/download/Jmservisor.msi">jmservisor.msi</a></li>
</ul> </ul>
</div> </div>
{% endif %} {% endif %}

View File

@ -1,11 +1,12 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
import pytz
import inspect
from datetime import datetime from datetime import datetime
from functools import reduce, partial from functools import reduce, partial
from itertools import groupby from itertools import groupby
import pytz
from uuid import UUID from uuid import UUID
import inspect
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.db.models import QuerySet as DJQuerySet 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.common import lazyproperty
from common.utils import get_logger from common.utils import get_logger
from common.utils.timezone import local_now_date_display, utc_now
from common.exceptions import JMSException from common.exceptions import JMSException
from .models import AbstractSessionCommand from .models import AbstractSessionCommand
@ -28,12 +30,13 @@ class InvalidElasticsearch(JMSException):
class CommandStore(object): class CommandStore(object):
def __init__(self, config): 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.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.exact_fields = {}
self.match_fields = {} self.match_fields = {}
hosts = config.get("HOSTS")
kwargs = config.get("OTHER", {})
ignore_verify_certs = kwargs.pop('IGNORE_VERIFY_CERTS', False) ignore_verify_certs = kwargs.pop('IGNORE_VERIFY_CERTS', False)
if ignore_verify_certs: if ignore_verify_certs:
@ -50,6 +53,17 @@ class CommandStore(object):
else: else:
self.match_fields.update(may_exact_fields) 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): def is_new_index_type(self):
if not self.ping(timeout=3): if not self.ping(timeout=3):
return False return False
@ -101,10 +115,17 @@ class CommandStore(object):
else: else:
mappings = {'mappings': {'properties': properties}} mappings = {'mappings': {'properties': properties}}
if self.is_index_by_date:
mappings['aliases'] = {
self.query_index: {}
}
try: try:
self.es.indices.create(self.index, body=mappings) self.es.indices.create(self.index, body=mappings)
return return
except RequestError as e: except RequestError as e:
if e.error == 'resource_already_exists_exception':
logger.warning(e)
else:
logger.exception(e) logger.exception(e)
@staticmethod @staticmethod
@ -141,7 +162,7 @@ class CommandStore(object):
body = self.get_query_body(**query) body = self.get_query_body(**query)
data = self.es.search( 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 sort=sort
) )
source_data = [] source_data = []
@ -154,7 +175,7 @@ class CommandStore(object):
def count(self, **query): def count(self, **query):
body = self.get_query_body(**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"] return data["count"]
def __getattr__(self, item): def __getattr__(self, item):

View File

@ -1,6 +1,6 @@
# Generated by Django 2.2.5 on 2019-11-22 10:07 # 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 from django.db import migrations, models
import uuid import uuid
@ -21,7 +21,7 @@ class Migration(migrations.Migration):
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
('name', models.CharField(max_length=32, unique=True, verbose_name='Name')), ('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')), ('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')), ('comment', models.TextField(blank=True, default='', max_length=128, verbose_name='Comment')),
], ],
options={ options={
@ -37,7 +37,7 @@ class Migration(migrations.Migration):
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
('name', models.CharField(max_length=32, unique=True, verbose_name='Name')), ('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')), ('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')), ('comment', models.TextField(blank=True, default='', max_length=128, verbose_name='Comment')),
], ],
options={ options={

View File

@ -1,7 +1,6 @@
# Generated by Django 3.1.14 on 2022-04-12 07:39 # Generated by Django 3.1.14 on 2022-04-12 07:39
import copy import common.db.fields
import common.fields.model
import django.core.validators import django.core.validators
from django.db import migrations, models from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
@ -83,13 +82,13 @@ class Migration(migrations.Migration):
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('name', models.CharField(max_length=128, unique=True, verbose_name='Name')), ('name', models.CharField(max_length=128, unique=True, verbose_name='Name')),
('host', models.CharField(max_length=256, verbose_name='Host', blank=True)), ('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')), ('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.fields.model.PortField(default=80, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(65535)], verbose_name='HTTP 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.fields.model.PortField(default=2222, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(65535)], verbose_name='SSH 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.fields.model.PortField(default=3389, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(65535)], verbose_name='RDP 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.fields.model.PortField(default=33060, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(65535)], verbose_name='MySQL 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.fields.model.PortField(default=33061, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(65535)], verbose_name='MariaDB 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.fields.model.PortField(default=54320, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(65535)], verbose_name='PostgreSQL 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')), ('comment', models.TextField(blank=True, default='', verbose_name='Comment')),
], ],
options={ options={

View File

@ -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'),
),
]

View File

@ -2,7 +2,7 @@ from django.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.core.validators import MinValueValidator, MaxValueValidator from django.core.validators import MinValueValidator, MaxValueValidator
from common.db.models import JMSModel 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 from common.utils.ip import contains_ip
@ -17,6 +17,7 @@ class Endpoint(JMSModel):
mysql_port = PortField(default=33060, verbose_name=_('MySQL Port')) mysql_port = PortField(default=33060, verbose_name=_('MySQL Port'))
mariadb_port = PortField(default=33061, verbose_name=_('MariaDB Port')) mariadb_port = PortField(default=33061, verbose_name=_('MariaDB Port'))
postgresql_port = PortField(default=54320, verbose_name=_('PostgreSQL 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')) comment = models.TextField(default='', blank=True, verbose_name=_('Comment'))
default_id = '00000000-0000-0000-0000-000000000001' default_id = '00000000-0000-0000-0000-000000000001'

View File

@ -1,6 +1,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import os import os
from importlib import import_module from importlib import import_module
import jms_storage import jms_storage
@ -9,7 +10,8 @@ from django.utils.translation import ugettext_lazy as _
from django.conf import settings from django.conf import settings
from common.mixins import CommonModelMixin from common.mixins import CommonModelMixin
from common.utils import get_logger 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.backends import TYPE_ENGINE_MAPPING
from .terminal import Terminal from .terminal import Terminal
from .command import Command from .command import Command
@ -63,6 +65,10 @@ class CommandStorage(CommonStorageModelMixin, CommonModelMixin):
def type_server(self): def type_server(self):
return self.type == const.CommandStorageTypeChoices.server.value return self.type == const.CommandStorageTypeChoices.server.value
@property
def type_es(self):
return self.type == const.CommandStorageTypeChoices.es.value
@property @property
def type_null_or_server(self): def type_null_or_server(self):
return self.type_null or self.type_server return self.type_null or self.type_server
@ -73,6 +79,18 @@ class CommandStorage(CommonStorageModelMixin, CommonModelMixin):
config.update({'TYPE': self.type}) config.update({'TYPE': self.type})
return config 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): def is_valid(self):
if self.type_null_or_server: if self.type_null_or_server:
return True return True
@ -89,17 +107,21 @@ class CommandStorage(CommonStorageModelMixin, CommonModelMixin):
return Terminal.objects.filter(command_storage=self.name, is_deleted=False).exists() return Terminal.objects.filter(command_storage=self.name, is_deleted=False).exists()
def get_command_queryset(self): def get_command_queryset(self):
if self.type_server: if self.type_null:
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.none()
if self.type_server:
return Command.objects.all()
if self.type in TYPE_ENGINE_MAPPING:
engine_mod = import_module(TYPE_ENGINE_MAPPING[self.type]) engine_mod = import_module(TYPE_ENGINE_MAPPING[self.type])
qs = engine_mod.QuerySet(self.config) qs = engine_mod.QuerySet(self.config)
qs.model = Command 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, def save(self, force_insert=False, force_update=False, using=None,
update_fields=None): update_fields=None):
super().save() super().save()

View File

@ -68,7 +68,7 @@ class StorageMixin:
def get_command_storage_config(self): def get_command_storage_config(self):
s = self.get_command_storage() s = self.get_command_storage()
if s: if s:
config = s.config config = s.valid_config
else: else:
config = settings.DEFAULT_TERMINAL_COMMAND_STORAGE config = settings.DEFAULT_TERMINAL_COMMAND_STORAGE
return config return config

View File

@ -16,7 +16,7 @@ class EndpointSerializer(BulkModelSerializer):
'host', 'host',
'https_port', 'http_port', 'ssh_port', 'https_port', 'http_port', 'ssh_port',
'rdp_port', 'mysql_port', 'mariadb_port', 'rdp_port', 'mysql_port', 'mariadb_port',
'postgresql_port', 'postgresql_port', 'redis_port',
] ]
fields = fields_mini + fields_small + [ fields = fields_mini + fields_small + [
'comment', 'date_created', 'date_updated', 'created_by' 'comment', 'date_created', 'date_updated', 'created_by'
@ -29,6 +29,7 @@ class EndpointSerializer(BulkModelSerializer):
'mysql_port': {'default': 33060}, 'mysql_port': {'default': 33060},
'mariadb_port': {'default': 33061}, 'mariadb_port': {'default': 33061},
'postgresql_port': {'default': 54320}, 'postgresql_port': {'default': 54320},
'redis_port': {'default': 63790},
} }

View File

@ -155,6 +155,10 @@ class CommandStorageTypeESSerializer(serializers.Serializer):
child=serializers.CharField(validators=[command_storage_es_host_format_validator]), child=serializers.CharField(validators=[command_storage_es_host_format_validator]),
label=_('Hosts'), help_text=_(hosts_help_text), allow_null=True 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( INDEX = serializers.CharField(
max_length=1024, default='jumpserver', label=_('Index'), allow_null=True max_length=1024, default='jumpserver', label=_('Index'), allow_null=True
) )

View File

@ -1,6 +1,6 @@
# Generated by Django 2.2.5 on 2019-11-15 06:57 # 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.conf import settings
from django.db import migrations, models from django.db import migrations, models
import django.db.models.deletion 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')), ('user_display', models.CharField(max_length=128, verbose_name='User display name')),
('title', models.CharField(max_length=256, verbose_name='Title')), ('title', models.CharField(max_length=256, verbose_name='Title')),
('body', models.TextField(verbose_name='Body')), ('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')), ('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')), ('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')), ('type', models.CharField(choices=[('general', 'General'), ('login_confirm', 'Login confirm')], default='general', max_length=16, verbose_name='Type')),

View File

@ -1,13 +1,15 @@
from urllib.parse import urljoin from urllib.parse import urljoin
from django.conf import settings 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.template.loader import render_to_string
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from . import const
from notifications.notifications import UserMessage from notifications.notifications import UserMessage
from common.utils import get_logger from common.utils import get_logger, random_string
from .models import Ticket from .models import Ticket
from . import const
logger = get_logger(__file__) logger = get_logger(__file__)
@ -57,6 +59,13 @@ class TicketAppliedToAssignee(BaseTicketMessage):
def __init__(self, user, ticket): def __init__(self, user, ticket):
self.ticket = ticket self.ticket = ticket
super().__init__(user) super().__init__(user)
self._token = None
@property
def token(self):
if self._token is None:
self._token = random_string(32)
return self._token
@property @property
def content_title(self): def content_title(self):
@ -71,6 +80,29 @@ class TicketAppliedToAssignee(BaseTicketMessage):
) )
return title 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 @classmethod
def gen_test_msg(cls): def gen_test_msg(cls):
from .models import Ticket from .models import Ticket

View File

@ -9,7 +9,13 @@
<br> <br>
<div> <div>
<a href="{{ ticket_detail_url }}" target="_blank"> <a href="{{ ticket_detail_url }}" target="_blank">
{% trans 'Click here to review' %} {% trans 'View details' %}
</a> </a>
<br>
{% if ticket_approval_url %}
<a href="{{ ticket_approval_url }}" target="_blank">
{% trans 'Direct approval' %}
</a>
{% endif %}
</div> </div>
</div> </div>

View File

@ -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 %}

View File

@ -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'),
]

View File

@ -0,0 +1 @@
from .approve import *

View File

@ -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)

View File

@ -5,6 +5,7 @@ from django.utils.translation import gettext_lazy as _
from captcha.fields import CaptchaField from captcha.fields import CaptchaField
from common.utils import validate_ssh_public_key from common.utils import validate_ssh_public_key
from authentication.forms import EncryptedField
from ..models import User from ..models import User
@ -17,7 +18,7 @@ __all__ = [
class UserCheckPasswordForm(forms.Form): class UserCheckPasswordForm(forms.Form):
password = forms.CharField( password = EncryptedField(
label=_('Password'), widget=forms.PasswordInput, label=_('Password'), widget=forms.PasswordInput,
max_length=1024, strip=False max_length=1024, strip=False
) )
@ -77,12 +78,12 @@ UserFirstLoginFinishForm.verbose_name = _("Finish")
class UserTokenResetPasswordForm(forms.Form): class UserTokenResetPasswordForm(forms.Form):
new_password = forms.CharField( new_password = EncryptedField(
min_length=5, max_length=128, min_length=5, max_length=128,
widget=forms.PasswordInput, widget=forms.PasswordInput,
label=_("New password") label=_("New password")
) )
confirm_password = forms.CharField( confirm_password = EncryptedField(
min_length=5, max_length=128, min_length=5, max_length=128,
widget=forms.PasswordInput, widget=forms.PasswordInput,
label=_("Confirm password") label=_("Confirm password")
@ -103,7 +104,7 @@ class UserForgotPasswordForm(forms.Form):
class UserPasswordForm(UserTokenResetPasswordForm): class UserPasswordForm(UserTokenResetPasswordForm):
old_password = forms.CharField( old_password = EncryptedField(
max_length=128, widget=forms.PasswordInput, max_length=128, widget=forms.PasswordInput,
label=_("Old password") label=_("Old password")
) )

View File

@ -1,6 +1,6 @@
# Generated by Django 2.1.7 on 2019-06-25 03:04 # 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 from django.db import migrations, models
@ -14,17 +14,17 @@ class Migration(migrations.Migration):
migrations.AlterField( migrations.AlterField(
model_name='user', model_name='user',
name='_otp_secret_key', 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( migrations.AlterField(
model_name='user', model_name='user',
name='_private_key', 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( migrations.AlterField(
model_name='user', model_name='user',
name='_public_key', 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( migrations.AlterField(
model_name='user', model_name='user',

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