Compare commits

...

11 Commits

Author SHA1 Message Date
fit2bot
4de90a72e6 feat: Update v3.10.17 2025-01-02 15:30:45 +08:00
Bai
2e7bd076f4 fix: limit connect method xpack 2024-12-25 16:27:51 +08:00
Bai
11f6fe0bf9 fix: system org 2024-12-25 15:34:19 +08:00
wangruidong
ae94648e80 fix: Add type check for secure command execution 2024-12-24 15:58:15 +08:00
jiangweidong
94e08f3d96 perf: The command amount does not record operation logs 2024-12-20 14:59:53 +08:00
Bai
8bedef92f0 fix: api prometheus count 2024-12-20 10:57:42 +08:00
jiangweidong
e5bb28231a perf: Oauth2.0 support two methods for passing authentication credentials. 2024-12-19 14:27:29 +08:00
jiangweidong
b5aeb24ae9 perf: create account add activity log 2024-12-18 15:53:08 +08:00
feng
674ea7142f perf: The entire organization can view activity log 2024-12-11 16:21:39 +08:00
fit2bot
5ab7b99b9d perf: add encrypted configuration API (#14633)
* perf: 添加加密配置API

* perf: modify url

---------

Co-authored-by: Eric <xplzv@126.com>
2024-12-11 11:42:34 +08:00
Bai
9cd163c99d fix: when oidc enabled and use_state user login raise 400 2024-12-06 16:26:59 +08:00
21 changed files with 86 additions and 67 deletions

1
GITSHA Normal file
View File

@@ -0,0 +1 @@
2e7bd076f464350f8fad93aaa2c22e1cd6c84b45

View File

@@ -62,7 +62,7 @@ def create_accounts_activities(account, action='create'):
@receiver(post_save, sender=Account)
def on_account_create_by_template(sender, instance, created=False, **kwargs):
if not created or instance.source != Source.TEMPLATE:
if not created:
return
push_accounts_if_need.delay(accounts=(instance,))
create_accounts_activities(instance, action='create')

View File

@@ -189,9 +189,13 @@ class ResourceActivityAPIView(generics.ListAPIView):
'id', 'datetime', 'r_detail', 'r_detail_id',
'r_user', 'r_action', 'r_type'
)
org_q = Q(org_id=Organization.SYSTEM_ID) | Q(org_id=current_org.id)
if resource_id:
org_q |= Q(org_id='') | Q(org_id=Organization.ROOT_ID)
org_q = Q()
if not current_org.is_root():
org_q = Q(org_id=Organization.SYSTEM_ID) | Q(org_id=current_org.id)
if resource_id:
org_q |= Q(org_id='') | Q(org_id=Organization.ROOT_ID)
with tmp_to_root_org():
qs1 = self.get_operate_log_qs(fields, limit, org_q, resource_id=resource_id)
qs2 = self.get_activity_log_qs(fields, limit, org_q, resource_id=resource_id)

View File

