mirror of
https://github.com/jumpserver/jumpserver.git
synced 2025-12-15 08:32:48 +00:00
Compare commits
19 Commits
revert-162
...
v3.7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3b30e329ab | ||
|
|
3dc853c7f2 | ||
|
|
932aed97d3 | ||
|
|
c464e95a21 | ||
|
|
0f0af19d49 | ||
|
|
6a54ff8714 | ||
|
|
1e0489bb96 | ||
|
|
980ddcd833 | ||
|
|
1ec2cd6087 | ||
|
|
257ef464f7 | ||
|
|
9cd3cc1da0 | ||
|
|
9520a23f4c | ||
|
|
a4ff9dace3 | ||
|
|
7a1214f358 | ||
|
|
2fdc4c613f | ||
|
|
df6525933a | ||
|
|
6aef27c824 | ||
|
|
26c3409d84 | ||
|
|
3c54c82ce9 |
@@ -36,9 +36,11 @@ ARG TOOLS=" \
|
||||
curl \
|
||||
default-libmysqlclient-dev \
|
||||
default-mysql-client \
|
||||
iputils-ping \
|
||||
locales \
|
||||
nmap \
|
||||
openssh-client \
|
||||
patch \
|
||||
sshpass \
|
||||
telnet \
|
||||
vim \
|
||||
|
||||
@@ -6,8 +6,8 @@ from rest_framework.status import HTTP_200_OK
|
||||
|
||||
from accounts import serializers
|
||||
from accounts.filters import AccountFilterSet
|
||||
from accounts.models import Account
|
||||
from accounts.mixins import AccountRecordViewLogMixin
|
||||
from accounts.models import Account
|
||||
from assets.models import Asset, Node
|
||||
from common.api.mixin import ExtraFilterFieldsMixin
|
||||
from common.permissions import UserConfirmation, ConfirmType, IsValidUser
|
||||
@@ -57,19 +57,19 @@ class AccountViewSet(OrgBulkModelViewSet):
|
||||
permission_classes=[IsValidUser]
|
||||
)
|
||||
def username_suggestions(self, request, *args, **kwargs):
|
||||
asset_ids = request.data.get('assets')
|
||||
node_ids = request.data.get('nodes')
|
||||
username = request.data.get('username')
|
||||
asset_ids = request.data.get('assets', [])
|
||||
node_ids = request.data.get('nodes', [])
|
||||
username = request.data.get('username', '')
|
||||
|
||||
assets = Asset.objects.all()
|
||||
if asset_ids:
|
||||
assets = assets.filter(id__in=asset_ids)
|
||||
accounts = Account.objects.all()
|
||||
if node_ids:
|
||||
nodes = Node.objects.filter(id__in=node_ids)
|
||||
node_asset_ids = Node.get_nodes_all_assets(*nodes).values_list('id', flat=True)
|
||||
assets = assets.filter(id__in=set(list(asset_ids) + list(node_asset_ids)))
|
||||
asset_ids.extend(node_asset_ids)
|
||||
|
||||
if asset_ids:
|
||||
accounts = accounts.filter(asset_id__in=list(set(asset_ids)))
|
||||
|
||||
accounts = Account.objects.filter(asset__in=assets)
|
||||
if username:
|
||||
accounts = accounts.filter(username__icontains=username)
|
||||
usernames = list(accounts.values_list('username', flat=True).distinct()[:10])
|
||||
|
||||
@@ -13,11 +13,11 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name='changesecretautomation',
|
||||
name='secret_strategy',
|
||||
field=models.CharField(choices=[('specific', 'Specific password'), ('random', 'Random')], default='specific', max_length=16, verbose_name='Secret strategy'),
|
||||
field=models.CharField(choices=[('specific', 'Specific secret'), ('random', 'Random generate')], default='specific', max_length=16, verbose_name='Secret strategy'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='pushaccountautomation',
|
||||
name='secret_strategy',
|
||||
field=models.CharField(choices=[('specific', 'Specific password'), ('random', 'Random')], default='specific', max_length=16, verbose_name='Secret strategy'),
|
||||
field=models.CharField(choices=[('specific', 'Specific secret'), ('random', 'Random generate')], default='specific', max_length=16, verbose_name='Secret strategy'),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -29,6 +29,6 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name='accounttemplate',
|
||||
name='secret_strategy',
|
||||
field=models.CharField(choices=[('specific', 'Specific password'), ('random', 'Random')], default='specific', max_length=16, verbose_name='Secret strategy'),
|
||||
field=models.CharField(choices=[('specific', 'Specific secret'), ('random', 'Random generate')], default='specific', max_length=16, verbose_name='Secret strategy'),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -37,8 +37,9 @@ class VaultManagerMixin(models.Manager):
|
||||
post_save.send(obj.__class__, instance=obj, created=True)
|
||||
return objs
|
||||
|
||||
def bulk_update(self, objs, batch_size=None, ignore_conflicts=False):
|
||||
objs = super().bulk_update(objs, batch_size=batch_size, ignore_conflicts=ignore_conflicts)
|
||||
def bulk_update(self, objs, fields, batch_size=None):
|
||||
fields = ["_secret" if field == "secret" else field for field in fields]
|
||||
super().bulk_update(objs, fields, batch_size=batch_size)
|
||||
for obj in objs:
|
||||
post_save.send(obj.__class__, instance=obj, created=False)
|
||||
return objs
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from accounts.const import SecretStrategy, SecretType
|
||||
from accounts.models import AccountTemplate, Account
|
||||
from accounts.utils import SecretGenerator
|
||||
from common.serializers import SecretReadableMixin
|
||||
from common.serializers.fields import ObjectRelatedField
|
||||
from .base import BaseAccountSerializer
|
||||
@@ -55,9 +57,20 @@ class AccountTemplateSerializer(BaseAccountSerializer):
|
||||
accounts = Account.objects.filter(**query_data)
|
||||
instance.bulk_sync_account_secret(accounts, self.context['request'].user.id)
|
||||
|
||||
@staticmethod
|
||||
def generate_secret(attrs):
|
||||
secret_type = attrs.get('secret_type', SecretType.PASSWORD)
|
||||
secret_strategy = attrs.get('secret_strategy', SecretStrategy.custom)
|
||||
password_rules = attrs.get('password_rules')
|
||||
if secret_strategy != SecretStrategy.random:
|
||||
return
|
||||
generator = SecretGenerator(secret_strategy, secret_type, password_rules)
|
||||
attrs['secret'] = generator.get_secret()
|
||||
|
||||
def validate(self, attrs):
|
||||
self._is_sync_account = attrs.pop('is_sync_account', None)
|
||||
attrs = super().validate(attrs)
|
||||
self.generate_secret(attrs)
|
||||
return attrs
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
|
||||
@@ -107,8 +107,9 @@ def create_app_nodes(apps, org_id):
|
||||
'key': next_key, 'value': name, 'parent_key': parent_key,
|
||||
'full_value': full_value, 'org_id': org_id
|
||||
}
|
||||
node, created = node_model.objects.get_or_create(
|
||||
node, __ = node_model.objects.get_or_create(
|
||||
defaults=defaults, value=name, org_id=org_id,
|
||||
parent_key=parent_key
|
||||
)
|
||||
node.parent = parent
|
||||
return node
|
||||
|
||||
@@ -25,7 +25,7 @@ def migrate_asset_accounts(apps, schema_editor):
|
||||
count += len(auth_books)
|
||||
# auth book 和 account 相同的属性
|
||||
same_attrs = [
|
||||
'id', 'username', 'comment', 'date_created', 'date_updated',
|
||||
'username', 'comment', 'date_created', 'date_updated',
|
||||
'created_by', 'asset_id', 'org_id',
|
||||
]
|
||||
# 认证的属性,可能是 auth_book 的,可能是 system_user 的
|
||||
|
||||
@@ -402,12 +402,7 @@ class NodeAssetsMixin(NodeAllAssetsMappingMixin):
|
||||
return Asset.objects.filter(q).distinct()
|
||||
|
||||
def get_assets_amount(self):
|
||||
q = Q(node__key__startswith=f'{self.key}:') | Q(node__key=self.key)
|
||||
return self.assets.through.objects.filter(q).count()
|
||||
|
||||
def get_assets_account_by_children(self):
|
||||
children = self.get_all_children().values_list()
|
||||
return self.assets.through.objects.filter(node_id__in=children).count()
|
||||
return self.get_all_assets().count()
|
||||
|
||||
@classmethod
|
||||
def get_node_all_assets_by_key_v2(cls, key):
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
from common.permissions import ServiceAccountSignaturePermission
|
||||
from .base import JMSBaseAuthBackend
|
||||
|
||||
UserModel = get_user_model()
|
||||
@@ -18,6 +19,10 @@ class PublicKeyAuthBackend(JMSBaseAuthBackend):
|
||||
def authenticate(self, request, username=None, public_key=None, **kwargs):
|
||||
if not public_key:
|
||||
return None
|
||||
|
||||
permission = ServiceAccountSignaturePermission()
|
||||
if not permission.has_permission(request, None):
|
||||
return None
|
||||
if username is None:
|
||||
username = kwargs.get(UserModel.USERNAME_FIELD)
|
||||
try:
|
||||
@@ -26,7 +31,7 @@ class PublicKeyAuthBackend(JMSBaseAuthBackend):
|
||||
return None
|
||||
else:
|
||||
if user.check_public_key(public_key) and \
|
||||
self.user_can_authenticate(user):
|
||||
self.user_can_authenticate(user):
|
||||
return user
|
||||
|
||||
def get_user(self, user_id):
|
||||
|
||||
@@ -310,12 +310,6 @@ class UserLoginGuardView(mixins.AuthMixin, RedirectView):
|
||||
age = self.request.session.get_expiry_age()
|
||||
self.request.session.set_expiry(age)
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
response = super().get(request, *args, **kwargs)
|
||||
if request.user.is_authenticated:
|
||||
response.set_cookie('jms_username', request.user.username)
|
||||
return response
|
||||
|
||||
def get_redirect_url(self, *args, **kwargs):
|
||||
try:
|
||||
user = self.get_user_from_session()
|
||||
|
||||
@@ -86,3 +86,38 @@ class UserConfirmation(permissions.BasePermission):
|
||||
min_level = ConfirmType.values.index(confirm_type) + 1
|
||||
name = 'UserConfirmationLevel{}TTL{}'.format(min_level, ttl)
|
||||
return type(name, (cls,), {'min_level': min_level, 'ttl': ttl, 'confirm_type': confirm_type})
|
||||
|
||||
|
||||
class ServiceAccountSignaturePermission(permissions.BasePermission):
|
||||
def has_permission(self, request, view):
|
||||
from authentication.models import AccessKey
|
||||
from common.utils.crypto import get_aes_crypto
|
||||
signature = request.META.get('HTTP_X_JMS_SVC', '')
|
||||
if not signature or not signature.startswith('Sign'):
|
||||
return False
|
||||
data = signature[4:].strip()
|
||||
if not data or ':' not in data:
|
||||
return False
|
||||
ak_id, time_sign = data.split(':', 1)
|
||||
if not ak_id or not time_sign:
|
||||
return False
|
||||
ak = AccessKey.objects.filter(id=ak_id).first()
|
||||
if not ak or not ak.is_active:
|
||||
return False
|
||||
if not ak.user or not ak.user.is_active or not ak.user.is_service_account:
|
||||
return False
|
||||
aes = get_aes_crypto(str(ak.secret).replace('-', ''), mode='ECB')
|
||||
try:
|
||||
timestamp = aes.decrypt(time_sign)
|
||||
if not timestamp or not timestamp.isdigit():
|
||||
return False
|
||||
timestamp = int(timestamp)
|
||||
interval = abs(int(time.time()) - timestamp)
|
||||
if interval > 30:
|
||||
return False
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def has_object_permission(self, request, view, obj):
|
||||
return False
|
||||
|
||||
@@ -36,7 +36,9 @@ def send_mail_async(*args, **kwargs):
|
||||
args[0] = (settings.EMAIL_SUBJECT_PREFIX or '') + args[0]
|
||||
from_email = settings.EMAIL_FROM or settings.EMAIL_HOST_USER
|
||||
args.insert(2, from_email)
|
||||
args = tuple(args)
|
||||
|
||||
args[3] = [mail for mail in args[3] if mail != 'admin@mycomany.com']
|
||||
args = tuple(args)
|
||||
|
||||
try:
|
||||
return send_mail(*args, **kwargs)
|
||||
@@ -50,6 +52,7 @@ def send_mail_attachment_async(subject, message, recipient_list, attachment_list
|
||||
attachment_list = []
|
||||
from_email = settings.EMAIL_FROM or settings.EMAIL_HOST_USER
|
||||
subject = (settings.EMAIL_SUBJECT_PREFIX or '') + subject
|
||||
recipient_list = [mail for mail in recipient_list if mail != 'admin@mycomany.com']
|
||||
email = EmailMultiAlternatives(
|
||||
subject=subject,
|
||||
body=message,
|
||||
|
||||
@@ -26,6 +26,7 @@ class SendAndVerifyCodeUtil(object):
|
||||
self.target = target
|
||||
self.backend = backend
|
||||
self.key = key or self.KEY_TMPL.format(target)
|
||||
self.verify_key = self.key + '_verify'
|
||||
self.timeout = settings.VERIFY_CODE_TTL if timeout is None else timeout
|
||||
self.other_args = kwargs
|
||||
|
||||
@@ -47,6 +48,11 @@ class SendAndVerifyCodeUtil(object):
|
||||
raise
|
||||
|
||||
def verify(self, code):
|
||||
times = cache.get(self.verify_key, 0)
|
||||
if times >= 3:
|
||||
self.__clear()
|
||||
raise CodeExpired
|
||||
cache.set(self.verify_key, times + 1, timeout=self.timeout)
|
||||
right = cache.get(self.key)
|
||||
if not right:
|
||||
raise CodeExpired
|
||||
@@ -59,6 +65,7 @@ class SendAndVerifyCodeUtil(object):
|
||||
|
||||
def __clear(self):
|
||||
cache.delete(self.key)
|
||||
cache.delete(self.verify_key)
|
||||
|
||||
def __ttl(self):
|
||||
return cache.ttl(self.key)
|
||||
|
||||
@@ -12,7 +12,6 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name='applet',
|
||||
name='edition',
|
||||
field=models.CharField(choices=[('community', 'Community'), ('enterprise', 'Enterprise')],
|
||||
default='community', max_length=128, verbose_name='Edition'),
|
||||
field=models.CharField(choices=[('community', 'Community edition'), ('enterprise', 'Enterprise')], default='community', max_length=128, verbose_name='Edition'),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -162,7 +162,7 @@ class Applet(JMSBaseModel):
|
||||
for host_id in using_host_ids.values():
|
||||
counts[host_id] += 1
|
||||
|
||||
hosts = list(sorted(hosts, key=lambda h: counts[h.id]))
|
||||
hosts = list(sorted(hosts, key=lambda h: counts[str(h.id)]))
|
||||
return hosts[0]
|
||||
|
||||
def select_host(self, user, asset):
|
||||
|
||||
@@ -114,6 +114,11 @@ class UserForgotPasswordView(FormView):
|
||||
target = form.cleaned_data[form_type]
|
||||
code = form.cleaned_data['code']
|
||||
|
||||
query_key = form_type
|
||||
if form_type == 'sms':
|
||||
query_key = 'phone'
|
||||
target = target.lstrip('+')
|
||||
|
||||
try:
|
||||
sender_util = SendAndVerifyCodeUtil(target, backend=form_type)
|
||||
sender_util.verify(code)
|
||||
@@ -121,7 +126,6 @@ class UserForgotPasswordView(FormView):
|
||||
form.add_error('code', str(e))
|
||||
return super().form_invalid(form)
|
||||
|
||||
query_key = 'phone' if form_type == 'sms' else form_type
|
||||
user = get_object_or_none(User, **{'username': username, query_key: target})
|
||||
if not user:
|
||||
form.add_error('code', _('No user matched'))
|
||||
|
||||
18
poetry.lock
generated
18
poetry.lock
generated
@@ -1,4 +1,4 @@
|
||||
# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand.
|
||||
# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "adal"
|
||||
@@ -2655,14 +2655,8 @@ files = [
|
||||
[package.dependencies]
|
||||
google-auth = ">=2.14.1,<3.0.dev0"
|
||||
googleapis-common-protos = ">=1.56.2,<2.0.dev0"
|
||||
grpcio = [
|
||||
{version = ">=1.33.2,<2.0dev", optional = true, markers = "extra == \"grpc\""},
|
||||
{version = ">=1.49.1,<2.0dev", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""},
|
||||
]
|
||||
grpcio-status = [
|
||||
{version = ">=1.33.2,<2.0.dev0", optional = true, markers = "extra == \"grpc\""},
|
||||
{version = ">=1.49.1,<2.0.dev0", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""},
|
||||
]
|
||||
grpcio = {version = ">=1.49.1,<2.0dev", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""}
|
||||
grpcio-status = {version = ">=1.49.1,<2.0.dev0", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""}
|
||||
protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<5.0.0.dev0"
|
||||
requests = ">=2.18.0,<3.0.0.dev0"
|
||||
|
||||
@@ -3345,12 +3339,12 @@ reference = "tsinghua"
|
||||
|
||||
[[package]]
|
||||
name = "jms-storage"
|
||||
version = "0.0.51"
|
||||
version = "0.0.52"
|
||||
description = "Jumpserver storage python sdk tools"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "jms-storage-0.0.51.tar.gz", hash = "sha256:47a50ac4d952a21693b0e2f926f42fa0d02bc1fa8e507a8284059743b2b81911"},
|
||||
{file = "jms-storage-0.0.52.tar.gz", hash = "sha256:15303281a1d1a3ac24a5a9fb0d78abda3aa1f752590aab867923647a485ccfbd"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -7276,4 +7270,4 @@ reference = "tsinghua"
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.11"
|
||||
content-hash = "b7d8e793f247e91e1bd22404559ed495e619fe691fa92712cacf7bd146c0eb8f"
|
||||
content-hash = "bf72acdbac5e62c239033fd629835ad30788a3fcb07ac9a2dc2f15f321da6c30"
|
||||
|
||||
@@ -47,7 +47,7 @@ pynacl = "1.5.0"
|
||||
python-dateutil = "2.8.2"
|
||||
pyyaml = "6.0.1"
|
||||
requests = "2.31.0"
|
||||
jms-storage = "0.0.51"
|
||||
jms-storage = "0.0.52"
|
||||
simplejson = "3.19.1"
|
||||
six = "1.16.0"
|
||||
sshtunnel = "0.4.0"
|
||||
|
||||
Reference in New Issue
Block a user