@@ -14,7 +14,7 @@ from audits.handler import (
create_or_update_operate_log, get_instance_dict_from_cache
)
from audits.utils import model_to_dict_for_operate_log as model_to_dict
from common.const.signals import POST_ADD, POST_REMOVE, POST_CLEAR, SKIP_SIGNAL
from common.const.signals import POST_ADD, POST_REMOVE, POST_CLEAR, OP_LOG_SKIP_SIGNAL
from common.signals import django_ready
from jumpserver.utils import current_request
from ..const import MODELS_NEED_RECORD, ActionChoices
@@ -77,7 +77,7 @@ def signal_of_operate_log_whether_continue(
condition = True
if not instance:
condition = False
if instance and getattr(instance, SKIP_SIGNAL, False):
if instance and getattr(instance, OP_LOG_SKIP_SIGNAL, False):
condition = False
# 不记录组件的操作日志
user = current_request.user if current_request else None

View File

@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
#
import base64
import requests
from django.utils.translation import gettext_lazy as _
@@ -67,14 +68,6 @@ class OAuth2Backend(JMSModelBackend):
response_data = response_data['data']
return response_data
@staticmethod
def get_query_dict(response_data, query_dict):
query_dict.update({
'uid': response_data.get('uid', ''),
'access_token': response_data.get('access_token', '')
})
return query_dict
def authenticate(self, request, code=None, **kwargs):
log_prompt = "Process authenticate [OAuth2Backend]: {}"
logger.debug(log_prompt.format('Start'))
@@ -83,29 +76,31 @@ class OAuth2Backend(JMSModelBackend):
return None
query_dict = {
'client_id': settings.AUTH_OAUTH2_CLIENT_ID,
'client_secret': settings.AUTH_OAUTH2_CLIENT_SECRET,
'grant_type': 'authorization_code',
'code': code,
'grant_type': 'authorization_code', 'code': code,
'redirect_uri': build_absolute_uri(
request, path=reverse(settings.AUTH_OAUTH2_AUTH_LOGIN_CALLBACK_URL_NAME)
)
}
if '?' in settings.AUTH_OAUTH2_ACCESS_TOKEN_ENDPOINT:
separator = '&'
else:
separator = '?'
separator = '&' if '?' in settings.AUTH_OAUTH2_ACCESS_TOKEN_ENDPOINT else '?'
access_token_url = '{url}{separator}{query}'.format(
url=settings.AUTH_OAUTH2_ACCESS_TOKEN_ENDPOINT, separator=separator, query=urlencode(query_dict)
url=settings.AUTH_OAUTH2_ACCESS_TOKEN_ENDPOINT,
separator=separator, query=urlencode(query_dict)
)
# token_method -> get, post(post_data), post_json
token_method = settings.AUTH_OAUTH2_ACCESS_TOKEN_METHOD.lower()
logger.debug(log_prompt.format('Call the access token endpoint[method: %s]' % token_method))
encoded_credentials = base64.b64encode(
f"{settings.AUTH_OAUTH2_CLIENT_ID}:{settings.AUTH_OAUTH2_CLIENT_SECRET}".encode()
).decode()
headers = {
'Accept': 'application/json'
'Accept': 'application/json', 'Authorization': f'Basic {encoded_credentials}'
}
if token_method.startswith('post'):
body_key = 'json' if token_method.endswith('json') else 'data'
query_dict.update({
'client_id': settings.AUTH_OAUTH2_CLIENT_ID,
'client_secret': settings.AUTH_OAUTH2_CLIENT_SECRET,
})
access_token_response = requests.post(
access_token_url, headers=headers, **{body_key: query_dict}
)
@@ -121,22 +116,12 @@ class OAuth2Backend(JMSModelBackend):
logger.error(log_prompt.format(error))
return None
query_dict = self.get_query_dict(response_data, query_dict)
headers = {
'Accept': 'application/json',
'Authorization': 'Bearer {}'.format(response_data.get('access_token', ''))
}
logger.debug(log_prompt.format('Get userinfo endpoint'))
if '?' in settings.AUTH_OAUTH2_PROVIDER_USERINFO_ENDPOINT:
separator = '&'
else:
separator = '?'
userinfo_url = '{url}{separator}{query}'.format(
url=settings.AUTH_OAUTH2_PROVIDER_USERINFO_ENDPOINT, separator=separator,
query=urlencode(query_dict)
)
userinfo_url = settings.AUTH_OAUTH2_PROVIDER_USERINFO_ENDPOINT
userinfo_response = requests.get(userinfo_url, headers=headers)
try:
userinfo_response.raise_for_status()

View File

@@ -107,7 +107,7 @@ class OIDCAuthCodeBackend(OIDCBaseBackend):
# parameters because we won't be able to get a valid token for the user in that case.
if (state is None and settings.AUTH_OPENID_USE_STATE) or code is None:
logger.debug(log_prompt.format('Authorization code or state value is missing'))
raise SuspiciousOperation('Authorization code or state value is missing')
return
# Prepares the token payload that will be used to request an authentication token to the
# token endpoint of the OIDC provider.
@@ -165,7 +165,7 @@ class OIDCAuthCodeBackend(OIDCBaseBackend):
error = "Json token response error, token response " \
"content is: {}, error is: {}".format(token_response.content, str(e))
logger.debug(log_prompt.format(error))
raise ParseError(error)
return
# Validates the token.
logger.debug(log_prompt.format('Validate ID Token'))
@@ -206,7 +206,7 @@ class OIDCAuthCodeBackend(OIDCBaseBackend):
error = "Json claims response error, claims response " \
"content is: {}, error is: {}".format(claims_response.content, str(e))
logger.debug(log_prompt.format(error))
raise ParseError(error)
return
logger.debug(log_prompt.format('Get or create user from claims'))
user, created = self.get_or_create_user_from_claims(request, claims)

View File

@@ -16,4 +16,4 @@ POST_CLEAR = 'post_clear'
POST_PREFIX = 'post'
PRE_PREFIX = 'pre'
SKIP_SIGNAL = 'skip_signal'
OP_LOG_SKIP_SIGNAL = 'operate_log_skip_signal'

View File

@@ -17,7 +17,7 @@ from django.db.models import F, ExpressionWrapper, CASCADE
from django.db.models import QuerySet
from django.utils.translation import gettext_lazy as _
from ..const.signals import SKIP_SIGNAL
from ..const.signals import OP_LOG_SKIP_SIGNAL
class ChoicesMixin:
@@ -82,7 +82,7 @@ def CASCADE_SIGNAL_SKIP(collector, field, sub_objs, using):
# 级联删除时,操作日志标记不保存,以免用户混淆
try:
for obj in sub_objs:
setattr(obj, SKIP_SIGNAL, True)
setattr(obj, OP_LOG_SKIP_SIGNAL, True)
except:
pass

View File

@@ -8,7 +8,7 @@ __all__ = ['BASE_DIR', 'PROJECT_DIR', 'VERSION', 'CONFIG']
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
PROJECT_DIR = os.path.dirname(BASE_DIR)
VERSION = '2.0.0'
VERSION = 'v3.10.17'
CONFIG = ConfigManager.load_user_config()

View File

@@ -12,6 +12,7 @@ from django.conf import settings
from common.const.crontab import CRONTAB_AT_AM_TWO
from common.utils import get_logger, get_object_or_none, get_log_keep_day
from ops.celery import app
from ops.const import Types
from orgs.utils import tmp_to_org, tmp_to_root_org
from .celery.decorator import (
register_as_period_task, after_app_ready_start, after_app_shutdown_clean_periodic
@@ -52,14 +53,13 @@ def _run_ops_job_execution(execution):
activity_callback=job_task_activity_callback
)
def run_ops_job(job_id):
if not settings.SECURITY_COMMAND_EXECUTION:
return
with tmp_to_root_org():
job = get_object_or_none(Job, id=job_id)
if not job:
logger.error("Did not get the execution: {}".format(job_id))
return
if not settings.SECURITY_COMMAND_EXECUTION and job.type != Types.upload_file:
return
with tmp_to_org(job.org):
execution = job.create_execution()
execution.creator = job.creator
@@ -80,14 +80,14 @@ def job_execution_task_activity_callback(self, execution_id, *args, **kwargs):
activity_callback=job_execution_task_activity_callback
)
def run_ops_job_execution(execution_id, **kwargs):
if not settings.SECURITY_COMMAND_EXECUTION:
return
with tmp_to_root_org():
execution = get_object_or_none(JobExecution, id=execution_id)
if not execution:
logger.error("Did not get the execution: {}".format(execution_id))
return
if not settings.SECURITY_COMMAND_EXECUTION and execution.job.type != Types.upload_file:
return
_run_ops_job_execution(execution)

View File

@@ -38,7 +38,7 @@ class OrgSerializer(ModelSerializer):
class CurrentOrgSerializer(ModelSerializer):
class Meta:
model = Organization
fields = ['id', 'name', 'is_default', 'is_root', 'comment']
fields = ['id', 'name', 'is_default', 'is_root', 'is_system', 'comment']
class CurrentOrgDefault:

View File

@@ -24,13 +24,13 @@ def get_org_from_request(request):
# 其次session
if not oid:
oid = request.session.get("oid")
if oid and oid.lower() == 'default':
return Organization.default()
if oid and oid.lower() == 'root':
return Organization.root()
if oid and oid.lower() == 'system':
return Organization.system()
@@ -39,14 +39,14 @@ def get_org_from_request(request):
if org and org.internal:
# 内置组织直接返回
return org
if not settings.XPACK_ENABLED:
# 社区版用户只能使用默认组织
return Organization.default()
if not org and request.user.is_authenticated:
# 企业版用户优先从自己有权限的组织中获取
org = request.user.orgs.first()
org = request.user.orgs.exclude(id=Organization.SYSTEM_ID).first()
if not org:
org = Organization.default()

View File

@@ -1,24 +1,26 @@
# -*- coding: utf-8 -*-
#
import logging
from django.db.models import Q
from django.conf import settings
from django.db.models import Q
from django.utils.translation import gettext_lazy as _
from django_filters import rest_framework as filters
from rest_framework import generics
from rest_framework import status
from rest_framework.views import APIView, Response
from django_filters import rest_framework as filters
from common.drf.filters import BaseFilterSet
from common.api import JMSBulkModelViewSet
from common.drf.filters import BaseFilterSet
from common.exceptions import JMSException
from common.permissions import WithBootstrapToken
from common.permissions import WithBootstrapToken, IsServiceAccount
from jumpserver.conf import ConfigCrypto
from terminal import serializers
from terminal.models import Terminal
__all__ = [
'TerminalViewSet', 'TerminalConfig',
'TerminalRegistrationApi',
'TerminalRegistrationApi', 'EncryptedTerminalConfig'
]
logger = logging.getLogger(__file__)
@@ -89,3 +91,17 @@ class TerminalRegistrationApi(generics.CreateAPIView):
return Response(data=data, status=status.HTTP_400_BAD_REQUEST)
return super().create(request, *args, **kwargs)
class EncryptedTerminalConfig(generics.CreateAPIView):
serializer_class = serializers.EncryptedConfigSerializer
permission_classes = [IsServiceAccount]
http_method_names = ['post']
def post(self, request, *args, **kwargs):
serializer = self.serializer_class(data=request.data)
serializer.is_valid(raise_exception=True)
encrypt_key = serializer.validated_data['secret_encrypt_key']
encrypted_value = serializer.validated_data['encrypted_value']
config_crypto = ConfigCrypto(encrypt_key)
value = config_crypto.decrypt(encrypted_value)
return Response(data={'value': value}, status=200)

View File

@@ -105,6 +105,8 @@ class AppletMethod:
if not has_applet_hosts:
return methods
applets = Applet.objects.filter(is_active=True)
if not settings.XPACK_LICENSE_IS_VALID:
applets = applets.filter(builtin=True)
for applet in applets:
for protocol in applet.protocols:
methods[protocol].append({
@@ -125,6 +127,8 @@ class VirtualAppMethod:
methods = defaultdict(list)
if not getattr(settings, 'VIRTUAL_APP_ENABLED'):
return methods
if not settings.XPACK_LICENSE_IS_VALID:
return methods
virtual_apps = VirtualApp.objects.filter(is_active=True)
for virtual_app in virtual_apps:
for protocol in virtual_app.protocols:

View File

@@ -5,7 +5,7 @@ from django.core.cache import cache
from django.db import models
from django.utils.translation import gettext_lazy as _
from common.const.signals import SKIP_SIGNAL
from common.const.signals import OP_LOG_SKIP_SIGNAL
from common.db.models import JMSBaseModel
from common.utils import get_logger, lazyproperty
from orgs.utils import tmp_to_root_org
@@ -152,7 +152,7 @@ class Terminal(StorageMixin, TerminalStatusMixin, JMSBaseModel):
def delete(self, using=None, keep_parents=False):
if self.user:
setattr(self.user, SKIP_SIGNAL, True)
setattr(self.user, OP_LOG_SKIP_SIGNAL, True)
self.user.delete()
self.name = self.name + '_' + uuid.uuid4().hex[:8]
self.user = None

View File

@@ -12,6 +12,7 @@ from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from assets.models import Asset
from common.const import OP_LOG_SKIP_SIGNAL
from common.utils import get_object_or_none, lazyproperty
from orgs.mixins.models import OrgModelMixin
from terminal.backends import get_multi_command_storage
@@ -203,6 +204,7 @@ class Session(OrgModelMixin):
if self.need_update_cmd_amount:
cmd_amount = self.compute_command_amount()
self.cmd_amount = cmd_amount
setattr(self, OP_LOG_SKIP_SIGNAL, True)
self.save()
elif self.need_compute_cmd_amount:
cmd_amount = self.compute_command_amount()

View File

@@ -147,3 +147,8 @@ class ConnectMethodSerializer(serializers.Serializer):
type = serializers.CharField(max_length=128)
endpoint_protocol = serializers.CharField(max_length=128)
component = serializers.CharField(max_length=128)
class EncryptedConfigSerializer(serializers.Serializer):
secret_encrypt_key = serializers.CharField(max_length=128)
encrypted_value = serializers.CharField(max_length=128)

View File

@@ -54,6 +54,7 @@ urlpatterns = [
# components
path('components/metrics/', api.ComponentsMetricsAPIView.as_view(), name='components-metrics'),
path('components/connect-methods/', api.ConnectMethodListApi.as_view(), name='connect-methods'),
path('encrypted-config/', api.EncryptedTerminalConfig.as_view(), name='encrypted-terminal-config'),
]
urlpatterns += router.urls

View File

@@ -97,10 +97,10 @@ class ComponentsPrometheusMetricsUtil(TypedComponentsStatusMetricsUtil):
def convert_status_metrics(metrics):
return {
'any': metrics['total'],
'normal': metrics['normal'],
'high': metrics['high'],
'critical': metrics['critical'],
'offline': metrics['offline']
'normal': len(metrics['normal']),
'high': len(metrics['high']),
'critical': len(metrics['critical']),
'offline': len(metrics['offline'])
}
def get_component_status_metrics(self):
@@ -112,8 +112,8 @@ class ComponentsPrometheusMetricsUtil(TypedComponentsStatusMetricsUtil):
tp = metric['type']
prometheus_metrics.append(f'## 组件: {tp}')
status_metrics = self.convert_status_metrics(metric)
for status, value in status_metrics.items():
metric_text = status_metric_text % (tp, status, value)
for status, count in status_metrics.items():
metric_text = status_metric_text % (tp, status, count)
prometheus_metrics.append(metric_text)
return prometheus_metrics

View File

@@ -12,6 +12,7 @@ class UserOrgSerializer(serializers.Serializer):
id = serializers.CharField()
name = serializers.CharField()
is_default = serializers.BooleanField(read_only=True)
is_system = serializers.BooleanField(read_only=True)
is_root = serializers.BooleanField(read_only=True)

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "jumpserver"
version = "v3.9"
version = "v3.10.17"
description = "广受欢迎的开源堡垒机"
authors = ["ibuler <ibuler@qq.com>"]
license = "GPLv3"