Compare commits

..

103 Commits

Author SHA1 Message Date
Jiangjie Bai
a005c78653 Merge pull request #16449 from jumpserver/dev
v4.10.14
2025-12-18 17:28:48 +08:00
Bai
6d92ddf2d3 perf: redirect confirm page only support back to index page 2025-12-18 16:16:06 +08:00
feng
296112bed8 perf: check_api 2025-12-17 17:58:39 +08:00
Bai
50a857e182 perf: click confirm, add interval 30s auto close tab 2025-12-17 17:24:59 +08:00
Bai
cfca2cdf5c perf: set redirect-confirm page, add close button, user refresh redirect to indexPage 2025-12-17 16:54:55 +08:00
Bai
599797299f perf: add oauth2_provider.log to logger 2025-12-17 15:19:03 +08:00
fit2bot
eee7333745 fix: Integrations services failed to call api (#16441)
Co-authored-by: wangruidong <940853815@qq.com>
2025-12-16 11:34:07 +08:00
wangruidong
e7ed098503 fix: Remove Ansible Receptor configuration and related code 2025-12-16 11:33:29 +08:00
ibuler
29fb518156 perf: change word spell 2025-12-16 11:33:07 +08:00
wangruidong
e16e2cb964 perf: Update translations for Diff and Export filtered in Chinese 2025-12-16 11:32:44 +08:00
fit2bot
a41b6b41e0 perf: Add custom chat model (#16439)
Co-authored-by: feng <1304903146@qq.com>
2025-12-16 09:18:56 +08:00
feng
933e6e4c15 perf: Vault save maximum recursion depth exceeded 2025-12-15 15:56:54 +08:00
feng
46384e19b5 perf: client version 2025-12-12 15:36:37 +08:00
fit2bot
e388a7efa0 perf: chat ai custom model (#16428) 2025-12-12 15:28:20 +08:00
Chenyang Shen
4bc345542c Merge pull request #16424 from jumpserver/pr@dev@feat_redis_lock_on_piico
feat: add redis lock on piico card init
2025-12-12 15:00:57 +08:00
Aaron3S
829e9b1497 feat: add redis lock on picco card init 2025-12-11 18:38:21 +08:00
fit2bot
8e703d306c feat: Add permission check for reading account secrets based on system settings (#16337) 2025-12-11 16:42:10 +08:00
wangruidong
af908480f4 fix: Add '/media/' to the list of whitelisted URLs for MFA login 2025-12-11 16:39:10 +08:00
wangruidong
fc2d4ae751 fix: Add user validity check to authentication process 2025-12-11 16:38:32 +08:00
wangruidong
cde5fb7a3e perf: Include 'id' in search and filter fields for AutomationExecution 2025-12-11 16:36:51 +08:00
Eric
19da95c6fb perf: update vnc protocol 2025-12-10 15:53:48 +08:00
Eric
bc4f29a6f6 perf: support virtual_app vnc client 2025-12-10 15:26:06 +08:00
Chenyang Shen
2b1fdb937b Merge pull request #16404 from jumpserver/pr@dev@feat_reset_key_store
feat: reset piico device after open device
2025-12-09 15:16:41 +08:00
Aaron3S
1e754546f1 feat: reset piico device after open device 2025-12-09 14:47:37 +08:00
Bai
2ec71feafc perf: rbac oauth2_provider perms i18n 2025-12-09 10:17:34 +08:00
Bai
02e8905330 perf: redirect/confirm page and i18n 2025-12-08 18:43:04 +08:00
Bai
8d68f5589b perf: redirect/confirm page and i18n 2025-12-08 18:43:04 +08:00
Bai
4df13fc384 perf: redirect/confirm page and i18n 2025-12-08 18:40:12 +08:00
Bai
78c1162028 perf: when DEBUG_DEV=True, allow OAUTH2_PROVIDER redirect_url localhost listen 2025-12-08 16:42:07 +08:00
Bai
14c2512b45 fix: accesskey authentication user is None error 2025-12-08 15:06:47 +08:00
Bai
d6d7072da5 perf: request.GET.copy() to dict(), because copy() returned values is list [] 2025-12-08 12:50:49 +08:00
fit2bot
993bc36c5e perf: handling the next parameter propagation issue in third-party authentication flows (#16395)
* perf: remove call client old- method via ?next=client

* feat: add 2 decorators for login-get and login-callback-get to set next_page and get next_page from session

* perf: code style

* perf: handling the next parameter propagation issue in third-party authentication flows

* perf: request.GET.dict() to copy()

* perf: style import

---------

Co-authored-by: Bai <baijiangjie@gmail.com>
2025-12-08 12:34:32 +08:00
fit2bot
ecff2ea07e perf: move oauth2_provider api auth_backend to the end, and while accesstoken_backend not user do not raise execption, go on next bakcned auth (#16393)
* perf: move oauth2_provider api auth_backend to the end, and while accesstoken_backend not user do not raise execption, go on next bakcned auth

* perf: re-sorted DEFAULT_AUTHENTICATION_CLASSES

---------

Co-authored-by: Bai <baijiangjie@gmail.com>
2025-12-08 09:57:17 +08:00
fit2bot
ba70edf221 perf: when oauth2 application delete expired well-known page cache via post_delete signal (#16392)
Co-authored-by: Bai <baijiangjie@gmail.com>
2025-12-08 09:54:18 +08:00
Bai
50050dff57 fix: cas only allow exist user login 2025-12-04 18:37:54 +08:00
jiangweidong
944226866c perf: Add a diff field to operate-log export 2025-12-04 18:01:01 +08:00
fit2bot
fe13221d88 fix: Improve server URI validation and connection testing in LDAP module (#16377)
Co-authored-by: wangruidong <940853815@qq.com>
2025-12-04 17:59:01 +08:00
fit2bot
ba17863892 perf: Remove unused CAS user exception handling and simplify login view error response (#16380)
* perf: Remove unused CAS user exception handling and simplify login view error response

* perf: position code

---------

Co-authored-by: wangruidong <940853815@qq.com>
Co-authored-by: Bai <baijiangjie@gmail.com>
2025-12-04 17:49:58 +08:00
fit2bot
065bfeda52 fix: only exists user login maybe invalid (#16379)
* fix: only exists user login maybe invalid

* fix: only exists user login maybe invalid

* fix: only exists user login maybe invalid

---------

Co-authored-by: Bai <baijiangjie@gmail.com>
2025-12-04 16:18:47 +08:00
wangruidong
04af26500a fix: Allow login with username or email for existing users 2025-12-04 10:04:32 +08:00
fit2bot
e0388364c3 fix: use third part authentication service rediect to client failed (#16370)
* perf: .well-known cached 1h and support saml2 redirect_to client

* fix: support wecom redirect_to client (reslove wecom waf 501 error)

* fix: support oauth2 auth rediect to client

* fix: safe next url

---------

Co-authored-by: Bai <baijiangjie@gmail.com>
2025-12-03 19:07:00 +08:00
Bai
3c96480b0c perf: add manage.py command: init_oauth2_provider, resolve init jumpserver client failed issue 2025-12-03 14:37:20 +08:00
Bai
95331a0c4b perf: redirect to client show tips 2025-12-02 18:39:48 +08:00
Bai
b8ecb703cf perf: url revoke_token/ to revoke/ 2025-12-02 18:21:13 +08:00
Bai
1a3f5e3f9a perf: default access token/refresh token expired at 1h/7day 2025-12-02 15:34:55 +08:00
Bai
854396e8d5 perf: access-token api 2025-12-02 15:25:55 +08:00
Bai
ab08603e66 perf: organize oauth2_provider urls, add .well-known API 2025-12-02 14:55:09 +08:00
Bai
427fd3f72c perf: organize oauth2_provider urls, add .well-known API 2025-12-02 14:55:09 +08:00
Bai
0aba9ba120 perf: hide the unused URLs in OAuth2 provider 2025-12-02 14:55:09 +08:00
Bai
045ca8807a feat: modify client redirect url 2025-12-01 19:04:19 +08:00
Bai
19a68d8930 feat: add api access token 2025-12-01 17:55:08 +08:00
Bai
75ed02a2d2 feat: add oauth2 provider accesstokens api 2025-12-01 17:55:08 +08:00
fit2bot
f420dac49c feat: Host cloud sync supports state cloud - i18n (#16304)
Co-authored-by: jiangweidong <1053570670@qq.com>
Co-authored-by: Jiangjie Bai <jiangjie.bai@fit2cloud.com>
2025-12-01 10:56:14 +08:00
Bai
1ee68134f2 fix: rename utils methond 2025-12-01 10:41:14 +08:00
Bai
937265db5d perf: add period task clear oauth2 provider expired tokens 2025-12-01 10:41:14 +08:00
Bai
c611d5e88b perf: add utils delete oauth2 provider application 2025-12-01 10:41:14 +08:00
Bai
883b6b6383 perf: skip_authorization for redirect to jms client 2025-12-01 10:41:14 +08:00
Bai
ac4c72064f perf: register jumpserver client logic 2025-12-01 10:41:14 +08:00
Bai
dbf8360e27 feat: add OAUTH2_PROVIDER_ACCESS_TOKEN_EXPIRE_SECONDS 2025-12-01 10:41:14 +08:00
github-actions[bot]
150d7a09bc perf: Update Dockerfile with new base image tag 2025-11-28 16:28:23 +08:00
Bai
a7ed20e059 perf: support as oauth2 provider 2025-11-28 16:28:23 +08:00
github-actions[bot]
1b7b8e6f2e perf: Update Dockerfile with new base image tag 2025-11-28 16:28:23 +08:00
Bai
cd22fbce19 perf: support as oauth2 provider 2025-11-28 16:28:23 +08:00
Jiangjie Bai
1d0db2ba8b Merge pull request #16316 from jumpserver/dev
v4.10.13-lts
2025-11-20 20:20:02 +08:00
老广
e617245b26 merge: to master 2025-10-16 17:30:34 +08:00
Bryan
9280884c1c Merge pull request #16056 from jumpserver/dev
v4.10.8-lts
2025-09-18 16:52:13 +08:00
Bryan
f31994fdcd Merge pull request #15899 from jumpserver/dev 2025-08-21 19:03:18 +08:00
Bryan
71766418bb Merge pull request #15742 from jumpserver/dev
merge: v4.10.4-lts
2025-07-17 15:12:58 +08:00
Bryan
a9399dd709 Merge pull request #15608 from jumpserver/dev
v4.10.2
2025-06-19 20:14:21 +08:00
Bryan
d0cb9e5432 Merge pull request #15412 from jumpserver/dev
v4.10.0
2025-05-15 17:11:43 +08:00
老广
558188da90 merge: dev to master
Ready to relase
2025-04-17 20:24:45 +08:00
Bryan
ad5460dab8 Merge pull request #15086 from jumpserver/dev
v4.8.0
2025-03-20 18:44:44 +08:00
Bryan
4d37dca0de Merge pull request #14901 from jumpserver/dev
v4.7.0
2025-02-20 10:21:16 +08:00
Bryan
2ca4002624 Merge pull request #14813 from jumpserver/dev
v4.6.0
2025-01-15 14:38:17 +08:00
Bryan
053d640e4c Merge pull request #14699 from jumpserver/dev
v4.5.0
2024-12-19 16:04:45 +08:00
Bryan
f3acc28ded Merge pull request #14697 from jumpserver/dev
v4.5.0
2024-12-19 15:57:11 +08:00
Bryan
25987545db Merge pull request #14511 from jumpserver/dev
v4.4.0
2024-11-21 19:00:35 +08:00
Bryan
6720ecc6e0 Merge pull request #14319 from jumpserver/dev
v4.3.0
2024-10-17 14:55:38 +08:00
老广
0b3a7bb020 Merge pull request #14203 from jumpserver/dev
merge: from dev to master
2024-09-19 19:37:19 +08:00
Bryan
56373e362b Merge pull request #13988 from jumpserver/dev
v4.1.0
2024-08-16 18:40:35 +08:00
Bryan
02fc045370 Merge pull request #13600 from jumpserver/dev
v4.0.0
2024-07-03 19:04:35 +08:00
Bryan
e4ac73896f Merge pull request #13452 from jumpserver/dev
v3.10.11-lts
2024-06-19 16:01:26 +08:00
Bryan
1518f792d6 Merge pull request #13236 from jumpserver/dev
v3.10.10-lts
2024-05-16 16:04:07 +08:00
Bai
67277dd622 fix: 修复仪表盘会话排序数量都是 1 的问题 2024-04-22 19:42:33 +08:00
Bryan
82e7f020ea Merge pull request #13094 from jumpserver/dev
v3.10.9 (dev to master)
2024-04-22 19:39:53 +08:00
Bryan
f20b9e01ab Merge pull request #13062 from jumpserver/dev
v3.10.8 dev to master
2024-04-18 18:01:20 +08:00
Bryan
8cf8a3701b Merge pull request #13059 from jumpserver/dev
v3.10.8
2024-04-18 17:16:37 +08:00
Bryan
7ba24293d1 Merge pull request #12736 from jumpserver/pr@dev@master_fix
fix: 解决冲突
2024-02-29 16:38:43 +08:00
Bai
f10114c9ed fix: 解决冲突 2024-02-29 16:37:10 +08:00
Bryan
cf31cbfb07 Merge pull request #12729 from jumpserver/dev
v3.10.4
2024-02-29 16:19:59 +08:00
wangruidong
0edad24d5d fix: 资产过期消息提示发送失败 2024-02-04 11:41:48 +08:00
ibuler
1f1c1a9157 fix: 修复定时检测用户是否活跃任务无法执行的问题 2024-01-23 09:28:38 +00:00
feng
6c9d271ae1 fix: redis 密码有特殊字符celery beat启动失败 2024-01-22 06:18:34 +00:00
Bai
6ff852e225 perf: 修复 Count 时没有去重的问题 2024-01-22 06:16:25 +00:00
Bryan
baa75dc735 Merge pull request #12566 from jumpserver/master
v3.10.2
2024-01-17 07:34:28 -04:00
Bryan
8a9f0436b8 Merge pull request #12565 from jumpserver/dev
v3.10.2
2024-01-17 07:23:30 -04:00
Bryan
a9620a3cbe Merge pull request #12461 from jumpserver/master
v3.10.1
2023-12-29 11:33:05 +05:00
Bryan
769e7dc8a0 Merge pull request #12460 from jumpserver/dev
v3.10.1
2023-12-29 11:20:36 +05:00
Bryan
2a70449411 Merge pull request #12458 from jumpserver/dev
v3.10.1
2023-12-29 11:01:13 +05:00
Bryan
8df720f19e Merge pull request #12401 from jumpserver/dev
v3.10
2023-12-21 15:14:19 +05:00
老广
dabbb45f6e Merge pull request #12144 from jumpserver/dev
v3.9.0
2023-11-16 18:23:05 +08:00
Bryan
ce24c1c3fd Merge pull request #11914 from jumpserver/dev
v3.8.0
2023-10-19 03:37:39 -05:00
Bryan
3c54c82ce9 Merge pull request #11636 from jumpserver/dev
v3.7.0
2023-09-21 17:02:48 +08:00
119 changed files with 4406 additions and 3299 deletions

View File

@@ -1,4 +1,4 @@
FROM jumpserver/core-base:20251113_092612 AS stage-build FROM jumpserver/core-base:20251128_025056 AS stage-build
ARG VERSION ARG VERSION

View File

@@ -1,3 +1,4 @@
from django.conf import settings
from django.db import transaction from django.db import transaction
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@@ -166,7 +167,7 @@ class AccountViewSet(OrgBulkModelViewSet):
class AccountSecretsViewSet(AccountRecordViewLogMixin, AccountViewSet): class AccountSecretsViewSet(AccountRecordViewLogMixin, AccountViewSet):
""" """
因为可能要导出所有账号所以单独建立了一个 viewset 因为可能要导出所有账号,所以单独建立了一个 viewset
""" """
serializer_classes = { serializer_classes = {
'default': serializers.AccountSecretSerializer, 'default': serializers.AccountSecretSerializer,

View File

@@ -81,4 +81,7 @@ class IntegrationApplicationViewSet(OrgBulkModelViewSet):
remote_addr=get_request_ip(request), service=service.name, service_id=service.id, remote_addr=get_request_ip(request), service=service.name, service_id=service.id,
account=f'{account.name}({account.username})', asset=f'{asset.name}({asset.address})', account=f'{account.name}({account.username})', asset=f'{asset.name}({asset.address})',
) )
return Response(data={'id': request.user.id, 'secret': account.secret})
# 根据配置决定是否返回密码
secret = account.secret if settings.SECURITY_ACCOUNT_SECRET_READ else None
return Response(data={'id': request.user.id, 'secret': secret})

View File

@@ -1,3 +1,5 @@
from django.conf import settings
from django.utils.translation import gettext_lazy as _
from django_filters import rest_framework as drf_filters from django_filters import rest_framework as drf_filters
from rest_framework import status from rest_framework import status
from rest_framework.decorators import action from rest_framework.decorators import action

View File

@@ -104,7 +104,7 @@ class AutomationExecutionViewSet(
mixins.CreateModelMixin, mixins.ListModelMixin, mixins.CreateModelMixin, mixins.ListModelMixin,
mixins.RetrieveModelMixin, viewsets.GenericViewSet mixins.RetrieveModelMixin, viewsets.GenericViewSet
): ):
search_fields = ('trigger', 'automation__name') search_fields = ('id', 'trigger', 'automation__name')
filterset_fields = ('trigger', 'automation_id', 'automation__name') filterset_fields = ('trigger', 'automation_id', 'automation__name')
filterset_class = AutomationExecutionFilterSet filterset_class = AutomationExecutionFilterSet
serializer_class = serializers.AutomationExecutionSerializer serializer_class = serializers.AutomationExecutionSerializer

View File

@@ -234,7 +234,7 @@ class AutomationExecutionFilterSet(DaysExecutionFilterMixin, BaseFilterSet):
class Meta: class Meta:
model = AutomationExecution model = AutomationExecution
fields = ["days", 'trigger', 'automation__name'] fields = ["id", "days", 'trigger', 'automation__name']
class PushAccountRecordFilterSet(SecretRecordMixin, UUIDFilterMixin, BaseFilterSet): class PushAccountRecordFilterSet(SecretRecordMixin, UUIDFilterMixin, BaseFilterSet):

View File

@@ -81,7 +81,9 @@ class VaultModelMixin(models.Model):
def mark_secret_save_to_vault(self): def mark_secret_save_to_vault(self):
self._secret = self._secret_save_to_vault_mark self._secret = self._secret_save_to_vault_mark
self.skip_history_when_saving = True self.skip_history_when_saving = True
self.save() # Avoid calling overridden `save()` on concrete models (e.g. AccountTemplate)
# which may mutate `secret/_secret` again and cause post_save recursion.
super(VaultModelMixin, self).save(update_fields=['_secret'])
@property @property
def secret_has_save_to_vault(self): def secret_has_save_to_vault(self):

View File

@@ -14,7 +14,7 @@ from accounts.models import Account, AccountTemplate, GatheredAccount
from accounts.tasks import push_accounts_to_assets_task from accounts.tasks import push_accounts_to_assets_task
from assets.const import Category, AllTypes from assets.const import Category, AllTypes
from assets.models import Asset from assets.models import Asset
from common.serializers import SecretReadableMixin, CommonBulkModelSerializer from common.serializers import SecretReadableMixin, SecretReadableCheckMixin, CommonBulkModelSerializer
from common.serializers.fields import ObjectRelatedField, LabeledChoiceField from common.serializers.fields import ObjectRelatedField, LabeledChoiceField
from common.utils import get_logger from common.utils import get_logger
from .base import BaseAccountSerializer, AuthValidateMixin from .base import BaseAccountSerializer, AuthValidateMixin
@@ -478,7 +478,7 @@ class AssetAccountBulkSerializer(
return results return results
class AccountSecretSerializer(SecretReadableMixin, AccountSerializer): class AccountSecretSerializer(SecretReadableCheckMixin, SecretReadableMixin, AccountSerializer):
spec_info = serializers.DictField(label=_('Spec info'), read_only=True) spec_info = serializers.DictField(label=_('Spec info'), read_only=True)
class Meta(AccountSerializer.Meta): class Meta(AccountSerializer.Meta):
@@ -491,9 +491,10 @@ class AccountSecretSerializer(SecretReadableMixin, AccountSerializer):
exclude_backup_fields = [ exclude_backup_fields = [
'passphrase', 'push_now', 'params', 'spec_info' 'passphrase', 'push_now', 'params', 'spec_info'
] ]
secret_fields = ['secret']
class AccountHistorySerializer(serializers.ModelSerializer): class AccountHistorySerializer(SecretReadableCheckMixin, serializers.ModelSerializer):
secret_type = LabeledChoiceField(choices=SecretType.choices, label=_('Secret type')) secret_type = LabeledChoiceField(choices=SecretType.choices, label=_('Secret type'))
secret = serializers.CharField(label=_('Secret'), read_only=True) secret = serializers.CharField(label=_('Secret'), read_only=True)
id = serializers.IntegerField(label=_('ID'), source='history_id', read_only=True) id = serializers.IntegerField(label=_('ID'), source='history_id', read_only=True)
@@ -509,6 +510,7 @@ class AccountHistorySerializer(serializers.ModelSerializer):
'history_user': {'label': _('User')}, 'history_user': {'label': _('User')},
'history_date': {'label': _('Date')}, 'history_date': {'label': _('Date')},
} }
secret_fields = ['secret']
class AccountTaskSerializer(serializers.Serializer): class AccountTaskSerializer(serializers.Serializer):

View File

@@ -2,7 +2,7 @@ from django.utils.translation import gettext_lazy as _
from rest_framework import serializers from rest_framework import serializers
from accounts.models import AccountTemplate from accounts.models import AccountTemplate
from common.serializers import SecretReadableMixin from common.serializers import SecretReadableMixin, SecretReadableCheckMixin
from common.serializers.fields import ObjectRelatedField from common.serializers.fields import ObjectRelatedField
from .base import BaseAccountSerializer from .base import BaseAccountSerializer
@@ -62,10 +62,11 @@ class AccountDetailTemplateSerializer(AccountTemplateSerializer):
fields = AccountTemplateSerializer.Meta.fields + ['spec_info'] fields = AccountTemplateSerializer.Meta.fields + ['spec_info']
class AccountTemplateSecretSerializer(SecretReadableMixin, AccountDetailTemplateSerializer): class AccountTemplateSecretSerializer(SecretReadableCheckMixin, SecretReadableMixin, AccountDetailTemplateSerializer):
class Meta(AccountDetailTemplateSerializer.Meta): class Meta(AccountDetailTemplateSerializer.Meta):
fields = AccountDetailTemplateSerializer.Meta.fields fields = AccountDetailTemplateSerializer.Meta.fields
extra_kwargs = { extra_kwargs = {
**AccountDetailTemplateSerializer.Meta.extra_kwargs, **AccountDetailTemplateSerializer.Meta.extra_kwargs,
'secret': {'write_only': False}, 'secret': {'write_only': False},
} }
secret_fields = ['secret']

View File

@@ -79,7 +79,7 @@ class VaultSignalHandler(object):
else: else:
vault_client.update(instance) vault_client.update(instance)
except Exception as e: except Exception as e:
logger.error('Vault save failed: {}'.format(e)) logger.exception('Vault save failed: %s', e)
raise VaultException() raise VaultException()
@staticmethod @staticmethod
@@ -87,7 +87,7 @@ class VaultSignalHandler(object):
try: try:
vault_client.delete(instance) vault_client.delete(instance)
except Exception as e: except Exception as e:
logger.error('Vault delete failed: {}'.format(e)) logger.exception('Vault delete failed: %s', e)
raise VaultException() raise VaultException()

View File

@@ -6,7 +6,7 @@ from django.utils.translation import gettext_lazy as _
from accounts.backends import vault_client from accounts.backends import vault_client
from accounts.const import VaultTypeChoices from accounts.const import VaultTypeChoices
from accounts.models import Account, AccountTemplate from accounts.models import AccountTemplate, Account
from common.utils import get_logger from common.utils import get_logger
from orgs.utils import tmp_to_root_org from orgs.utils import tmp_to_root_org

View File

@@ -126,7 +126,7 @@ class BaseManager:
self.execution.save() self.execution.save()
def print_summary(self): def print_summary(self):
content = "\nSummery: \n" content = "\nSummary: \n"
for k, v in self.summary.items(): for k, v in self.summary.items():
content += f"\t - {k}: {v}\n" content += f"\t - {k}: {v}\n"
content += "\t - Using: {}s\n".format(self.duration) content += "\t - Using: {}s\n".format(self.duration)

View File

@@ -43,7 +43,7 @@ from .serializers import (
OperateLogSerializer, OperateLogActionDetailSerializer, OperateLogSerializer, OperateLogActionDetailSerializer,
PasswordChangeLogSerializer, ActivityUnionLogSerializer, PasswordChangeLogSerializer, ActivityUnionLogSerializer,
FileSerializer, UserSessionSerializer, JobsAuditSerializer, FileSerializer, UserSessionSerializer, JobsAuditSerializer,
ServiceAccessLogSerializer ServiceAccessLogSerializer, OperateLogFullSerializer
) )
from .utils import construct_userlogin_usernames, record_operate_log_and_activity_log from .utils import construct_userlogin_usernames, record_operate_log_and_activity_log
@@ -256,7 +256,9 @@ class OperateLogViewSet(OrgReadonlyModelViewSet):
def get_serializer_class(self): def get_serializer_class(self):
if self.is_action_detail: if self.is_action_detail:
return OperateLogActionDetailSerializer return OperateLogActionDetailSerializer
return super().get_serializer_class() elif self.request.query_params.get('format'):
return OperateLogFullSerializer
return OperateLogSerializer
def get_queryset(self): def get_queryset(self):
current_org_id = str(current_org.id) current_org_id = str(current_org.id)

View File

@@ -127,6 +127,21 @@ class OperateLogSerializer(BulkOrgResourceModelSerializer):
return i18n_trans(instance.resource) return i18n_trans(instance.resource)
class DiffFieldSerializer(serializers.JSONField):
def to_file_representation(self, value):
row = getattr(self, '_row') or {}
attrs = {'diff': value, 'resource_type': row.get('resource_type')}
instance = type('OperateLog', (), attrs)
return OperateLogStore.convert_diff_friendly(instance)
class OperateLogFullSerializer(OperateLogSerializer):
diff = DiffFieldSerializer(label=_("Diff"))
class Meta(OperateLogSerializer.Meta):
fields = OperateLogSerializer.Meta.fields + ['diff']
class PasswordChangeLogSerializer(serializers.ModelSerializer): class PasswordChangeLogSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = models.PasswordChangeLog model = models.PasswordChangeLog

View File

@@ -16,3 +16,4 @@ from .sso import *
from .temp_token import * from .temp_token import *
from .token import * from .token import *
from .face import * from .face import *
from .access_token import *

View File

@@ -0,0 +1,47 @@
from django.shortcuts import get_object_or_404
from django.utils.translation import gettext as _
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.status import HTTP_204_NO_CONTENT, HTTP_404_NOT_FOUND
from oauth2_provider.models import get_access_token_model
from common.api import JMSModelViewSet
from rbac.permissions import RBACPermission
from ..serializers import AccessTokenSerializer
AccessToken = get_access_token_model()
class AccessTokenViewSet(JMSModelViewSet):
"""
OAuth2 Access Token 管理视图集
用户只能查看和撤销自己的 access token
"""
serializer_class = AccessTokenSerializer
permission_classes = [RBACPermission]
http_method_names = ['get', 'options', 'delete']
rbac_perms = {
'revoke': 'oauth2_provider.delete_accesstoken',
}
def get_queryset(self):
"""只返回当前用户的 access token按创建时间倒序"""
return AccessToken.objects.filter(user=self.request.user).order_by('-created')
@action(methods=['DELETE'], detail=True, url_path='revoke')
def revoke(self, request, *args, **kwargs):
"""
撤销 access token 及其关联的 refresh token
如果 token 不存在或不属于当前用户,返回 404
"""
token = get_object_or_404(
AccessToken.objects.filter(user=request.user),
id=kwargs['pk']
)
# 优先撤销 refresh token会自动撤销关联的 access token
token_to_revoke = token.refresh_token if token.refresh_token else token
token_to_revoke.revoke()
return Response(status=HTTP_204_NO_CONTENT)

View File

@@ -219,8 +219,18 @@ class RDPFileClientProtocolURLMixin:
} }
}) })
else: else:
if connect_method_dict['type'] == 'virtual_app':
endpoint_protocol = 'vnc'
token_protocol = 'vnc'
data.update({
'protocol': 'vnc',
})
else:
endpoint_protocol = connect_method_dict['endpoint_protocol']
token_protocol = token.protocol
endpoint = self.get_smart_endpoint( endpoint = self.get_smart_endpoint(
protocol=connect_method_dict['endpoint_protocol'], protocol=endpoint_protocol,
asset=asset asset=asset
) )
data.update({ data.update({
@@ -236,7 +246,7 @@ class RDPFileClientProtocolURLMixin:
}, },
'endpoint': { 'endpoint': {
'host': endpoint.host, 'host': endpoint.host,
'port': endpoint.get_port(token.asset, token.protocol), 'port': endpoint.get_port(token.asset, token_protocol),
} }
}) })
return data return data

View File

@@ -1,6 +1,5 @@
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend from django.contrib.auth.backends import ModelBackend
from django.views import View
from common.utils import get_logger from common.utils import get_logger
from users.models import User from users.models import User
@@ -66,11 +65,3 @@ class JMSBaseAuthBackend:
class JMSModelBackend(JMSBaseAuthBackend, ModelBackend): class JMSModelBackend(JMSBaseAuthBackend, ModelBackend):
def user_can_authenticate(self, user): def user_can_authenticate(self, user):
return True return True
class BaseAuthCallbackClientView(View):
http_method_names = ['get']
def get(self, request):
from authentication.views.utils import redirect_to_guard_view
return redirect_to_guard_view(query_string='next=client')

View File

@@ -1,51 +1,22 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
import threading
from django.conf import settings from django.conf import settings
from django.contrib.auth import get_user_model
from django_cas_ng.backends import CASBackend as _CASBackend from django_cas_ng.backends import CASBackend as _CASBackend
from common.utils import get_logger from common.utils import get_logger
from ..base import JMSBaseAuthBackend from ..base import JMSBaseAuthBackend
__all__ = ['CASBackend', 'CASUserDoesNotExist'] __all__ = ['CASBackend']
logger = get_logger(__name__) logger = get_logger(__name__)
class CASUserDoesNotExist(Exception):
"""Exception raised when a CAS user does not exist."""
pass
class CASBackend(JMSBaseAuthBackend, _CASBackend): class CASBackend(JMSBaseAuthBackend, _CASBackend):
@staticmethod @staticmethod
def is_enabled(): def is_enabled():
return settings.AUTH_CAS return settings.AUTH_CAS
def authenticate(self, request, ticket, service): def authenticate(self, request, ticket, service):
UserModel = get_user_model() # 这里做个hack ,让父类始终走CAS_CREATE_USER=True的逻辑然后调用 authentication/mixins.py 中的 custom_get_or_create 方法
manager = UserModel._default_manager settings.CAS_CREATE_USER = True
original_get_by_natural_key = manager.get_by_natural_key return super().authenticate(request, ticket, service)
thread_local = threading.local()
thread_local.thread_id = threading.get_ident()
logger.debug(f"CASBackend.authenticate: thread_id={thread_local.thread_id}")
def get_by_natural_key(self, username):
logger.debug(f"CASBackend.get_by_natural_key: thread_id={threading.get_ident()}, username={username}")
if threading.get_ident() != thread_local.thread_id:
return original_get_by_natural_key(username)
try:
user = original_get_by_natural_key(username)
except UserModel.DoesNotExist:
raise CASUserDoesNotExist(username)
return user
try:
manager.get_by_natural_key = get_by_natural_key.__get__(manager, type(manager))
user = super().authenticate(request, ticket=ticket, service=service)
finally:
manager.get_by_natural_key = original_get_by_natural_key
return user

View File

@@ -3,11 +3,10 @@
import django_cas_ng.views import django_cas_ng.views
from django.urls import path from django.urls import path
from .views import CASLoginView, CASCallbackClientView from .views import CASLoginView
urlpatterns = [ urlpatterns = [
path('login/', CASLoginView.as_view(), name='cas-login'), path('login/', CASLoginView.as_view(), name='cas-login'),
path('logout/', django_cas_ng.views.LogoutView.as_view(), name='cas-logout'), path('logout/', django_cas_ng.views.LogoutView.as_view(), name='cas-logout'),
path('callback/', django_cas_ng.views.CallbackView.as_view(), name='cas-proxy-callback'), path('callback/', django_cas_ng.views.CallbackView.as_view(), name='cas-proxy-callback')
path('login/client', CASCallbackClientView.as_view(), name='cas-proxy-callback-client'),
] ]

View File

@@ -3,31 +3,20 @@ from django.http import HttpResponseRedirect
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django_cas_ng.views import LoginView from django_cas_ng.views import LoginView
from authentication.backends.base import BaseAuthCallbackClientView from authentication.views.mixins import FlashMessageMixin
from common.utils import FlashMessageUtil
from .backends import CASUserDoesNotExist
__all__ = ['LoginView'] __all__ = ['LoginView']
class CASLoginView(LoginView): class CASLoginView(LoginView, FlashMessageMixin):
def get(self, request): def get(self, request):
try: try:
resp = super().get(request) resp = super().get(request)
return resp
except PermissionDenied: except PermissionDenied:
return HttpResponseRedirect('/') resp = HttpResponseRedirect('/')
except CASUserDoesNotExist as e: error_message = getattr(request, 'error_message', '')
message_data = { if error_message:
'title': _('User does not exist: {}').format(e), response = self.get_failed_response('/', title=_('CAS Error'), msg=error_message)
'error': _( return response
'CAS login was successful, but no corresponding local user was found in the system, and automatic ' else:
'user creation is disabled in the CAS authentication configuration. Login failed.'), return resp
'interval': 10,
'redirect_url': '/',
}
return FlashMessageUtil.gen_and_redirect_to(message_data)
class CASCallbackClientView(BaseAuthCallbackClientView):
pass

View File

@@ -69,6 +69,8 @@ class AccessTokenAuthentication(authentication.BaseAuthentication):
msg = _('Invalid token header. Sign string should not contain invalid characters.') msg = _('Invalid token header. Sign string should not contain invalid characters.')
raise exceptions.AuthenticationFailed(msg) raise exceptions.AuthenticationFailed(msg)
user, header = self.authenticate_credentials(token) user, header = self.authenticate_credentials(token)
if not user:
return None
after_authenticate_update_date(user) after_authenticate_update_date(user)
return user, header return user, header
@@ -77,10 +79,6 @@ class AccessTokenAuthentication(authentication.BaseAuthentication):
model = get_user_model() model = get_user_model()
user_id = cache.get(token) user_id = cache.get(token)
user = get_object_or_none(model, id=user_id) user = get_object_or_none(model, id=user_id)
if not user:
msg = _('Invalid token or cache refreshed.')
raise exceptions.AuthenticationFailed(msg)
return user, None return user, None
def authenticate_header(self, request): def authenticate_header(self, request):
@@ -110,7 +108,7 @@ class SessionAuthentication(authentication.SessionAuthentication):
user = getattr(request._request, 'user', None) user = getattr(request._request, 'user', None)
# Unauthenticated, CSRF validation not required # Unauthenticated, CSRF validation not required
if not user or not user.is_active: if not user or not user.is_active or not user.is_valid:
return None return None
try: try:

View File

@@ -7,6 +7,5 @@ from . import views
urlpatterns = [ urlpatterns = [
path('login/', views.OAuth2AuthRequestView.as_view(), name='login'), path('login/', views.OAuth2AuthRequestView.as_view(), name='login'),
path('callback/', views.OAuth2AuthCallbackView.as_view(), name='login-callback'), path('callback/', views.OAuth2AuthCallbackView.as_view(), name='login-callback'),
path('callback/client/', views.OAuth2AuthCallbackClientView.as_view(), name='login-callback-client'),
path('logout/', views.OAuth2EndSessionView.as_view(), name='logout') path('logout/', views.OAuth2EndSessionView.as_view(), name='logout')
] ]

View File

@@ -6,27 +6,34 @@ from django.utils.http import urlencode
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views import View from django.views import View
from authentication.backends.base import BaseAuthCallbackClientView from authentication.decorators import pre_save_next_to_session, redirect_to_pre_save_next_after_auth
from authentication.mixins import authenticate from authentication.mixins import authenticate
from authentication.utils import build_absolute_uri from authentication.utils import build_absolute_uri
from authentication.views.mixins import FlashMessageMixin from authentication.views.mixins import FlashMessageMixin
from common.utils import get_logger from common.utils import get_logger, safe_next_url
logger = get_logger(__file__) logger = get_logger(__file__)
class OAuth2AuthRequestView(View): class OAuth2AuthRequestView(View):
@pre_save_next_to_session()
def get(self, request): def get(self, request):
log_prompt = "Process OAuth2 GET requests: {}" log_prompt = "Process OAuth2 GET requests: {}"
logger.debug(log_prompt.format('Start')) logger.debug(log_prompt.format('Start'))
request_params = request.GET.dict()
request_params.pop('next', None)
query = urlencode(request_params)
redirect_uri = build_absolute_uri(
request, path=reverse(settings.AUTH_OAUTH2_AUTH_LOGIN_CALLBACK_URL_NAME)
)
redirect_uri = f"{redirect_uri}?{query}"
query_dict = { query_dict = {
'client_id': settings.AUTH_OAUTH2_CLIENT_ID, 'response_type': 'code', 'client_id': settings.AUTH_OAUTH2_CLIENT_ID, 'response_type': 'code',
'scope': settings.AUTH_OAUTH2_SCOPE, 'scope': settings.AUTH_OAUTH2_SCOPE,
'redirect_uri': build_absolute_uri( 'redirect_uri': redirect_uri
request, path=reverse(settings.AUTH_OAUTH2_AUTH_LOGIN_CALLBACK_URL_NAME)
)
} }
if '?' in settings.AUTH_OAUTH2_PROVIDER_AUTHORIZATION_ENDPOINT: if '?' in settings.AUTH_OAUTH2_PROVIDER_AUTHORIZATION_ENDPOINT:
@@ -45,6 +52,7 @@ class OAuth2AuthRequestView(View):
class OAuth2AuthCallbackView(View, FlashMessageMixin): class OAuth2AuthCallbackView(View, FlashMessageMixin):
http_method_names = ['get', ] http_method_names = ['get', ]
@redirect_to_pre_save_next_after_auth
def get(self, request): def get(self, request):
""" Processes GET requests. """ """ Processes GET requests. """
log_prompt = "Process GET requests [OAuth2AuthCallbackView]: {}" log_prompt = "Process GET requests [OAuth2AuthCallbackView]: {}"
@@ -59,9 +67,7 @@ class OAuth2AuthCallbackView(View, FlashMessageMixin):
logger.debug(log_prompt.format('Login: {}'.format(user))) logger.debug(log_prompt.format('Login: {}'.format(user)))
auth.login(self.request, user) auth.login(self.request, user)
logger.debug(log_prompt.format('Redirect')) logger.debug(log_prompt.format('Redirect'))
return HttpResponseRedirect( return HttpResponseRedirect(settings.AUTH_OAUTH2_AUTHENTICATION_REDIRECT_URI)
settings.AUTH_OAUTH2_AUTHENTICATION_REDIRECT_URI
)
else: else:
if getattr(request, 'error_message', ''): if getattr(request, 'error_message', ''):
response = self.get_failed_response('/', title=_('OAuth2 Error'), msg=request.error_message) response = self.get_failed_response('/', title=_('OAuth2 Error'), msg=request.error_message)
@@ -72,10 +78,6 @@ class OAuth2AuthCallbackView(View, FlashMessageMixin):
return HttpResponseRedirect(redirect_url) return HttpResponseRedirect(redirect_url)
class OAuth2AuthCallbackClientView(BaseAuthCallbackClientView):
pass
class OAuth2EndSessionView(View): class OAuth2EndSessionView(View):
http_method_names = ['get', 'post', ] http_method_names = ['get', 'post', ]

View File

@@ -0,0 +1,20 @@
from django.db.models.signals import post_delete
from django.dispatch import receiver
from django.core.cache import cache
from django.conf import settings
from oauth2_provider.models import get_application_model
from .utils import clear_oauth2_authorization_server_view_cache
__all__ = ['on_oauth2_provider_application_deleted']
Application = get_application_model()
@receiver(post_delete, sender=Application)
def on_oauth2_provider_application_deleted(sender, instance, **kwargs):
if instance.name == settings.OAUTH2_PROVIDER_JUMPSERVER_CLIENT_NAME:
clear_oauth2_authorization_server_view_cache()

View File

@@ -0,0 +1,14 @@
# -*- coding: utf-8 -*-
#
from django.urls import path
from oauth2_provider import views as op_views
from . import views
urlpatterns = [
path("authorize/", op_views.AuthorizationView.as_view(), name="authorize"),
path("token/", op_views.TokenView.as_view(), name="token"),
path("revoke/", op_views.RevokeTokenView.as_view(), name="revoke-token"),
path(".well-known/oauth-authorization-server", views.OAuthAuthorizationServerView.as_view(), name="oauth-authorization-server"),
]

View File

@@ -0,0 +1,31 @@
from django.conf import settings
from django.core.cache import cache
from oauth2_provider.models import get_application_model
from common.utils import get_logger
logger = get_logger(__name__)
def get_or_create_jumpserver_client_application():
"""Auto get or create OAuth2 JumpServer Client application."""
Application = get_application_model()
application, created = Application.objects.get_or_create(
name=settings.OAUTH2_PROVIDER_JUMPSERVER_CLIENT_NAME,
defaults={
'client_type': Application.CLIENT_PUBLIC,
'authorization_grant_type': Application.GRANT_AUTHORIZATION_CODE,
'redirect_uris': settings.OAUTH2_PROVIDER_CLIENT_REDIRECT_URI,
'skip_authorization': True,
}
)
return application
CACHE_OAUTH_SERVER_VIEW_KEY_PREFIX = 'oauth2_provider_metadata'
def clear_oauth2_authorization_server_view_cache():
logger.info("Clearing OAuth2 Authorization Server Metadata view cache")
cache_key = f'views.decorators.cache.cache_page.{CACHE_OAUTH_SERVER_VIEW_KEY_PREFIX}.GET*'
cache.delete_pattern(cache_key)

View File

@@ -0,0 +1,77 @@
from django.views.generic import View
from django.http import JsonResponse
from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_page
from django.views.decorators.csrf import csrf_exempt
from django.conf import settings
from django.urls import reverse
from oauth2_provider.settings import oauth2_settings
from typing import List, Dict, Any
from .utils import get_or_create_jumpserver_client_application, CACHE_OAUTH_SERVER_VIEW_KEY_PREFIX
@method_decorator(csrf_exempt, name='dispatch')
@method_decorator(cache_page(timeout=60 * 60 * 24, key_prefix=CACHE_OAUTH_SERVER_VIEW_KEY_PREFIX), name='dispatch')
class OAuthAuthorizationServerView(View):
"""
OAuth 2.0 Authorization Server Metadata Endpoint
RFC 8414: https://datatracker.ietf.org/doc/html/rfc8414
This endpoint provides machine-readable information about the
OAuth 2.0 authorization server's configuration.
"""
def get_base_url(self, request) -> str:
scheme = 'https' if request.is_secure() else 'http'
host = request.get_host()
return f"{scheme}://{host}"
def get_supported_scopes(self) -> List[str]:
scopes_config = oauth2_settings.SCOPES
if isinstance(scopes_config, dict):
return list(scopes_config.keys())
return []
def get_metadata(self, request) -> Dict[str, Any]:
base_url = self.get_base_url(request)
application = get_or_create_jumpserver_client_application()
metadata = {
"issuer": base_url,
"client_id": application.client_id if application else "Not found any application.",
"authorization_endpoint": base_url + reverse('authentication:oauth2-provider:authorize'),
"token_endpoint": base_url + reverse('authentication:oauth2-provider:token'),
"revocation_endpoint": base_url + reverse('authentication:oauth2-provider:revoke-token'),
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code", "refresh_token"],
"scopes_supported": self.get_supported_scopes(),
"token_endpoint_auth_methods_supported": ["none"],
"revocation_endpoint_auth_methods_supported": ["none"],
"code_challenge_methods_supported": ["S256"],
"response_modes_supported": ["query"],
}
if hasattr(oauth2_settings, 'ACCESS_TOKEN_EXPIRE_SECONDS'):
metadata["token_expires_in"] = oauth2_settings.ACCESS_TOKEN_EXPIRE_SECONDS
if hasattr(oauth2_settings, 'REFRESH_TOKEN_EXPIRE_SECONDS'):
if oauth2_settings.REFRESH_TOKEN_EXPIRE_SECONDS:
metadata["refresh_token_expires_in"] = oauth2_settings.REFRESH_TOKEN_EXPIRE_SECONDS
return metadata
def get(self, request, *args, **kwargs):
metadata = self.get_metadata(request)
response = JsonResponse(metadata)
self.add_cors_headers(response)
return response
def options(self, request, *args, **kwargs):
response = JsonResponse({})
self.add_cors_headers(response)
return response
@staticmethod
def add_cors_headers(response):
response['Access-Control-Allow-Origin'] = '*'
response['Access-Control-Allow-Methods'] = 'GET, OPTIONS'
response['Access-Control-Allow-Headers'] = 'Content-Type, Authorization'
response['Access-Control-Max-Age'] = '3600'

View File

@@ -15,6 +15,5 @@ from . import views
urlpatterns = [ urlpatterns = [
path('login/', views.OIDCAuthRequestView.as_view(), name='login'), path('login/', views.OIDCAuthRequestView.as_view(), name='login'),
path('callback/', views.OIDCAuthCallbackView.as_view(), name='login-callback'), path('callback/', views.OIDCAuthCallbackView.as_view(), name='login-callback'),
path('callback/client/', views.OIDCAuthCallbackClientView.as_view(), name='login-callback-client'),
path('logout/', views.OIDCEndSessionView.as_view(), name='logout'), path('logout/', views.OIDCEndSessionView.as_view(), name='logout'),
] ]

View File

@@ -25,11 +25,11 @@ from django.utils.http import urlencode
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views.generic import View from django.views.generic import View
from authentication.decorators import pre_save_next_to_session, redirect_to_pre_save_next_after_auth
from authentication.utils import build_absolute_uri_for_oidc from authentication.utils import build_absolute_uri_for_oidc
from authentication.views.mixins import FlashMessageMixin from authentication.views.mixins import FlashMessageMixin
from common.utils import safe_next_url from common.utils import safe_next_url
from .utils import get_logger from .utils import get_logger
from ..base import BaseAuthCallbackClientView
logger = get_logger(__file__) logger = get_logger(__file__)
@@ -58,6 +58,7 @@ class OIDCAuthRequestView(View):
b = base64.urlsafe_b64encode(h) b = base64.urlsafe_b64encode(h)
return b.decode('ascii')[:-1] return b.decode('ascii')[:-1]
@pre_save_next_to_session()
def get(self, request): def get(self, request):
""" Processes GET requests. """ """ Processes GET requests. """
@@ -66,8 +67,9 @@ class OIDCAuthRequestView(View):
# Defines common parameters used to bootstrap the authentication request. # Defines common parameters used to bootstrap the authentication request.
logger.debug(log_prompt.format('Construct request params')) logger.debug(log_prompt.format('Construct request params'))
authentication_request_params = request.GET.dict() request_params = request.GET.dict()
authentication_request_params.update({ request_params.pop('next', None)
request_params.update({
'scope': settings.AUTH_OPENID_SCOPES, 'scope': settings.AUTH_OPENID_SCOPES,
'response_type': 'code', 'response_type': 'code',
'client_id': settings.AUTH_OPENID_CLIENT_ID, 'client_id': settings.AUTH_OPENID_CLIENT_ID,
@@ -80,7 +82,7 @@ class OIDCAuthRequestView(View):
code_verifier = self.gen_code_verifier() code_verifier = self.gen_code_verifier()
code_challenge_method = settings.AUTH_OPENID_CODE_CHALLENGE_METHOD or 'S256' code_challenge_method = settings.AUTH_OPENID_CODE_CHALLENGE_METHOD or 'S256'
code_challenge = self.gen_code_challenge(code_verifier, code_challenge_method) code_challenge = self.gen_code_challenge(code_verifier, code_challenge_method)
authentication_request_params.update({ request_params.update({
'code_challenge_method': code_challenge_method, 'code_challenge_method': code_challenge_method,
'code_challenge': code_challenge 'code_challenge': code_challenge
}) })
@@ -91,7 +93,7 @@ class OIDCAuthRequestView(View):
if settings.AUTH_OPENID_USE_STATE: if settings.AUTH_OPENID_USE_STATE:
logger.debug(log_prompt.format('Use state')) logger.debug(log_prompt.format('Use state'))
state = get_random_string(settings.AUTH_OPENID_STATE_LENGTH) state = get_random_string(settings.AUTH_OPENID_STATE_LENGTH)
authentication_request_params.update({'state': state}) request_params.update({'state': state})
request.session['oidc_auth_state'] = state request.session['oidc_auth_state'] = state
# Nonces should be used too! In that case the generated nonce is stored both in the # Nonces should be used too! In that case the generated nonce is stored both in the
@@ -99,17 +101,12 @@ class OIDCAuthRequestView(View):
if settings.AUTH_OPENID_USE_NONCE: if settings.AUTH_OPENID_USE_NONCE:
logger.debug(log_prompt.format('Use nonce')) logger.debug(log_prompt.format('Use nonce'))
nonce = get_random_string(settings.AUTH_OPENID_NONCE_LENGTH) nonce = get_random_string(settings.AUTH_OPENID_NONCE_LENGTH)
authentication_request_params.update({'nonce': nonce, }) request_params.update({'nonce': nonce, })
request.session['oidc_auth_nonce'] = nonce request.session['oidc_auth_nonce'] = nonce
# Stores the "next" URL in the session if applicable.
logger.debug(log_prompt.format('Stores next url in the session'))
next_url = request.GET.get('next')
request.session['oidc_auth_next_url'] = safe_next_url(next_url, request=request)
# Redirects the user to authorization endpoint. # Redirects the user to authorization endpoint.
logger.debug(log_prompt.format('Construct redirect url')) logger.debug(log_prompt.format('Construct redirect url'))
query = urlencode(authentication_request_params) query = urlencode(request_params)
redirect_url = '{url}?{query}'.format( redirect_url = '{url}?{query}'.format(
url=settings.AUTH_OPENID_PROVIDER_AUTHORIZATION_ENDPOINT, query=query) url=settings.AUTH_OPENID_PROVIDER_AUTHORIZATION_ENDPOINT, query=query)
@@ -129,6 +126,8 @@ class OIDCAuthCallbackView(View, FlashMessageMixin):
http_method_names = ['get', ] http_method_names = ['get', ]
@redirect_to_pre_save_next_after_auth
def get(self, request): def get(self, request):
""" Processes GET requests. """ """ Processes GET requests. """
log_prompt = "Process GET requests [OIDCAuthCallbackView]: {}" log_prompt = "Process GET requests [OIDCAuthCallbackView]: {}"
@@ -167,7 +166,6 @@ class OIDCAuthCallbackView(View, FlashMessageMixin):
raise SuspiciousOperation('Invalid OpenID Connect callback state value') raise SuspiciousOperation('Invalid OpenID Connect callback state value')
# Authenticates the end-user. # Authenticates the end-user.
next_url = request.session.get('oidc_auth_next_url', None)
code_verifier = request.session.get('oidc_auth_code_verifier', None) code_verifier = request.session.get('oidc_auth_code_verifier', None)
logger.debug(log_prompt.format('Process authenticate')) logger.debug(log_prompt.format('Process authenticate'))
try: try:
@@ -191,9 +189,7 @@ class OIDCAuthCallbackView(View, FlashMessageMixin):
callback_params.get('session_state', None) callback_params.get('session_state', None)
logger.debug(log_prompt.format('Redirect')) logger.debug(log_prompt.format('Redirect'))
return HttpResponseRedirect( return HttpResponseRedirect(settings.AUTH_OPENID_AUTHENTICATION_REDIRECT_URI)
next_url or settings.AUTH_OPENID_AUTHENTICATION_REDIRECT_URI
)
if 'error' in callback_params: if 'error' in callback_params:
logger.debug( logger.debug(
log_prompt.format('Error in callback params: {}'.format(callback_params['error'])) log_prompt.format('Error in callback params: {}'.format(callback_params['error']))
@@ -212,10 +208,6 @@ class OIDCAuthCallbackView(View, FlashMessageMixin):
return HttpResponseRedirect(redirect_url) return HttpResponseRedirect(redirect_url)
class OIDCAuthCallbackClientView(BaseAuthCallbackClientView):
pass
class OIDCEndSessionView(View): class OIDCEndSessionView(View):
""" Allows to end the session of any user authenticated using OpenID Connect. """ Allows to end the session of any user authenticated using OpenID Connect.

View File

@@ -8,6 +8,5 @@ urlpatterns = [
path('login/', views.Saml2AuthRequestView.as_view(), name='saml2-login'), path('login/', views.Saml2AuthRequestView.as_view(), name='saml2-login'),
path('logout/', views.Saml2EndSessionView.as_view(), name='saml2-logout'), path('logout/', views.Saml2EndSessionView.as_view(), name='saml2-logout'),
path('callback/', views.Saml2AuthCallbackView.as_view(), name='saml2-callback'), path('callback/', views.Saml2AuthCallbackView.as_view(), name='saml2-callback'),
path('callback/client/', views.Saml2AuthCallbackClientView.as_view(), name='saml2-callback-client'),
path('metadata/', views.Saml2AuthMetadataView.as_view(), name='saml2-metadata'), path('metadata/', views.Saml2AuthMetadataView.as_view(), name='saml2-metadata'),
] ]

View File

@@ -17,9 +17,8 @@ from onelogin.saml2.idp_metadata_parser import (
) )
from authentication.views.mixins import FlashMessageMixin from authentication.views.mixins import FlashMessageMixin
from common.utils import get_logger from common.utils import get_logger, safe_next_url
from .settings import JmsSaml2Settings from .settings import JmsSaml2Settings
from ..base import BaseAuthCallbackClientView
logger = get_logger(__file__) logger = get_logger(__file__)
@@ -208,13 +207,16 @@ class Saml2AuthRequestView(View, PrepareRequestMixin):
log_prompt = "Process SAML GET requests: {}" log_prompt = "Process SAML GET requests: {}"
logger.debug(log_prompt.format('Start')) logger.debug(log_prompt.format('Start'))
request_params = request.GET.dict()
try: try:
saml_instance = self.init_saml_auth(request) saml_instance = self.init_saml_auth(request)
except OneLogin_Saml2_Error as error: except OneLogin_Saml2_Error as error:
logger.error(log_prompt.format('Init saml auth error: %s' % error)) logger.error(log_prompt.format('Init saml auth error: %s' % error))
return HttpResponse(error, status=412) return HttpResponse(error, status=412)
next_url = settings.AUTH_SAML2_PROVIDER_AUTHORIZATION_ENDPOINT next_url = request_params.get('next') or settings.AUTH_SAML2_PROVIDER_AUTHORIZATION_ENDPOINT
next_url = safe_next_url(next_url, request=request)
url = saml_instance.login(return_to=next_url) url = saml_instance.login(return_to=next_url)
logger.debug(log_prompt.format('Redirect login url')) logger.debug(log_prompt.format('Redirect login url'))
return HttpResponseRedirect(url) return HttpResponseRedirect(url)
@@ -293,10 +295,11 @@ class Saml2AuthCallbackView(View, PrepareRequestMixin, FlashMessageMixin):
return response return response
logger.debug(log_prompt.format('Redirect')) logger.debug(log_prompt.format('Redirect'))
redir = post_data.get('RelayState') relay_state = post_data.get('RelayState')
if not redir or len(redir) == 0: if not relay_state or len(relay_state) == 0:
redir = "/" relay_state = "/"
next_url = saml_instance.redirect_to(redir) next_url = saml_instance.redirect_to(relay_state)
next_url = safe_next_url(next_url, request=request)
return HttpResponseRedirect(next_url) return HttpResponseRedirect(next_url)
@csrf_exempt @csrf_exempt
@@ -304,10 +307,6 @@ class Saml2AuthCallbackView(View, PrepareRequestMixin, FlashMessageMixin):
return super().dispatch(*args, **kwargs) return super().dispatch(*args, **kwargs)
class Saml2AuthCallbackClientView(BaseAuthCallbackClientView):
pass
class Saml2AuthMetadataView(View, PrepareRequestMixin): class Saml2AuthMetadataView(View, PrepareRequestMixin):
def get(self, request): def get(self, request):

View File

@@ -1,6 +1,9 @@
from django.db.models import TextChoices from django.db.models import TextChoices
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
USER_LOGIN_GUARD_VIEW_REDIRECT_FIELD = 'next'
RSA_PRIVATE_KEY = 'rsa_private_key' RSA_PRIVATE_KEY = 'rsa_private_key'
RSA_PUBLIC_KEY = 'rsa_public_key' RSA_PUBLIC_KEY = 'rsa_public_key'

View File

@@ -0,0 +1,193 @@
"""
This module provides decorators to handle redirect URLs during the authentication flow:
1. pre_save_next_to_session: Captures and stores the intended next URL before redirecting to auth provider
2. redirect_to_pre_save_next_after_auth: Redirects to the stored next URL after successful authentication
3. post_save_next_to_session: Copies the stored next URL to session['next'] after view execution
"""
from urllib.parse import urlparse
from django.http import HttpResponseRedirect
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from functools import wraps
from common.utils import get_logger, safe_next_url
from .const import USER_LOGIN_GUARD_VIEW_REDIRECT_FIELD
logger = get_logger(__file__)
__all__ = [
'pre_save_next_to_session', 'redirect_to_pre_save_next_after_auth',
'post_save_next_to_session_if_guard_redirect'
]
# Session key for storing the redirect URL after authentication
AUTH_SESSION_NEXT_URL_KEY = 'auth_next_url'
def pre_save_next_to_session(get_next_url=None):
"""
Decorator to capture and store the 'next' parameter into session BEFORE view execution.
This decorator is applied to the authentication request view to preserve the user's
intended destination URL before redirecting to the authentication provider.
Args:
get_next_url: Optional callable that extracts the next URL from request.
Default: lambda req: req.GET.get('next')
Usage:
# Use default (request.GET.get('next'))
@pre_save_next_to_session()
def get(self, request):
pass
# Custom extraction from POST data
@pre_save_next_to_session(get_next_url=lambda req: req.POST.get('next'))
def post(self, request):
pass
# Custom extraction from both GET and POST
@pre_save_next_to_session(
get_next_url=lambda req: req.GET.get('next') or req.POST.get('next')
)
def get(self, request):
pass
Example flow:
User accesses: /auth/oauth2/?next=/dashboard/
↓ (decorator saves '/dashboard/' to session)
Redirected to OAuth2 provider for authentication
"""
# Default function to extract next URL from request.GET
if get_next_url is None:
get_next_url = lambda req: req.GET.get('next')
def decorator(view_func):
@wraps(view_func)
def wrapper(self, request, *args, **kwargs):
next_url = get_next_url(request)
if next_url:
request.session[AUTH_SESSION_NEXT_URL_KEY] = next_url
logger.debug(f"[Auth] Saved next_url to session: {next_url}")
return view_func(self, request, *args, **kwargs)
return wrapper
return decorator
def redirect_to_pre_save_next_after_auth(view_func):
"""
Decorator to redirect to the previously saved 'next' URL after successful authentication.
This decorator is applied to the authentication callback view. After the user successfully
authenticates, if a 'next' URL was previously saved in the session (by pre_save_next_to_session),
the user will be redirected to that URL instead of the default redirect location.
Conditions for redirect:
- User must be authenticated (request.user.is_authenticated)
- Session must contain the saved next URL (AUTH_SESSION_NEXT_URL_KEY)
- The next URL must not be '/' (avoid unnecessary redirects)
- The next URL must pass security validation (safe_next_url)
If any condition fails, returns the original view response.
Usage:
@redirect_to_pre_save_next_after_auth
def get(self, request):
# Process authentication callback
if user_authenticated:
auth.login(request, user)
return HttpResponseRedirect(default_url)
Example flow:
User redirected back from OAuth2 provider: /auth/oauth2/callback/?code=xxx
↓ (view processes authentication, user becomes authenticated)
Decorator checks session for saved next URL
↓ (finds '/dashboard/' in session)
Redirects to: /dashboard/
↓ (clears saved URL from session)
"""
@wraps(view_func)
def wrapper(self, request, *args, **kwargs):
# Execute the original view method first
response = view_func(self, request, *args, **kwargs)
# Check if user has been authenticated
if request.user and request.user.is_authenticated:
# Check if session contains a saved next URL
saved_next_url = request.session.get(AUTH_SESSION_NEXT_URL_KEY)
if saved_next_url and saved_next_url != '/':
# Validate the URL for security
safe_url = safe_next_url(saved_next_url, request=request)
if safe_url:
# Clear the saved URL from session (one-time use)
request.session.pop(AUTH_SESSION_NEXT_URL_KEY, None)
logger.debug(f"[Auth] Redirecting authenticated user to saved next_url: {safe_url}")
return HttpResponseRedirect(safe_url)
# Return the original response if no redirect conditions are met
return response
return wrapper
def post_save_next_to_session_if_guard_redirect(view_func):
"""
Decorator to copy AUTH_SESSION_NEXT_URL_KEY to session['next'] after view execution,
but only if redirecting to login-guard view.
This decorator is applied AFTER view execution. It copies the value from
AUTH_SESSION_NEXT_URL_KEY (internal storage) to 'next' (standard session key)
for use by downstream code.
Only sets the 'next' session key when the response is a redirect to guard-view
(i.e., response with redirect status code and location path matching login-guard view URL).
Usage:
@post_save_next_to_session_if_guard_redirect
def get(self, request):
# Process the request and return response
if some_condition:
return self.redirect_to_guard_view() # Decorator will copy next to session
return HttpResponseRedirect(url) # Decorator won't copy if not to guard-view
Example flow:
View executes and returns redirect to guard view
↓ (response is redirect with 'login-guard' in Location)
Decorator checks if response is redirect to guard-view and session has saved next URL
↓ (copies AUTH_SESSION_NEXT_URL_KEY to session['next'])
User is redirected to guard-view with 'next' available in session
"""
@wraps(view_func)
def wrapper(self, request, *args, **kwargs):
# Execute the original view method
response = view_func(self, request, *args, **kwargs)
# Check if response is a redirect to guard view
# Redirect responses typically have status codes 301, 302, 303, 307, 308
is_guard_redirect = False
if hasattr(response, 'status_code') and response.status_code in (301, 302, 303, 307, 308):
# Check if the redirect location is to guard view
location = response.get('Location', '')
if location:
# Extract path from location URL (handle both absolute and relative URLs)
parsed_url = urlparse(location)
path = parsed_url.path
# Check if path matches guard view URL pattern
guard_view_url = reverse('authentication:login-guard')
if path == guard_view_url:
is_guard_redirect = True
# Only set 'next' if response is a redirect to guard view
if is_guard_redirect:
# Copy AUTH_SESSION_NEXT_URL_KEY to 'next' if it exists
saved_next_url = request.session.get(AUTH_SESSION_NEXT_URL_KEY)
if saved_next_url:
# 这里 'next' 是 UserLoginGuardView.redirect_field_name
request.session[USER_LOGIN_GUARD_VIEW_REDIRECT_FIELD] = saved_next_url
logger.debug(f"[Auth] Copied {AUTH_SESSION_NEXT_URL_KEY} to 'next' in session: {saved_next_url}")
return response
return wrapper

View File

@@ -50,7 +50,7 @@ class UserLoginForm(forms.Form):
class UserCheckOtpCodeForm(forms.Form): class UserCheckOtpCodeForm(forms.Form):
code = forms.CharField(label=_('MFA Code'), max_length=128, required=False) code = forms.CharField(label=_('MFA Code'), max_length=128, required=False)
mfa_type = forms.CharField(label=_('MFA type'), max_length=128, required=False) mfa_type = forms.CharField(label=_('MFA type'), max_length=128)
class CustomCaptchaTextInput(CaptchaTextInput): class CustomCaptchaTextInput(CaptchaTextInput):

View File

@@ -0,0 +1,75 @@
# -*- coding: utf-8 -*-
#
from django.core.management.base import BaseCommand
from django.db.utils import OperationalError, ProgrammingError
from django.conf import settings
class Command(BaseCommand):
help = 'Initialize OAuth2 Provider - Create default JumpServer Client application'
def add_arguments(self, parser):
parser.add_argument(
'--force',
action='store_true',
help='Force recreate the application even if it exists',
)
def handle(self, *args, **options):
force = options.get('force', False)
try:
from authentication.backends.oauth2_provider.utils import (
get_or_create_jumpserver_client_application
)
from oauth2_provider.models import get_application_model
Application = get_application_model()
# 检查表是否存在
try:
Application.objects.exists()
except (OperationalError, ProgrammingError) as e:
self.stdout.write(
self.style.ERROR(
f'OAuth2 Provider tables not found. Please run migrations first:\n'
f' python manage.py migrate oauth2_provider\n'
f'Error: {e}'
)
)
return
# 如果强制重建,先删除已存在的应用
if force:
deleted_count, _ = Application.objects.filter(
name=settings.OAUTH2_PROVIDER_JUMPSERVER_CLIENT_NAME
).delete()
if deleted_count > 0:
self.stdout.write(
self.style.WARNING(f'Deleted {deleted_count} existing application(s)')
)
# 创建或获取应用
application = get_or_create_jumpserver_client_application()
if application:
self.stdout.write(
self.style.SUCCESS(
f'✓ OAuth2 JumpServer Client application initialized successfully\n'
f' - Client ID: {application.client_id}\n'
f' - Client Type: {application.get_client_type_display()}\n'
f' - Grant Type: {application.get_authorization_grant_type_display()}\n'
f' - Redirect URIs: {application.redirect_uris}\n'
f' - Skip Authorization: {application.skip_authorization}'
)
)
else:
self.stdout.write(
self.style.ERROR('Failed to create OAuth2 application')
)
except Exception as e:
self.stdout.write(
self.style.ERROR(f'Error initializing OAuth2 Provider: {e}')
)
raise

View File

@@ -72,9 +72,10 @@ class BaseMFA(abc.ABC):
def is_active(self): def is_active(self):
return False return False
@classmethod @staticmethod
def global_enabled(cls): @abc.abstractmethod
return cls.name in settings.SECURITY_MFA_ENABLED_BACKENDS def global_enabled():
return False
@abc.abstractmethod @abc.abstractmethod
def get_enable_url(self) -> str: def get_enable_url(self) -> str:

View File

@@ -39,9 +39,9 @@ class MFACustom(BaseMFA):
def is_active(self): def is_active(self):
return True return True
@classmethod @staticmethod
def global_enabled(cls): def global_enabled():
return super().global_enabled() and settings.MFA_CUSTOM and callable(mfa_custom_method) return settings.MFA_CUSTOM and callable(mfa_custom_method)
def get_enable_url(self) -> str: def get_enable_url(self) -> str:
return '' return ''

View File

@@ -50,9 +50,9 @@ class MFAEmail(BaseMFA):
) )
sender_util.gen_and_send_async() sender_util.gen_and_send_async()
@classmethod @staticmethod
def global_enabled(cls): def global_enabled():
return super().global_enabled and settings.SECURITY_MFA_BY_EMAIL return settings.SECURITY_MFA_BY_EMAIL
def disable(self): def disable(self):
return '/ui/#/profile/index' return '/ui/#/profile/index'

View File

@@ -29,10 +29,9 @@ class MFAFace(BaseMFA, AuthFaceMixin):
return True return True
return bool(self.user.face_vector) return bool(self.user.face_vector)
@classmethod @staticmethod
def global_enabled(cls): def global_enabled():
return ( return (
super().global_enabled() and
settings.XPACK_LICENSE_IS_VALID and settings.XPACK_LICENSE_IS_VALID and
settings.XPACK_LICENSE_EDITION_ULTIMATE and settings.XPACK_LICENSE_EDITION_ULTIMATE and
settings.FACE_RECOGNITION_ENABLED settings.FACE_RECOGNITION_ENABLED

View File

@@ -25,6 +25,10 @@ class MFAOtp(BaseMFA):
return True return True
return self.user.otp_secret_key return self.user.otp_secret_key
@staticmethod
def global_enabled():
return True
def get_enable_url(self) -> str: def get_enable_url(self) -> str:
return reverse('authentication:user-otp-enable-start') return reverse('authentication:user-otp-enable-start')

View File

@@ -23,9 +23,9 @@ class MFAPasskey(BaseMFA):
return False return False
return self.user.passkey_set.count() return self.user.passkey_set.count()
@classmethod @staticmethod
def global_enabled(cls): def global_enabled():
return super().global_enabled() and settings.AUTH_PASSKEY return settings.AUTH_PASSKEY
def get_enable_url(self) -> str: def get_enable_url(self) -> str:
return '/ui/#/profile/passkeys' return '/ui/#/profile/passkeys'

View File

@@ -27,9 +27,9 @@ class MFARadius(BaseMFA):
def is_active(self): def is_active(self):
return True return True
@classmethod @staticmethod
def global_enabled(cls): def global_enabled():
return super().global_enabled() and settings.OTP_IN_RADIUS return settings.OTP_IN_RADIUS
def get_enable_url(self) -> str: def get_enable_url(self) -> str:
return '' return ''

View File

@@ -46,9 +46,9 @@ class MFASms(BaseMFA):
def send_challenge(self): def send_challenge(self):
self.sms.gen_and_send_async() self.sms.gen_and_send_async()
@classmethod @staticmethod
def global_enabled(cls): def global_enabled():
return super().global_enabled() and settings.SMS_ENABLED return settings.SMS_ENABLED
def get_enable_url(self) -> str: def get_enable_url(self) -> str:
return '/ui/#/profile/index' return '/ui/#/profile/index'

View File

@@ -36,7 +36,7 @@ class MFAMiddleware:
# 这个是 mfa 登录页需要的请求, 也得放出来, 用户其实已经在 CAS/OIDC 中完成登录了 # 这个是 mfa 登录页需要的请求, 也得放出来, 用户其实已经在 CAS/OIDC 中完成登录了
white_urls = [ white_urls = [
'login/mfa', 'mfa/select', 'face/context', 'jsi18n/', '/static/', 'login/mfa', 'mfa/select', 'face/context', 'jsi18n/', '/static/',
'/profile/otp', '/logout/', '/profile/otp', '/logout/', '/media/'
] ]
for url in white_urls: for url in white_urls:
if request.path.find(url) > -1: if request.path.find(url) > -1:

View File

@@ -6,6 +6,7 @@ import time
import uuid import uuid
from functools import partial from functools import partial
from typing import Callable from typing import Callable
from werkzeug.local import Local
from django.conf import settings from django.conf import settings
from django.contrib import auth from django.contrib import auth
@@ -16,6 +17,7 @@ from django.contrib.auth import (
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.core.cache import cache from django.core.cache import cache
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.db.models import Q
from django.shortcuts import reverse, redirect, get_object_or_404 from django.shortcuts import reverse, redirect, get_object_or_404
from django.utils.http import urlencode from django.utils.http import urlencode
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
@@ -31,6 +33,87 @@ from .signals import post_auth_success, post_auth_failed
logger = get_logger(__name__) logger = get_logger(__name__)
# 模块级别的线程上下文,用于 authenticate 函数中标记当前线程
_auth_thread_context = Local()
# 保存 Django 原始的 get_or_create 方法(在模块加载时保存一次)
def _save_original_get_or_create():
"""保存 Django 原始的 get_or_create 方法"""
from django.contrib.auth import get_user_model as get_user_model_func
UserModel = get_user_model_func()
return UserModel.objects.get_or_create
_django_original_get_or_create = _save_original_get_or_create()
class OnlyAllowExistUserAuthError(Exception):
pass
def _authenticate_context(func):
"""
装饰器:管理 authenticate 函数的执行上下文
功能:
1. 执行前:
- 在线程本地存储中标记当前正在执行 authenticate
- 临时替换 UserModel.objects.get_or_create 方法
2. 执行后:
- 清理线程本地存储标记
- 恢复 get_or_create 为 Django 原始方法
作用:
- 确保 get_or_create 行为仅在 authenticate 生命周期内生效
- 支持 ONLY_ALLOW_EXIST_USER_AUTH 配置的线程安全实现
- 防止跨请求或跨线程的状态污染
"""
from functools import wraps
@wraps(func)
def wrapper(request=None, **credentials):
from django.contrib.auth import get_user_model
UserModel = get_user_model()
def custom_get_or_create(*args, **kwargs):
create_username = kwargs.get('username')
logger.debug(f"get_or_create: thread_id={threading.get_ident()}, username={create_username}")
# 如果当前线程正在执行 authenticate 且仅允许已存在用户认证,则提前判断用户是否存在
if (
getattr(_auth_thread_context, 'in_authenticate', False) and
settings.ONLY_ALLOW_EXIST_USER_AUTH
):
try:
UserModel.objects.get(username=create_username)
except UserModel.DoesNotExist:
raise OnlyAllowExistUserAuthError
# 调用 Django 原始方法(已是绑定方法,直接传参)
return _django_original_get_or_create(*args, **kwargs)
try:
# 执行前:设置线程上下文和 monkey-patch
setattr(_auth_thread_context, 'in_authenticate', True)
UserModel.objects.get_or_create = custom_get_or_create
# 执行原函数
return func(request, **credentials)
finally:
# 执行后:清理线程上下文和恢复原始方法
try:
if hasattr(_auth_thread_context, 'in_authenticate'):
delattr(_auth_thread_context, 'in_authenticate')
except Exception:
pass
try:
UserModel.objects.get_or_create = _django_original_get_or_create
except Exception:
pass
return wrapper
def _get_backends(return_tuples=False): def _get_backends(return_tuples=False):
backends = [] backends = []
@@ -48,39 +131,16 @@ def _get_backends(return_tuples=False):
return backends return backends
class OnlyAllowExistUserAuthError(Exception):
pass
auth._get_backends = _get_backends auth._get_backends = _get_backends
@_authenticate_context
def authenticate(request=None, **credentials): def authenticate(request=None, **credentials):
""" """
If the given credentials are valid, return a User object. If the given credentials are valid, return a User object.
之所以 hack 这个 authenticate
""" """
UserModel = get_user_model()
original_get_or_create = UserModel.objects.get_or_create
thread_local = threading.local()
thread_local.thread_id = threading.get_ident()
def custom_get_or_create(self, *args, **kwargs):
logger.debug(f"get_or_create: thread_id={threading.get_ident()}, username={username}")
if threading.get_ident() != thread_local.thread_id or not settings.ONLY_ALLOW_EXIST_USER_AUTH:
return original_get_or_create(*args, **kwargs)
create_username = kwargs.get('username')
try:
UserModel.objects.get(username=create_username)
except UserModel.DoesNotExist:
raise OnlyAllowExistUserAuthError
return original_get_or_create(*args, **kwargs)
username = credentials.get('username')
temp_user = None temp_user = None
username = credentials.get('username')
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))) logger.info('Try using auth backend: {}'.format(str(backend)))
@@ -94,26 +154,27 @@ def authenticate(request=None, **credentials):
except TypeError: except TypeError:
# This backend doesn't accept these credentials as arguments. Try the next one. # This backend doesn't accept these credentials as arguments. Try the next one.
continue continue
try: try:
UserModel.objects.get_or_create = custom_get_or_create.__get__(UserModel.objects)
user = backend.authenticate(request, **credentials) user = backend.authenticate(request, **credentials)
except PermissionDenied: except PermissionDenied:
# This backend says to stop in our tracks - this user should not be allowed in at all. # This backend says to stop in our tracks - this user should not be allowed in at all.
break break
except OnlyAllowExistUserAuthError: except OnlyAllowExistUserAuthError:
if request:
request.error_message = _( request.error_message = _(
'''The administrator has enabled "Only allow existing users to log in", '''The administrator has enabled "Only allow existing users to log in",
and the current user is not in the user list. Please contact the administrator.''' and the current user is not in the user list. Please contact the administrator.'''
) )
continue continue
finally:
UserModel.objects.get_or_create = original_get_or_create
if user is None: if user is None:
continue continue
if not user.is_valid: if not user.is_valid:
temp_user = user temp_user = user
temp_user.backend = backend_path temp_user.backend = backend_path
if request:
request.error_message = _('User is invalid') request.error_message = _('User is invalid')
return temp_user return temp_user
@@ -129,8 +190,11 @@ def authenticate(request=None, **credentials):
else: else:
if temp_user is not None: if temp_user is not None:
source_display = temp_user.source_display source_display = temp_user.source_display
request.error_message = _('''The administrator has enabled 'Only allow login from user source'. if request:
The current user source is {}. Please contact the administrator.''').format(source_display) request.error_message = _(
''' The administrator has enabled 'Only allow login from user source'.
The current user source is {}. Please contact the administrator. '''
).format(source_display)
return temp_user return temp_user
# The credentials supplied are invalid to all backends, fire signal # The credentials supplied are invalid to all backends, fire signal
@@ -228,7 +292,8 @@ class AuthPreCheckMixin:
if not settings.ONLY_ALLOW_EXIST_USER_AUTH: if not settings.ONLY_ALLOW_EXIST_USER_AUTH:
return return
exist = User.objects.filter(username=username).exists() q = Q(username=username) | Q(email=username)
exist = User.objects.filter(q).exists()
if not exist: if not exist:
logger.error(f"Only allow exist user auth, login failed: {username}") logger.error(f"Only allow exist user auth, login failed: {username}")
self.raise_credential_error(errors.reason_user_not_exist) self.raise_credential_error(errors.reason_user_not_exist)

View File

@@ -9,11 +9,12 @@ from common.utils import get_object_or_none, random_string
from users.models import User from users.models import User
from users.serializers import UserProfileSerializer from users.serializers import UserProfileSerializer
from ..models import AccessKey, TempToken from ..models import AccessKey, TempToken
from oauth2_provider.models import get_access_token_model
__all__ = [ __all__ = [
'AccessKeySerializer', 'BearerTokenSerializer', 'AccessKeySerializer', 'BearerTokenSerializer',
'SSOTokenSerializer', 'TempTokenSerializer', 'SSOTokenSerializer', 'TempTokenSerializer',
'AccessKeyCreateSerializer' 'AccessKeyCreateSerializer', 'AccessTokenSerializer',
] ]
@@ -114,3 +115,28 @@ class TempTokenSerializer(serializers.ModelSerializer):
token = TempToken(**kwargs) token = TempToken(**kwargs)
token.save() token.save()
return token return token
class AccessTokenSerializer(serializers.ModelSerializer):
token_preview = serializers.SerializerMethodField(label=_("Token"))
class Meta:
model = get_access_token_model()
fields = [
'id', 'user', 'token_preview', 'is_valid',
'is_expired', 'expires', 'scope', 'created', 'updated',
]
read_only_fields = fields
extra_kwargs = {
'scope': { 'label': _('Scope') },
'expires': { 'label': _('Date expired') },
'updated': { 'label': _('Date updated') },
'created': { 'label': _('Date created') },
}
def get_token_preview(self, obj):
token_string = obj.token
if len(token_string) > 16:
return f"{token_string[:6]}...{token_string[-4:]}"
return "****"

View File

@@ -9,6 +9,8 @@ from audits.models import UserSession
from common.sessions.cache import user_session_manager from common.sessions.cache import user_session_manager
from .signals import post_auth_success, post_auth_failed, user_auth_failed, user_auth_success from .signals import post_auth_success, post_auth_failed, user_auth_failed, user_auth_success
from .backends.oauth2_provider.signal_handlers import *
@receiver(user_logged_in) @receiver(user_logged_in)
def on_user_auth_login_success(sender, user, request, **kwargs): def on_user_auth_login_success(sender, user, request, **kwargs):
@@ -57,3 +59,4 @@ def on_user_login_success(sender, request, user, backend, create=False, **kwargs
def on_user_login_failed(sender, username, request, reason, backend, **kwargs): def on_user_login_failed(sender, username, request, reason, backend, **kwargs):
request.session['auth_backend'] = backend request.session['auth_backend'] = backend
post_auth_failed.send(sender, username=username, request=request, reason=reason) post_auth_failed.send(sender, username=username, request=request, reason=reason)

View File

@@ -47,3 +47,9 @@ def clean_expire_token():
count = TempToken.objects.filter(date_expired__lt=expired_time).delete() count = TempToken.objects.filter(date_expired__lt=expired_time).delete()
logging.info('Deleted %d temporary tokens.', count[0]) logging.info('Deleted %d temporary tokens.', count[0])
logging.info('Cleaned expired temporary and connection tokens.') logging.info('Cleaned expired temporary and connection tokens.')
@register_as_period_task(crontab=CRONTAB_AT_AM_TWO)
def clear_oauth2_provider_expired_tokens():
from oauth2_provider.models import clear_expired
clear_expired()

View File

@@ -376,7 +376,7 @@
</div> </div>
{% if form.challenge %} {% if form.challenge %}
{% bootstrap_field form.challenge show_label=False %} {% bootstrap_field form.challenge show_label=False %}
{% elif form.mfa_type and mfa_backends %} {% elif form.mfa_type %}
<div class="form-group" style="display: flex"> <div class="form-group" style="display: flex">
{% include '_mfa_login_field.html' %} {% include '_mfa_login_field.html' %}
</div> </div>

View File

@@ -16,6 +16,7 @@ router.register('super-connection-token', api.SuperConnectionTokenViewSet, 'supe
router.register('admin-connection-token', api.AdminConnectionTokenViewSet, 'admin-connection-token') router.register('admin-connection-token', api.AdminConnectionTokenViewSet, 'admin-connection-token')
router.register('confirm', api.UserConfirmationViewSet, 'confirm') router.register('confirm', api.UserConfirmationViewSet, 'confirm')
router.register('ssh-key', api.SSHkeyViewSet, 'ssh-key') router.register('ssh-key', api.SSHkeyViewSet, 'ssh-key')
router.register('access-tokens', api.AccessTokenViewSet, 'access-token')
urlpatterns = [ urlpatterns = [
path('<str:backend>/qr/unbind/', api.QRUnBindForUserApi.as_view(), name='qr-unbind'), path('<str:backend>/qr/unbind/', api.QRUnBindForUserApi.as_view(), name='qr-unbind'),

View File

@@ -83,4 +83,6 @@ urlpatterns = [
path('oauth2/', include(('authentication.backends.oauth2.urls', 'authentication'), namespace='oauth2')), path('oauth2/', include(('authentication.backends.oauth2.urls', 'authentication'), namespace='oauth2')),
path('captcha/', include('captcha.urls')), path('captcha/', include('captcha.urls')),
path('oauth2-provider/', include(('authentication.backends.oauth2_provider.urls', 'authentication'), namespace='oauth2-provider'))
] ]

View File

@@ -11,6 +11,7 @@ from rest_framework.request import Request
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 authentication.decorators import post_save_next_to_session_if_guard_redirect
from common.utils import get_logger from common.utils import get_logger
from common.utils.common import get_request_ip from common.utils.common import get_request_ip
from common.utils.django import reverse, get_object_or_none from common.utils.django import reverse, get_object_or_none
@@ -72,6 +73,7 @@ class BaseLoginCallbackView(AuthMixin, FlashMessageMixin, IMClientMixin, View):
return user, None return user, None
@post_save_next_to_session_if_guard_redirect
def get(self, request: Request): def get(self, request: Request):
code = request.GET.get('code') code = request.GET.get('code')
redirect_url = request.GET.get('redirect_url') redirect_url = request.GET.get('redirect_url')
@@ -110,8 +112,6 @@ class BaseLoginCallbackView(AuthMixin, FlashMessageMixin, IMClientMixin, View):
response = self.get_failed_response(login_url, title=msg, msg=msg) response = self.get_failed_response(login_url, title=msg, msg=msg)
return response return response
if redirect_url and 'next=client' in redirect_url:
self.request.META['QUERY_STRING'] += '&next=client'
return self.redirect_to_guard_view() return self.redirect_to_guard_view()

View File

@@ -10,6 +10,7 @@ from django.views import View
from rest_framework.exceptions import APIException from rest_framework.exceptions import APIException
from rest_framework.permissions import AllowAny, IsAuthenticated from rest_framework.permissions import AllowAny, IsAuthenticated
from authentication.decorators import post_save_next_to_session_if_guard_redirect, pre_save_next_to_session
from authentication import errors from authentication import errors
from authentication.const import ConfirmType from authentication.const import ConfirmType
from authentication.mixins import AuthMixin from authentication.mixins import AuthMixin
@@ -24,7 +25,7 @@ from common.views.mixins import PermissionsMixin, UserConfirmRequiredExceptionMi
from users.models import User from users.models import User
from users.views import UserVerifyPasswordView from users.views import UserVerifyPasswordView
from .base import BaseLoginCallbackView from .base import BaseLoginCallbackView
from .mixins import METAMixin, FlashMessageMixin from .mixins import FlashMessageMixin
logger = get_logger(__file__) logger = get_logger(__file__)
@@ -171,20 +172,18 @@ class DingTalkEnableStartView(UserVerifyPasswordView):
return success_url return success_url
class DingTalkQRLoginView(DingTalkQRMixin, METAMixin, View): class DingTalkQRLoginView(DingTalkQRMixin, View):
permission_classes = (AllowAny,) permission_classes = (AllowAny,)
@pre_save_next_to_session()
def get(self, request: HttpRequest): def get(self, request: HttpRequest):
redirect_url = request.GET.get('redirect_url') or reverse('index') redirect_url = request.GET.get('redirect_url') or reverse('index')
query_string = request.GET.urlencode() query_string = request.GET.urlencode()
redirect_url = f'{redirect_url}?{query_string}' redirect_url = f'{redirect_url}?{query_string}'
next_url = self.get_next_url_from_meta() or reverse('index')
next_url = safe_next_url(next_url, request=request)
redirect_uri = reverse('authentication:dingtalk-qr-login-callback', external=True) redirect_uri = reverse('authentication:dingtalk-qr-login-callback', external=True)
redirect_uri += '?' + urlencode({ redirect_uri += '?' + urlencode({
'redirect_url': redirect_url, 'redirect_url': redirect_url,
'next': next_url,
}) })
url = self.get_qr_url(redirect_uri) url = self.get_qr_url(redirect_uri)
@@ -210,6 +209,7 @@ class DingTalkQRLoginCallbackView(DingTalkQRMixin, BaseLoginCallbackView):
class DingTalkOAuthLoginView(DingTalkOAuthMixin, View): class DingTalkOAuthLoginView(DingTalkOAuthMixin, View):
permission_classes = (AllowAny,) permission_classes = (AllowAny,)
@pre_save_next_to_session()
def get(self, request: HttpRequest): def get(self, request: HttpRequest):
redirect_url = request.GET.get('redirect_url') redirect_url = request.GET.get('redirect_url')
@@ -223,6 +223,7 @@ class DingTalkOAuthLoginView(DingTalkOAuthMixin, View):
class DingTalkOAuthLoginCallbackView(AuthMixin, DingTalkOAuthMixin, View): class DingTalkOAuthLoginCallbackView(AuthMixin, DingTalkOAuthMixin, View):
permission_classes = (AllowAny,) permission_classes = (AllowAny,)
@post_save_next_to_session_if_guard_redirect
def get(self, request: HttpRequest): def get(self, request: HttpRequest):
code = request.GET.get('code') code = request.GET.get('code')
redirect_url = request.GET.get('redirect_url') redirect_url = request.GET.get('redirect_url')

View File

@@ -8,6 +8,7 @@ from django.views import View
from rest_framework.exceptions import APIException from rest_framework.exceptions import APIException
from rest_framework.permissions import AllowAny, IsAuthenticated from rest_framework.permissions import AllowAny, IsAuthenticated
from authentication.decorators import pre_save_next_to_session
from authentication.const import ConfirmType from authentication.const import ConfirmType
from authentication.permissions import UserConfirmation from authentication.permissions import UserConfirmation
from common.sdk.im.feishu import URL from common.sdk.im.feishu import URL
@@ -108,9 +109,12 @@ class FeiShuQRBindCallbackView(FeiShuQRMixin, BaseBindCallbackView):
class FeiShuQRLoginView(FeiShuQRMixin, View): class FeiShuQRLoginView(FeiShuQRMixin, View):
permission_classes = (AllowAny,) permission_classes = (AllowAny,)
@pre_save_next_to_session()
def get(self, request: HttpRequest): def get(self, request: HttpRequest):
redirect_url = request.GET.get('redirect_url') or reverse('index') redirect_url = request.GET.get('redirect_url') or reverse('index')
query_string = request.GET.urlencode() query_string = request.GET.copy()
query_string.pop('next', None)
query_string = query_string.urlencode()
redirect_url = f'{redirect_url}?{query_string}' redirect_url = f'{redirect_url}?{query_string}'
redirect_uri = reverse(f'authentication:{self.category}-qr-login-callback', external=True) redirect_uri = reverse(f'authentication:{self.category}-qr-login-callback', external=True)
redirect_uri += '?' + urlencode({ redirect_uri += '?' + urlencode({

View File

@@ -29,7 +29,7 @@ from users.utils import (
redirect_user_first_login_or_index redirect_user_first_login_or_index
) )
from .. import mixins, errors from .. import mixins, errors
from ..const import RSA_PRIVATE_KEY, RSA_PUBLIC_KEY from ..const import RSA_PRIVATE_KEY, RSA_PUBLIC_KEY, USER_LOGIN_GUARD_VIEW_REDIRECT_FIELD
from ..forms import get_user_login_form_cls from ..forms import get_user_login_form_cls
from ..utils import get_auth_methods from ..utils import get_auth_methods
@@ -260,7 +260,7 @@ class UserLoginView(mixins.AuthMixin, UserLoginContextMixin, FormView):
class UserLoginGuardView(mixins.AuthMixin, RedirectView): class UserLoginGuardView(mixins.AuthMixin, RedirectView):
redirect_field_name = 'next' redirect_field_name = USER_LOGIN_GUARD_VIEW_REDIRECT_FIELD
login_url = reverse_lazy('authentication:login') login_url = reverse_lazy('authentication:login')
login_mfa_url = reverse_lazy('authentication:login-mfa') login_mfa_url = reverse_lazy('authentication:login-mfa')
login_confirm_url = reverse_lazy('authentication:login-wait-confirm') login_confirm_url = reverse_lazy('authentication:login-wait-confirm')

View File

@@ -4,17 +4,6 @@ from django.utils.translation import gettext_lazy as _
from common.utils import FlashMessageUtil from common.utils import FlashMessageUtil
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
class FlashMessageMixin: class FlashMessageMixin:
@staticmethod @staticmethod
def get_response(redirect_url='', title='', msg='', m_type='message', interval=5): def get_response(redirect_url='', title='', msg='', m_type='message', interval=5):

View File

@@ -8,6 +8,7 @@ from rest_framework.exceptions import APIException
from rest_framework.permissions import AllowAny, IsAuthenticated from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.request import Request from rest_framework.request import Request
from authentication.decorators import pre_save_next_to_session
from authentication.const import ConfirmType from authentication.const import ConfirmType
from authentication.permissions import UserConfirmation from authentication.permissions import UserConfirmation
from common.sdk.im.slack import URL, SLACK_REDIRECT_URI_SESSION_KEY from common.sdk.im.slack import URL, SLACK_REDIRECT_URI_SESSION_KEY
@@ -96,9 +97,12 @@ class SlackEnableStartView(UserVerifyPasswordView):
class SlackQRLoginView(SlackMixin, View): class SlackQRLoginView(SlackMixin, View):
permission_classes = (AllowAny,) permission_classes = (AllowAny,)
@pre_save_next_to_session()
def get(self, request: Request): def get(self, request: Request):
redirect_url = request.GET.get('redirect_url') or reverse('index') redirect_url = request.GET.get('redirect_url') or reverse('index')
query_string = request.GET.urlencode() query_string = request.GET.copy()
query_string.pop('next', None)
query_string = query_string.urlencode()
redirect_url = f'{redirect_url}?{query_string}' redirect_url = f'{redirect_url}?{query_string}'
redirect_uri = reverse('authentication:slack-qr-login-callback', external=True) redirect_uri = reverse('authentication:slack-qr-login-callback', external=True)
redirect_uri += '?' + urlencode({ redirect_uri += '?' + urlencode({

View File

@@ -12,6 +12,7 @@ from authentication import errors
from authentication.const import ConfirmType from authentication.const import ConfirmType
from authentication.mixins import AuthMixin from authentication.mixins import AuthMixin
from authentication.permissions import UserConfirmation from authentication.permissions import UserConfirmation
from authentication.decorators import post_save_next_to_session_if_guard_redirect, pre_save_next_to_session
from common.sdk.im.wecom import URL from common.sdk.im.wecom import URL
from common.sdk.im.wecom import WeCom, wecom_tool from common.sdk.im.wecom import WeCom, wecom_tool
from common.utils import get_logger from common.utils import get_logger
@@ -20,7 +21,7 @@ from common.views.mixins import UserConfirmRequiredExceptionMixin, PermissionsMi
from users.models import User from users.models import User
from users.views import UserVerifyPasswordView from users.views import UserVerifyPasswordView
from .base import BaseLoginCallbackView, BaseBindCallbackView from .base import BaseLoginCallbackView, BaseBindCallbackView
from .mixins import METAMixin, FlashMessageMixin from .mixins import FlashMessageMixin
logger = get_logger(__file__) logger = get_logger(__file__)
@@ -115,19 +116,14 @@ class WeComEnableStartView(UserVerifyPasswordView):
return success_url return success_url
class WeComQRLoginView(WeComQRMixin, METAMixin, View): class WeComQRLoginView(WeComQRMixin, View):
permission_classes = (AllowAny,) permission_classes = (AllowAny,)
@pre_save_next_to_session()
def get(self, request: HttpRequest): def get(self, request: HttpRequest):
redirect_url = request.GET.get('redirect_url') or reverse('index') redirect_url = request.GET.get('redirect_url') or reverse('index')
next_url = self.get_next_url_from_meta() or reverse('index')
next_url = safe_next_url(next_url, request=request)
redirect_uri = reverse('authentication:wecom-qr-login-callback', external=True) redirect_uri = reverse('authentication:wecom-qr-login-callback', external=True)
redirect_uri += '?' + urlencode({ redirect_uri += '?' + urlencode({'redirect_url': redirect_url})
'redirect_url': redirect_url,
'next': next_url,
})
url = self.get_qr_url(redirect_uri) url = self.get_qr_url(redirect_uri)
return HttpResponseRedirect(url) return HttpResponseRedirect(url)
@@ -148,12 +144,11 @@ class WeComQRLoginCallbackView(WeComQRMixin, BaseLoginCallbackView):
class WeComOAuthLoginView(WeComOAuthMixin, View): class WeComOAuthLoginView(WeComOAuthMixin, View):
permission_classes = (AllowAny,) permission_classes = (AllowAny,)
@pre_save_next_to_session()
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-oauth-login-callback', external=True) redirect_uri = reverse('authentication:wecom-oauth-login-callback', external=True)
redirect_uri += '?' + urlencode({'redirect_url': redirect_url}) redirect_uri += '?' + urlencode({'redirect_url': redirect_url})
url = self.get_oauth_url(redirect_uri) url = self.get_oauth_url(redirect_uri)
return HttpResponseRedirect(url) return HttpResponseRedirect(url)
@@ -161,6 +156,7 @@ class WeComOAuthLoginView(WeComOAuthMixin, View):
class WeComOAuthLoginCallbackView(AuthMixin, WeComOAuthMixin, View): class WeComOAuthLoginCallbackView(AuthMixin, WeComOAuthMixin, View):
permission_classes = (AllowAny,) permission_classes = (AllowAny,)
@post_save_next_to_session_if_guard_redirect
def get(self, request: HttpRequest): def get(self, request: HttpRequest):
code = request.GET.get('code') code = request.GET.get('code')
redirect_url = request.GET.get('redirect_url') redirect_url = request.GET.get('redirect_url')

View File

@@ -183,6 +183,7 @@ class BaseFileRenderer(LogMixin, BaseRenderer):
for item in data: for item in data:
row = [] row = []
for field in render_fields: for field in render_fields:
field._row = item
value = item.get(field.field_name) value = item.get(field.field_name)
value = self.render_value(field, value) value = self.render_value(field, value)
row.append(value) row.append(value)

View File

@@ -1,13 +1,12 @@
import re import re
import uuid
import time import time
import uuid
from django.conf import settings from django.conf import settings
from django.contrib.auth import get_user_model
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from django.test import Client from django.test import Client
from django.urls import URLPattern, URLResolver from django.urls import URLPattern, URLResolver
from django.contrib.auth import get_user_model
from django.contrib.auth.models import AnonymousUser
from jumpserver.urls import api_v1 from jumpserver.urls import api_v1
@@ -81,7 +80,8 @@ known_unauth_urls = [
"/api/v1/authentication/mfa/send-code/", "/api/v1/authentication/mfa/send-code/",
"/api/v1/authentication/sso/login/", "/api/v1/authentication/sso/login/",
"/api/v1/authentication/user-session/", "/api/v1/authentication/user-session/",
"/api/v1/settings/i18n/zh-hans/" "/api/v1/settings/i18n/zh-hans/",
"/api/v1/settings/client/versions/"
] ]
known_error_urls = [ known_error_urls = [

View File

@@ -1,9 +1,12 @@
import os
from ctypes import * from ctypes import *
from .exception import PiicoError from .exception import PiicoError
from .session import Session from .session import Session
from .cipher import * from .cipher import *
from .digest import * from .digest import *
from django.core.cache import cache
from redis_lock import Lock as RedisLock
class Device: class Device:
@@ -15,6 +18,7 @@ class Device:
self.__load_driver(driver_path) self.__load_driver(driver_path)
# open device # open device
self.__open_device() self.__open_device()
self.__reset_key_store()
def close(self): def close(self):
if self.__device is None: if self.__device is None:
@@ -68,3 +72,30 @@ class Device:
if ret != 0: if ret != 0:
raise PiicoError("open piico device failed", ret) raise PiicoError("open piico device failed", ret)
self.__device = device self.__device = device
def __reset_key_store(self):
redis_client = cache.client.get_client()
server_hostname = os.environ.get("SERVER_HOSTNAME")
RESET_LOCK_KEY = f"spiico:{server_hostname}:reset"
LOCK_EXPIRE_SECONDS = 300
if self._driver is None:
raise PiicoError("no driver loaded", 0)
if self.__device is None:
raise PiicoError("device not open", 0)
# ---- 分布式锁Redis-Lock 实现 Redlock ----
lock = RedisLock(
redis_client,
RESET_LOCK_KEY,
expire=LOCK_EXPIRE_SECONDS, # 锁自动过期
auto_renewal=False, # 不自动续租
)
# 尝试获取锁,拿不到直接返回
if not lock.acquire(blocking=False):
return
# ---- 真正执行 reset ----
ret = self._driver.SPII_ResetModule(self.__device)
if ret != 0:
raise PiicoError("reset device failed", ret)

View File

@@ -192,6 +192,7 @@ class WeCom(RequestMixin):
class WeComTool(object): class WeComTool(object):
WECOM_STATE_SESSION_KEY = '_wecom_state' WECOM_STATE_SESSION_KEY = '_wecom_state'
WECOM_STATE_VALUE = 'wecom' WECOM_STATE_VALUE = 'wecom'
WECOM_STATE_NEXT_URL_KEY = 'wecom_oauth_next_url'
@lazyproperty @lazyproperty
def qr_cb_url(self): def qr_cb_url(self):

View File

@@ -30,12 +30,30 @@ __all__ = [
"CommonSerializerMixin", "CommonSerializerMixin",
"CommonBulkSerializerMixin", "CommonBulkSerializerMixin",
"SecretReadableMixin", "SecretReadableMixin",
"SecretReadableCheckMixin",
"CommonModelSerializer", "CommonModelSerializer",
"CommonBulkModelSerializer", "CommonBulkModelSerializer",
"ResourceLabelsMixin", "ResourceLabelsMixin",
] ]
class SecretReadableCheckMixin(serializers.Serializer):
"""
根据 SECURITY_ACCOUNT_SECRET_READ 配置控制密码字段的可读性
当配置为 False 时,密码字段返回 None
"""
def to_representation(self, instance):
ret = super().to_representation(instance)
if not settings.SECURITY_ACCOUNT_SECRET_READ:
secret_fields = getattr(self.Meta, 'secret_fields', ['secret'])
for field_name in secret_fields:
if field_name in ret:
ret[field_name] = '<REDACTED>'
return ret
class SecretReadableMixin(serializers.Serializer): class SecretReadableMixin(serializers.Serializer):
"""加密字段 (EncryptedField) 可读性""" """加密字段 (EncryptedField) 可读性"""

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -280,7 +280,8 @@
"CACertificate": "Ca certificate", "CACertificate": "Ca certificate",
"CAS": "CAS", "CAS": "CAS",
"CMPP2": "Cmpp v2.0", "CMPP2": "Cmpp v2.0",
"CTYunPrivate": "eCloud Private Cloud", "CTYun": "State Cloud",
"CTYunPrivate": "State Cloud(Private)",
"CalculationResults": "Error in cron expression", "CalculationResults": "Error in cron expression",
"CallRecords": "Call Records", "CallRecords": "Call Records",
"CanDragSelect": "Select by dragging; Empty means all selected", "CanDragSelect": "Select by dragging; Empty means all selected",
@@ -1634,5 +1635,9 @@
"selectedAssets": "Selected assets", "selectedAssets": "Selected assets",
"setVariable": "Set variable", "setVariable": "Set variable",
"userId": "User ID", "userId": "User ID",
"userName": "User name" "userName": "User name",
"AccountSecretReadDisabled": "Account secret reading has been disabled by administrator",
"AccessToken": "Access tokens",
"AccessTokenTip": "Access Token is a temporary credential generated through the OAuth2 (Authorization Code Grant) flow using the JumpServer client, which is used to access protected resources.",
"Revoke": "Revoke"
} }

View File

@@ -8,6 +8,8 @@
"AccessDistribution": "Distribución de visitas", "AccessDistribution": "Distribución de visitas",
"AccessIP": "Lista blanca de IP", "AccessIP": "Lista blanca de IP",
"AccessKey": "Clave de acceso", "AccessKey": "Clave de acceso",
"AccessToken": "Token de acceso",
"AccessTokenTip": "El token de acceso es un certificado temporal generado a través del cliente JumpServer utilizando el flujo de OAuth2 (autorización por código) para acceder a recursos protegidos.",
"Account": "Información de la cuenta", "Account": "Información de la cuenta",
"AccountActivities": "Actividad de la cuenta", "AccountActivities": "Actividad de la cuenta",
"AccountAndPasswordChangeRank": "Clasificación de cambio de contraseña de cuenta", "AccountAndPasswordChangeRank": "Clasificación de cambio de contraseña de cuenta",
@@ -44,6 +46,7 @@
"AccountPushUpdate": "Actualizar la notificación de la cuenta", "AccountPushUpdate": "Actualizar la notificación de la cuenta",
"AccountReport": "Informe de cuentas", "AccountReport": "Informe de cuentas",
"AccountResult": "Cambio de contraseña de cuenta exitoso/fallido", "AccountResult": "Cambio de contraseña de cuenta exitoso/fallido",
"AccountSecretReadDisabled": "La función de lectura de nombre de usuario y contraseña ha sido desactivada por el administrador",
"AccountSelectHelpText": "La lista de cuentas agrega el nombre de usuario de la cuenta. Tipo de contraseña.", "AccountSelectHelpText": "La lista de cuentas agrega el nombre de usuario de la cuenta. Tipo de contraseña.",
"AccountSessions": "Sesión de cuenta", "AccountSessions": "Sesión de cuenta",
"AccountStatisticsReport": "Informe estadístico de cuentas", "AccountStatisticsReport": "Informe estadístico de cuentas",
@@ -279,6 +282,7 @@
"CACertificate": "Certificado CA", "CACertificate": "Certificado CA",
"CAS": "CAS", "CAS": "CAS",
"CMPP2": "CMPP v2.0", "CMPP2": "CMPP v2.0",
"CTYun": "Tianyi Cloud",
"CTYunPrivate": "eCloud Nube Privada", "CTYunPrivate": "eCloud Nube Privada",
"CalculationResults": "Error en la expresión cron", "CalculationResults": "Error en la expresión cron",
"CallRecords": "Registro de llamadas", "CallRecords": "Registro de llamadas",
@@ -1176,6 +1180,8 @@
"RetrySelected": "Reintentar selección", "RetrySelected": "Reintentar selección",
"Review": "Revisión", "Review": "Revisión",
"Reviewer": "Aprobador", "Reviewer": "Aprobador",
"Revoke": "Revocar",
"Risk": "Riesgo",
"RiskDetection": "Detección de riesgos", "RiskDetection": "Detección de riesgos",
"RiskDetectionDetail": "Detalles de detección de riesgos", "RiskDetectionDetail": "Detalles de detección de riesgos",
"RiskyAccount": "Cuenta de riesgo", "RiskyAccount": "Cuenta de riesgo",
@@ -1639,6 +1645,7 @@
"overwriteProtocolsAndPortsMsg": "Esta acción reemplazará todos los protocolos y puertos, ¿continuar?", "overwriteProtocolsAndPortsMsg": "Esta acción reemplazará todos los protocolos y puertos, ¿continuar?",
"pleaseSelectAssets": "Por favor, seleccione un activo.", "pleaseSelectAssets": "Por favor, seleccione un activo.",
"removeWarningMsg": "¿Está seguro de que desea eliminar?", "removeWarningMsg": "¿Está seguro de que desea eliminar?",
"selectFiles": "Se ha seleccionado el archivo número {number}",
"selectedAssets": "Activos seleccionados", "selectedAssets": "Activos seleccionados",
"setVariable": "configurar parámetros", "setVariable": "configurar parámetros",
"userId": "ID de usuario", "userId": "ID de usuario",

View File

@@ -8,6 +8,8 @@
"AccessDistribution": "アクセス分布", "AccessDistribution": "アクセス分布",
"AccessIP": "IP ホワイトリスト", "AccessIP": "IP ホワイトリスト",
"AccessKey": "アクセスキー", "AccessKey": "アクセスキー",
"AccessToken": "アクセス・トークン",
"AccessTokenTip": "アクセス・トークンは、JumpServer クライアントを通じて OAuth2Authorization Code Grantフローを使用して生成される一時的な証明書であり、保護されたリソースへのアクセスに使用されます。",
"Account": "アカウント情報", "Account": "アカウント情報",
"AccountActivities": "アカウント活動", "AccountActivities": "アカウント活動",
"AccountAmount": "アカウント数", "AccountAmount": "アカウント数",
@@ -46,6 +48,7 @@
"AccountPushUpdate": "アカウント更新プッシュ", "AccountPushUpdate": "アカウント更新プッシュ",
"AccountReport": "アカウントレポート", "AccountReport": "アカウントレポート",
"AccountResult": "アカウントパスワード変更成功/失敗", "AccountResult": "アカウントパスワード変更成功/失敗",
"AccountSecretReadDisabled": "アカウントパスワードの読み取り機能は管理者によって無効になっています。",
"AccountSelectHelpText": "アカウント一覧に追加されている内容は、アカウントのユーザー名", "AccountSelectHelpText": "アカウント一覧に追加されている内容は、アカウントのユーザー名",
"AccountSessions": " アカウントセッション ", "AccountSessions": " アカウントセッション ",
"AccountStatisticsReport": "アカウント統計レポート", "AccountStatisticsReport": "アカウント統計レポート",
@@ -283,6 +286,7 @@
"CACertificate": "CA 証明書", "CACertificate": "CA 証明書",
"CAS": "CAS", "CAS": "CAS",
"CMPP2": "CMPP v2.0", "CMPP2": "CMPP v2.0",
"CTYun": "天翼クラウド",
"CTYunPrivate": "イークラウド・プライベートクラウド", "CTYunPrivate": "イークラウド・プライベートクラウド",
"CalculationResults": "cron 式のエラー", "CalculationResults": "cron 式のエラー",
"CallRecords": "つうわきろく", "CallRecords": "つうわきろく",
@@ -1181,6 +1185,8 @@
"RetrySelected": "選択したものを再試行", "RetrySelected": "選択したものを再試行",
"Review": "審査", "Review": "審査",
"Reviewer": "承認者", "Reviewer": "承認者",
"Revoke": "取り消し",
"Risk": "リスク",
"RiskDetection": "リスク検出", "RiskDetection": "リスク検出",
"RiskDetectionDetail": "リスク検出の詳細", "RiskDetectionDetail": "リスク検出の詳細",
"RiskyAccount": "リスクアカウント", "RiskyAccount": "リスクアカウント",
@@ -1644,6 +1650,7 @@
"overwriteProtocolsAndPortsMsg": "この操作はすべてのプロトコルとポートを上書きしますが、続行してよろしいですか?", "overwriteProtocolsAndPortsMsg": "この操作はすべてのプロトコルとポートを上書きしますが、続行してよろしいですか?",
"pleaseSelectAssets": "資産を選択してください", "pleaseSelectAssets": "資産を選択してください",
"removeWarningMsg": "削除してもよろしいですか", "removeWarningMsg": "削除してもよろしいですか",
"selectFiles": "{number}ファイルを選択しました。",
"selectedAssets": "選択した資産", "selectedAssets": "選択した資産",
"setVariable": "パラメータ設定", "setVariable": "パラメータ設定",
"userId": "ユーザーID", "userId": "ユーザーID",

View File

@@ -8,6 +8,8 @@
"AccessDistribution": "방문 분포", "AccessDistribution": "방문 분포",
"AccessIP": "IP 화이트리스트", "AccessIP": "IP 화이트리스트",
"AccessKey": "액세스 키", "AccessKey": "액세스 키",
"AccessToken": "접속 토큰",
"AccessTokenTip": "접속 토큰은 JumpServer 클라이언트를 통해 OAuth2(인증 코드 인증) 프로세스를 사용하여 생성된 임시 자격 증명으로, 보호된 리소스에 접근하는 데 사용됩니다.",
"Account": "계정", "Account": "계정",
"AccountActivities": "계정 활동", "AccountActivities": "계정 활동",
"AccountAndPasswordChangeRank": "계정 비밀번호 변경 순위", "AccountAndPasswordChangeRank": "계정 비밀번호 변경 순위",
@@ -44,6 +46,7 @@
"AccountPushUpdate": "계정 업데이트 푸시", "AccountPushUpdate": "계정 업데이트 푸시",
"AccountReport": "계정 보고서", "AccountReport": "계정 보고서",
"AccountResult": "계정 비밀번호 변경 성공/실패", "AccountResult": "계정 비밀번호 변경 성공/실패",
"AccountSecretReadDisabled": "계정 비밀번호 읽기 기능은 관리자가 비활성화하였습니다.",
"AccountSelectHelpText": "계정 목록에 추가하는 것은 계정의 사용자 이름—암호 유형입니다.", "AccountSelectHelpText": "계정 목록에 추가하는 것은 계정의 사용자 이름—암호 유형입니다.",
"AccountSessions": "계정 세션", "AccountSessions": "계정 세션",
"AccountStatisticsReport": "계정 통계 보고서", "AccountStatisticsReport": "계정 통계 보고서",
@@ -279,6 +282,7 @@
"CACertificate": "CA 인증서", "CACertificate": "CA 인증서",
"CAS": "CAS", "CAS": "CAS",
"CMPP2": "CMPP v2.0", "CMPP2": "CMPP v2.0",
"CTYun": "천윳 클라우드",
"CTYunPrivate": "천翼 개인 클라우드", "CTYunPrivate": "천翼 개인 클라우드",
"CalculationResults": "cron 표현식 오류", "CalculationResults": "cron 표현식 오류",
"CallRecords": "호출 기록", "CallRecords": "호출 기록",
@@ -1176,6 +1180,8 @@
"RetrySelected": "선택한 항목 재시도", "RetrySelected": "선택한 항목 재시도",
"Review": "검토", "Review": "검토",
"Reviewer": "승인자", "Reviewer": "승인자",
"Revoke": "취소",
"Risk": "위험",
"RiskDetection": "위험 감지", "RiskDetection": "위험 감지",
"RiskDetectionDetail": "위험 감지 상세 정보", "RiskDetectionDetail": "위험 감지 상세 정보",
"RiskyAccount": "위험 계정", "RiskyAccount": "위험 계정",
@@ -1639,6 +1645,7 @@
"overwriteProtocolsAndPortsMsg": "이 작업은 모든 프로토콜과 포트를 덮어씌우게 됩니다. 계속하시겠습니까?", "overwriteProtocolsAndPortsMsg": "이 작업은 모든 프로토콜과 포트를 덮어씌우게 됩니다. 계속하시겠습니까?",
"pleaseSelectAssets": "자산을 선택해 주세요", "pleaseSelectAssets": "자산을 선택해 주세요",
"removeWarningMsg": "제거할 것인지 확실합니까?", "removeWarningMsg": "제거할 것인지 확실합니까?",
"selectFiles": "선택한 파일 {number}개",
"selectedAssets": "선택한 자산", "selectedAssets": "선택한 자산",
"setVariable": "설정 매개변수", "setVariable": "설정 매개변수",
"userId": "사용자 ID", "userId": "사용자 ID",

View File

@@ -8,6 +8,8 @@
"AccessDistribution": "Distribuição de Acesso", "AccessDistribution": "Distribuição de Acesso",
"AccessIP": "Lista branca de IP", "AccessIP": "Lista branca de IP",
"AccessKey": "Chave de Acesso", "AccessKey": "Chave de Acesso",
"AccessToken": "Token de acesso",
"AccessTokenTip": "O token de acesso é um credencial temporário gerado pelo cliente JumpServer usando o fluxo OAuth2 (autorização por código), utilizado para acessar recursos protegidos.",
"Account": "Informações da conta", "Account": "Informações da conta",
"AccountActivities": "Atividades da conta", "AccountActivities": "Atividades da conta",
"AccountAndPasswordChangeRank": "Alteração de Senha por Classificação", "AccountAndPasswordChangeRank": "Alteração de Senha por Classificação",
@@ -44,6 +46,7 @@
"AccountPushUpdate": " Atualização de notificação de conta ", "AccountPushUpdate": " Atualização de notificação de conta ",
"AccountReport": "Relatório de Contas", "AccountReport": "Relatório de Contas",
"AccountResult": "Alteração de senha da conta bem-sucedida/falhada", "AccountResult": "Alteração de senha da conta bem-sucedida/falhada",
"AccountSecretReadDisabled": "A funcionalidade de leitura de nome de usuário e senha foi desativada pelo administrador",
"AccountSelectHelpText": "A lista de contas inclui o nome de usuário da conta", "AccountSelectHelpText": "A lista de contas inclui o nome de usuário da conta",
"AccountSessions": "Conta de sessão ", "AccountSessions": "Conta de sessão ",
"AccountStatisticsReport": "Relatório de Estatísticas de Contas", "AccountStatisticsReport": "Relatório de Estatísticas de Contas",
@@ -280,6 +283,7 @@
"CACertificate": " Certificado CA", "CACertificate": " Certificado CA",
"CAS": "CAS", "CAS": "CAS",
"CMPP2": "CMPP v2.0", "CMPP2": "CMPP v2.0",
"CTYun": "Tianyi Cloud",
"CTYunPrivate": " eCloud Nuvem Privada", "CTYunPrivate": " eCloud Nuvem Privada",
"CalculationResults": "Erro de expressão cron", "CalculationResults": "Erro de expressão cron",
"CallRecords": "Registro de chamadas", "CallRecords": "Registro de chamadas",
@@ -1177,6 +1181,8 @@
"RetrySelected": "repetir a seleção", "RetrySelected": "repetir a seleção",
"Review": "Revisar", "Review": "Revisar",
"Reviewer": "Aprovador", "Reviewer": "Aprovador",
"Revoke": "Revogar",
"Risk": "Risco",
"RiskDetection": " Detecção de risco ", "RiskDetection": " Detecção de risco ",
"RiskDetectionDetail": "Detalhes da Detecção de Risco", "RiskDetectionDetail": "Detalhes da Detecção de Risco",
"RiskyAccount": " Conta de risco ", "RiskyAccount": " Conta de risco ",
@@ -1640,6 +1646,7 @@
"overwriteProtocolsAndPortsMsg": "Esta ação substituirá todos os protocolos e portas. Deseja continuar?", "overwriteProtocolsAndPortsMsg": "Esta ação substituirá todos os protocolos e portas. Deseja continuar?",
"pleaseSelectAssets": "Por favor, selecione um ativo.", "pleaseSelectAssets": "Por favor, selecione um ativo.",
"removeWarningMsg": "Tem certeza de que deseja remover", "removeWarningMsg": "Tem certeza de que deseja remover",
"selectFiles": "Foram selecionados {number} arquivos",
"selectedAssets": "Ativos selecionados", "selectedAssets": "Ativos selecionados",
"setVariable": "Parâmetros de configuração", "setVariable": "Parâmetros de configuração",
"userId": "ID do usuário", "userId": "ID do usuário",

View File

@@ -8,6 +8,8 @@
"AccessDistribution": "Распределение доступа", "AccessDistribution": "Распределение доступа",
"AccessIP": "Белый список IP", "AccessIP": "Белый список IP",
"AccessKey": "Ключ доступа", "AccessKey": "Ключ доступа",
"AccessToken": "Токен доступа",
"AccessTokenTip": "Токен доступа создается через клиент JumpServer с использованием процесса OAuth2 (авторизация через код) в качестве временного удостоверения для доступа к защищенным ресурсам.",
"Account": "Информация об УЗ", "Account": "Информация об УЗ",
"AccountActivities": "Активность учетной записи", "AccountActivities": "Активность учетной записи",
"AccountAndPasswordChangeRank": "Рейтинг изменений паролей и учётных записей", "AccountAndPasswordChangeRank": "Рейтинг изменений паролей и учётных записей",
@@ -44,6 +46,7 @@
"AccountPushUpdate": "Обновление УЗ для публикации", "AccountPushUpdate": "Обновление УЗ для публикации",
"AccountReport": "Отчет по УЗ", "AccountReport": "Отчет по УЗ",
"AccountResult": "Успешное или неудачное изменение секрета УЗ", "AccountResult": "Успешное или неудачное изменение секрета УЗ",
"AccountSecretReadDisabled": "Функция чтения логина и пароля была отключена администратором",
"AccountSelectHelpText": "В списке учетных записей отображается имя пользователя", "AccountSelectHelpText": "В списке учетных записей отображается имя пользователя",
"AccountSessions": "Сессии учетной записи", "AccountSessions": "Сессии учетной записи",
"AccountStatisticsReport": "Отчет по учетным записям", "AccountStatisticsReport": "Отчет по учетным записям",
@@ -279,6 +282,7 @@
"CACertificate": "Сертификат ЦС", "CACertificate": "Сертификат ЦС",
"CAS": "CAS", "CAS": "CAS",
"CMPP2": "CMPP v2.0", "CMPP2": "CMPP v2.0",
"CTYun": "Tianyi Cloud",
"CTYunPrivate": "eCloud Private Cloud", "CTYunPrivate": "eCloud Private Cloud",
"CalculationResults": "Ошибка в выражении cron", "CalculationResults": "Ошибка в выражении cron",
"CallRecords": "Запись вызовов", "CallRecords": "Запись вызовов",
@@ -1176,6 +1180,8 @@
"RetrySelected": "Повторить выбранное", "RetrySelected": "Повторить выбранное",
"Review": "Требовать одобрения", "Review": "Требовать одобрения",
"Reviewer": "Утверждающий", "Reviewer": "Утверждающий",
"Revoke": "Отмена",
"Risk": "Риск",
"RiskDetection": "Выявление рисков", "RiskDetection": "Выявление рисков",
"RiskDetectionDetail": "Детали обнаружения риска", "RiskDetectionDetail": "Детали обнаружения риска",
"RiskyAccount": "УЗ с риском", "RiskyAccount": "УЗ с риском",
@@ -1639,6 +1645,7 @@
"overwriteProtocolsAndPortsMsg": "Это действие заменит все протоколы и порты. Продолжить?", "overwriteProtocolsAndPortsMsg": "Это действие заменит все протоколы и порты. Продолжить?",
"pleaseSelectAssets": "Пожалуйста, выберите актив", "pleaseSelectAssets": "Пожалуйста, выберите актив",
"removeWarningMsg": "Вы уверены, что хотите удалить", "removeWarningMsg": "Вы уверены, что хотите удалить",
"selectFiles": "Выбрано {number} файлов",
"selectedAssets": "Выбранные активы", "selectedAssets": "Выбранные активы",
"setVariable": "Задать переменную", "setVariable": "Задать переменную",
"userId": "ID пользователя", "userId": "ID пользователя",

View File

@@ -8,6 +8,8 @@
"AccessDistribution": "Phân bố truy cập", "AccessDistribution": "Phân bố truy cập",
"AccessIP": "Danh sách trắng IP", "AccessIP": "Danh sách trắng IP",
"AccessKey": "Khóa truy cập", "AccessKey": "Khóa truy cập",
"AccessToken": "Mã thông hành",
"AccessTokenTip": "Mã thông hành là giấy chứng nhận tạm thời được tạo ra thông qua quy trình OAuth2 (ủy quyền mã) sử dụng khách hàng JumpServer, dùng để truy cập tài nguyên được bảo vệ.",
"Account": "Tài khoản", "Account": "Tài khoản",
"AccountActivities": "Tài khoản hoạt động", "AccountActivities": "Tài khoản hoạt động",
"AccountAndPasswordChangeRank": "Thay đổi mật khẩu tài khoản xếp hạng", "AccountAndPasswordChangeRank": "Thay đổi mật khẩu tài khoản xếp hạng",
@@ -44,6 +46,7 @@
"AccountPushUpdate": "Cập nhật thông tin tài khoản", "AccountPushUpdate": "Cập nhật thông tin tài khoản",
"AccountReport": "Báo cáo tài khoản", "AccountReport": "Báo cáo tài khoản",
"AccountResult": "Thay đổi mật khẩu tài khoản thành công/thất bại", "AccountResult": "Thay đổi mật khẩu tài khoản thành công/thất bại",
"AccountSecretReadDisabled": "Chức năng đọc tài khoản mật khẩu đã bị quản lý bởi quản trị viên vô hiệu hóa",
"AccountSelectHelpText": "Danh sách tài khoản thêm tên người dùng của tài khoản", "AccountSelectHelpText": "Danh sách tài khoản thêm tên người dùng của tài khoản",
"AccountSessions": "Phiên tài khoản", "AccountSessions": "Phiên tài khoản",
"AccountStatisticsReport": "Báo cáo thống kê tài khoản", "AccountStatisticsReport": "Báo cáo thống kê tài khoản",
@@ -279,6 +282,7 @@
"CACertificate": "Chứng chỉ CA", "CACertificate": "Chứng chỉ CA",
"CAS": "CAS", "CAS": "CAS",
"CMPP2": "CMPP v2.0", "CMPP2": "CMPP v2.0",
"CTYun": "Điện toán đám mây Thiên Vân",
"CTYunPrivate": "Đám mây riêng Tianyi", "CTYunPrivate": "Đám mây riêng Tianyi",
"CalculationResults": "Biểu thức cron sai", "CalculationResults": "Biểu thức cron sai",
"CallRecords": "Ghi chép gọi", "CallRecords": "Ghi chép gọi",
@@ -1176,6 +1180,8 @@
"RetrySelected": "Thử lại đã chọn", "RetrySelected": "Thử lại đã chọn",
"Review": "Xem xét", "Review": "Xem xét",
"Reviewer": "Người phê duyệt", "Reviewer": "Người phê duyệt",
"Revoke": "Huỷ bỏ",
"Risk": "Rủi ro",
"RiskDetection": "Phát hiện rủi ro", "RiskDetection": "Phát hiện rủi ro",
"RiskDetectionDetail": "Chi tiết phát hiện rủi ro", "RiskDetectionDetail": "Chi tiết phát hiện rủi ro",
"RiskyAccount": "Tài khoản rủi ro", "RiskyAccount": "Tài khoản rủi ro",
@@ -1639,6 +1645,7 @@
"overwriteProtocolsAndPortsMsg": "Hành động này sẽ ghi đè lên tất cả các giao thức và cổng, có tiếp tục không?", "overwriteProtocolsAndPortsMsg": "Hành động này sẽ ghi đè lên tất cả các giao thức và cổng, có tiếp tục không?",
"pleaseSelectAssets": "Vui lòng chọn tài sản.", "pleaseSelectAssets": "Vui lòng chọn tài sản.",
"removeWarningMsg": "Bạn có chắc chắn muốn xóa bỏ?", "removeWarningMsg": "Bạn có chắc chắn muốn xóa bỏ?",
"selectFiles": "Đã chọn chọn {number} tệp tin",
"selectedAssets": "Tài sản đã chọn", "selectedAssets": "Tài sản đã chọn",
"setVariable": "Cài đặt tham số", "setVariable": "Cài đặt tham số",
"userId": "ID người dùng", "userId": "ID người dùng",

View File

@@ -279,6 +279,7 @@
"CACertificate": "CA 证书", "CACertificate": "CA 证书",
"CAS": "CAS", "CAS": "CAS",
"CMPP2": "CMPP v2.0", "CMPP2": "CMPP v2.0",
"CTYun": "天翼云",
"CTYunPrivate": "天翼私有云", "CTYunPrivate": "天翼私有云",
"CalculationResults": "cron 表达式错误", "CalculationResults": "cron 表达式错误",
"CallRecords": "调用记录", "CallRecords": "调用记录",
@@ -1644,5 +1645,9 @@
"userId": "用户ID", "userId": "用户ID",
"userName": "用户名", "userName": "用户名",
"Risk": "风险", "Risk": "风险",
"selectFiles": "已选择选择{number}文件" "selectFiles": "已选择选择{number}文件",
"AccessToken": "访问令牌",
"AccessTokenTip": "访问令牌是通过 JumpServer 客户端使用 OAuth2授权码授权流程生成的临时凭证用于访问受保护的资源。",
"Revoke": "撤销",
"AccountSecretReadDisabled": "账号密码读取功能已被管理员禁用"
} }

View File

@@ -8,6 +8,8 @@
"AccessDistribution": "訪問分布", "AccessDistribution": "訪問分布",
"AccessIP": "IP 白名單", "AccessIP": "IP 白名單",
"AccessKey": "訪問金鑰", "AccessKey": "訪問金鑰",
"AccessToken": "訪問令牌",
"AccessTokenTip": "訪問令牌是透過 JumpServer 客戶端使用 OAuth2授權碼授權流程生成的臨時憑證用於訪問受保護的資源。",
"Account": "雲帳號", "Account": "雲帳號",
"AccountActivities": "帳號活動", "AccountActivities": "帳號活動",
"AccountAmount": "帳號數量", "AccountAmount": "帳號數量",
@@ -46,6 +48,7 @@
"AccountPushUpdate": "更新帳號推送", "AccountPushUpdate": "更新帳號推送",
"AccountReport": "帳號報表", "AccountReport": "帳號報表",
"AccountResult": "帳號改密成功/失敗", "AccountResult": "帳號改密成功/失敗",
"AccountSecretReadDisabled": "帳號密碼讀取功能已被管理員禁用",
"AccountSelectHelpText": "帳號清單所加入的是使用者名稱", "AccountSelectHelpText": "帳號清單所加入的是使用者名稱",
"AccountSessions": "帳號會話", "AccountSessions": "帳號會話",
"AccountStatisticsReport": "帳號統計報告", "AccountStatisticsReport": "帳號統計報告",
@@ -283,6 +286,7 @@
"CACertificate": "CA 證書", "CACertificate": "CA 證書",
"CAS": "CAS", "CAS": "CAS",
"CMPP2": "CMPP v2.0", "CMPP2": "CMPP v2.0",
"CTYun": "天翼雲",
"CTYunPrivate": "天翼私有雲", "CTYunPrivate": "天翼私有雲",
"CalculationResults": "呼叫記錄", "CalculationResults": "呼叫記錄",
"CallRecords": "調用記錄", "CallRecords": "調用記錄",
@@ -1181,6 +1185,8 @@
"RetrySelected": "重新嘗試所選", "RetrySelected": "重新嘗試所選",
"Review": "審查", "Review": "審查",
"Reviewer": "審批人", "Reviewer": "審批人",
"Revoke": "撤銷",
"Risk": "風險",
"RiskDetection": "風險檢測", "RiskDetection": "風險檢測",
"RiskDetectionDetail": "風險檢測詳情", "RiskDetectionDetail": "風險檢測詳情",
"RiskyAccount": "風險帳號", "RiskyAccount": "風險帳號",
@@ -1644,6 +1650,7 @@
"overwriteProtocolsAndPortsMsg": "此操作將覆蓋所有協議和端口,是否繼續?", "overwriteProtocolsAndPortsMsg": "此操作將覆蓋所有協議和端口,是否繼續?",
"pleaseSelectAssets": "請選擇資產", "pleaseSelectAssets": "請選擇資產",
"removeWarningMsg": "你確定要移除", "removeWarningMsg": "你確定要移除",
"selectFiles": "已選擇{number}文件",
"selectedAssets": "已選資產", "selectedAssets": "已選資產",
"setVariable": "設置參數", "setVariable": "設置參數",
"userId": "用戶ID", "userId": "用戶ID",

View File

@@ -288,5 +288,6 @@
"start time": "Start time", "start time": "Start time",
"success": "Success", "success": "Success",
"system user": "System user", "system user": "System user",
"user": "User" "user": "User",
"tabLimits": "15 tabs are currently open.\nTo ensure system stability, would you like to open Luna in a new browser tab to continue?"
} }

View File

@@ -288,5 +288,6 @@
"start time": "Hora de inicio", "start time": "Hora de inicio",
"success": "Éxito", "success": "Éxito",
"system user": "Usuario del sistema", "system user": "Usuario del sistema",
"tabLimits": "Actualmente tienes 15 pestañas abiertas. \n¿Para garantizar la estabilidad del sistema, deberías abrir Luna en una nueva pestaña del navegador para continuar con la operación?",
"user": "Usuario" "user": "Usuario"
} }

View File

@@ -288,5 +288,6 @@
"start time": "開始時間", "start time": "開始時間",
"success": "成功", "success": "成功",
"system user": "システムユーザー", "system user": "システムユーザー",
"tabLimits": "現在、15個のタブが開かれています。システムの安定性を確保するために、新しいブラウザのタブでLunaを開いて操作を続けますか",
"user": "ユーザー" "user": "ユーザー"
} }

View File

@@ -288,5 +288,6 @@
"start time": "시작 시간", "start time": "시작 시간",
"success": "성공", "success": "성공",
"system user": "시스템 사용자", "system user": "시스템 사용자",
"tabLimits": "현재 15개의 탭이 열려 있습니다. 시스템의 안정성을 위해 Luna를 계속 사용하려면 새로운 브라우저 탭에서 열어보시겠습니까?",
"user": "사용자" "user": "사용자"
} }

View File

@@ -288,5 +288,6 @@
"start time": "Hora de início", "start time": "Hora de início",
"success": " Sucesso", "success": " Sucesso",
"system user": "Usuário do Sistema", "system user": "Usuário do Sistema",
"tabLimits": "Atualmente, 15 abas estão abertas. Para garantir a estabilidade do sistema, você deseja abrir o Luna em uma nova aba do navegador para continuar a operação?",
"user": "Usuário" "user": "Usuário"
} }

View File

@@ -288,5 +288,6 @@
"start time": "время начала", "start time": "время начала",
"success": "успешно", "success": "успешно",
"system user": "системный пользователь", "system user": "системный пользователь",
"tabLimits": "В данный момент открыто 15 вкладок. \nЧтобы обеспечить стабильность системы, стоит ли открыть Luna в новой вкладке браузера для продолжения работы?",
"user": "пользователь" "user": "пользователь"
} }

View File

@@ -288,5 +288,6 @@
"start time": "Thời gian bắt đầu", "start time": "Thời gian bắt đầu",
"success": "Thành công", "success": "Thành công",
"system user": "Tên đăng nhập", "system user": "Tên đăng nhập",
"tabLimits": "Hiện tại đã mở 15 tab. \nĐể đảm bảo tính ổn định của hệ thống, có qua tab trình duyệt mới để mở Luna và tiếp tục thao tác không?",
"user": "Người dùng" "user": "Người dùng"
} }

View File

@@ -288,5 +288,6 @@
"start time": "开始时间", "start time": "开始时间",
"success": "成功", "success": "成功",
"system user": "系统用户", "system user": "系统用户",
"user": "用户" "user": "用户",
"tabLimits": "当前已打开 15 个标签页。\n为保证系统稳定是否在新的浏览器标签页中打开 Luna 以继续操作?"
} }

View File

@@ -289,5 +289,6 @@
"start time": "開始時間", "start time": "開始時間",
"success": "成功", "success": "成功",
"system user": "系統用戶", "system user": "系統用戶",
"tabLimits": "當前已打開 15 個標籤頁。 \n為了確保系統穩定是否在新的瀏覽器標籤頁中打開 Luna 以繼續操作?",
"user": "用戶" "user": "用戶"
} }

View File

@@ -13,11 +13,11 @@ import json
import logging import logging
import os import os
import re import re
import sys
import types import types
from importlib import import_module from importlib import import_module
from urllib.parse import urljoin, urlparse, quote from urllib.parse import urljoin, urlparse, quote
import sys
import yaml import yaml
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@@ -381,7 +381,6 @@ class Config(dict):
'CAS_USERNAME_ATTRIBUTE': 'cas:user', 'CAS_USERNAME_ATTRIBUTE': 'cas:user',
'CAS_APPLY_ATTRIBUTES_TO_USER': False, 'CAS_APPLY_ATTRIBUTES_TO_USER': False,
'CAS_RENAME_ATTRIBUTES': {'cas:user': 'username'}, 'CAS_RENAME_ATTRIBUTES': {'cas:user': 'username'},
'CAS_CREATE_USER': True,
'CAS_ORG_IDS': [DEFAULT_ID], 'CAS_ORG_IDS': [DEFAULT_ID],
'AUTH_SSO': False, 'AUTH_SSO': False,
@@ -569,13 +568,14 @@ class Config(dict):
'SAFE_MODE': False, 'SAFE_MODE': False,
'SECURITY_MFA_AUTH': 0, # 0 不开启 1 全局开启 2 管理员开启 'SECURITY_MFA_AUTH': 0, # 0 不开启 1 全局开启 2 管理员开启
'SECURITY_MFA_AUTH_ENABLED_FOR_THIRD_PARTY': True, 'SECURITY_MFA_AUTH_ENABLED_FOR_THIRD_PARTY': True,
'SECURITY_MFA_ENABLED_BACKENDS': [], 'SECURITY_MFA_BY_EMAIL': False,
'SECURITY_COMMAND_EXECUTION': False, 'SECURITY_COMMAND_EXECUTION': False,
'SECURITY_COMMAND_BLACKLIST': [ 'SECURITY_COMMAND_BLACKLIST': [
'reboot', 'shutdown', 'poweroff', 'halt', 'dd', 'half', 'top' 'reboot', 'shutdown', 'poweroff', 'halt', 'dd', 'half', 'top'
], ],
'SECURITY_SERVICE_ACCOUNT_REGISTRATION': 'auto', 'SECURITY_SERVICE_ACCOUNT_REGISTRATION': 'auto',
'SECURITY_VIEW_AUTH_NEED_MFA': True, 'SECURITY_VIEW_AUTH_NEED_MFA': True,
'SECURITY_ACCOUNT_SECRET_READ': True,
'SECURITY_MAX_IDLE_TIME': 30, 'SECURITY_MAX_IDLE_TIME': 30,
'SECURITY_MAX_SESSION_TIME': 24, 'SECURITY_MAX_SESSION_TIME': 24,
'SECURITY_PASSWORD_EXPIRATION_TIME': 9999, 'SECURITY_PASSWORD_EXPIRATION_TIME': 9999,
@@ -699,6 +699,7 @@ class Config(dict):
'LIMIT_SUPER_PRIV': False, 'LIMIT_SUPER_PRIV': False,
# Chat AI # Chat AI
'IS_CUSTOM_MODEL': False,
'CHAT_AI_ENABLED': False, 'CHAT_AI_ENABLED': False,
'CHAT_AI_METHOD': 'api', 'CHAT_AI_METHOD': 'api',
'CHAT_AI_EMBED_URL': '', 'CHAT_AI_EMBED_URL': '',
@@ -707,10 +708,12 @@ class Config(dict):
'GPT_API_KEY': '', 'GPT_API_KEY': '',
'GPT_PROXY': '', 'GPT_PROXY': '',
'GPT_MODEL': 'gpt-4o-mini', 'GPT_MODEL': 'gpt-4o-mini',
'CUSTOM_GPT_MODEL': 'gpt-4o-mini',
'DEEPSEEK_BASE_URL': '', 'DEEPSEEK_BASE_URL': '',
'DEEPSEEK_API_KEY': '', 'DEEPSEEK_API_KEY': '',
'DEEPSEEK_PROXY': '', 'DEEPSEEK_PROXY': '',
'DEEPSEEK_MODEL': 'deepseek-chat', 'DEEPSEEK_MODEL': 'deepseek-chat',
'CUSTOM_DEEPSEEK_MODEL': 'deepseek-chat',
'VIRTUAL_APP_ENABLED': False, 'VIRTUAL_APP_ENABLED': False,
'FILE_UPLOAD_SIZE_LIMIT_MB': 200, 'FILE_UPLOAD_SIZE_LIMIT_MB': 200,
@@ -718,11 +721,6 @@ class Config(dict):
'TICKET_APPLY_ASSET_SCOPE': 'all', 'TICKET_APPLY_ASSET_SCOPE': 'all',
'LEAK_PASSWORD_DB_PATH': os.path.join(PROJECT_DIR, 'data', 'system', 'leak_passwords.db'), 'LEAK_PASSWORD_DB_PATH': os.path.join(PROJECT_DIR, 'data', 'system', 'leak_passwords.db'),
# Ansible Receptor
'RECEPTOR_ENABLED': False,
'ANSIBLE_RECEPTOR_GATEWAY_PROXY_HOST': 'jms_celery',
'ANSIBLE_RECEPTOR_TCP_LISTEN_ADDRESS': 'receptor:7521',
'FILE_UPLOAD_TEMP_DIR': None, 'FILE_UPLOAD_TEMP_DIR': None,
'LOKI_LOG_ENABLED': False, 'LOKI_LOG_ENABLED': False,
@@ -735,6 +733,10 @@ class Config(dict):
# MCP # MCP
'MCP_ENABLED': False, 'MCP_ENABLED': False,
# oauth2_provider settings
'OAUTH2_PROVIDER_ACCESS_TOKEN_EXPIRE_SECONDS': 60 * 60,
'OAUTH2_PROVIDER_REFRESH_TOKEN_EXPIRE_SECONDS': 60 * 60 * 24 * 7,
} }
old_config_map = { old_config_map = {

View File

@@ -151,8 +151,13 @@ class SafeRedirectMiddleware:
if not (300 <= response.status_code < 400): if not (300 <= response.status_code < 400):
return response return response
if request.resolver_match and request.resolver_match.namespace.startswith('authentication'): if (
# 认证相关的路由跳过验证core/auth/xxxx request.resolver_match and
request.resolver_match.namespace.startswith('authentication') and
not request.resolver_match.namespace.startswith('authentication:oauth2-provider')
):
# 认证相关的路由跳过验证 /core/auth/...,
# 但 oauth2-provider 除外, 因为它会重定向到第三方客户端, 希望给出更友好的提示
return response return response
location = response.get('Location') location = response.get('Location')
if not location: if not location:

View File

@@ -159,7 +159,8 @@ CAS_CHECK_NEXT = lambda _next_page: True
CAS_USERNAME_ATTRIBUTE = CONFIG.CAS_USERNAME_ATTRIBUTE CAS_USERNAME_ATTRIBUTE = CONFIG.CAS_USERNAME_ATTRIBUTE
CAS_APPLY_ATTRIBUTES_TO_USER = CONFIG.CAS_APPLY_ATTRIBUTES_TO_USER CAS_APPLY_ATTRIBUTES_TO_USER = CONFIG.CAS_APPLY_ATTRIBUTES_TO_USER
CAS_RENAME_ATTRIBUTES = CONFIG.CAS_RENAME_ATTRIBUTES CAS_RENAME_ATTRIBUTES = CONFIG.CAS_RENAME_ATTRIBUTES
CAS_CREATE_USER = CONFIG.CAS_CREATE_USER CAS_CREATE_USER = True
CAS_STORE_NEXT = True
# SSO auth # SSO auth
AUTH_SSO = CONFIG.AUTH_SSO AUTH_SSO = CONFIG.AUTH_SSO

View File

@@ -130,6 +130,7 @@ INSTALLED_APPS = [
'settings.apps.SettingsConfig', 'settings.apps.SettingsConfig',
'terminal.apps.TerminalConfig', 'terminal.apps.TerminalConfig',
'audits.apps.AuditsConfig', 'audits.apps.AuditsConfig',
'oauth2_provider',
'authentication.apps.AuthenticationConfig', # authentication 'authentication.apps.AuthenticationConfig', # authentication
'tickets.apps.TicketsConfig', 'tickets.apps.TicketsConfig',
'acls.apps.AclsConfig', 'acls.apps.AclsConfig',

View File

@@ -60,6 +60,7 @@ VERIFY_CODE_TTL = CONFIG.VERIFY_CODE_TTL
SECURITY_MFA_VERIFY_TTL = CONFIG.SECURITY_MFA_VERIFY_TTL SECURITY_MFA_VERIFY_TTL = CONFIG.SECURITY_MFA_VERIFY_TTL
SECURITY_UNCOMMON_USERS_TTL = CONFIG.SECURITY_UNCOMMON_USERS_TTL SECURITY_UNCOMMON_USERS_TTL = CONFIG.SECURITY_UNCOMMON_USERS_TTL
SECURITY_VIEW_AUTH_NEED_MFA = CONFIG.SECURITY_VIEW_AUTH_NEED_MFA SECURITY_VIEW_AUTH_NEED_MFA = CONFIG.SECURITY_VIEW_AUTH_NEED_MFA
SECURITY_ACCOUNT_SECRET_READ = CONFIG.SECURITY_ACCOUNT_SECRET_READ
SECURITY_SERVICE_ACCOUNT_REGISTRATION = CONFIG.SECURITY_SERVICE_ACCOUNT_REGISTRATION SECURITY_SERVICE_ACCOUNT_REGISTRATION = CONFIG.SECURITY_SERVICE_ACCOUNT_REGISTRATION
SECURITY_LOGIN_CAPTCHA_ENABLED = CONFIG.SECURITY_LOGIN_CAPTCHA_ENABLED SECURITY_LOGIN_CAPTCHA_ENABLED = CONFIG.SECURITY_LOGIN_CAPTCHA_ENABLED
SECURITY_MFA_IN_LOGIN_PAGE = CONFIG.SECURITY_MFA_IN_LOGIN_PAGE SECURITY_MFA_IN_LOGIN_PAGE = CONFIG.SECURITY_MFA_IN_LOGIN_PAGE
@@ -238,6 +239,9 @@ LIMIT_SUPER_PRIV = CONFIG.LIMIT_SUPER_PRIV
ASSET_SIZE = 'small' ASSET_SIZE = 'small'
# Chat AI # Chat AI
IS_CUSTOM_MODEL = CONFIG.IS_CUSTOM_MODEL
CUSTOM_GPT_MODEL = CONFIG.CUSTOM_GPT_MODEL
CUSTOM_DEEPSEEK_MODEL = CONFIG.CUSTOM_DEEPSEEK_MODEL
CHAT_AI_ENABLED = CONFIG.CHAT_AI_ENABLED CHAT_AI_ENABLED = CONFIG.CHAT_AI_ENABLED
CHAT_AI_METHOD = CONFIG.CHAT_AI_METHOD CHAT_AI_METHOD = CONFIG.CHAT_AI_METHOD
CHAT_AI_EMBED_URL = CONFIG.CHAT_AI_EMBED_URL CHAT_AI_EMBED_URL = CONFIG.CHAT_AI_EMBED_URL
@@ -257,11 +261,6 @@ FILE_UPLOAD_SIZE_LIMIT_MB = CONFIG.FILE_UPLOAD_SIZE_LIMIT_MB
TICKET_APPLY_ASSET_SCOPE = CONFIG.TICKET_APPLY_ASSET_SCOPE TICKET_APPLY_ASSET_SCOPE = CONFIG.TICKET_APPLY_ASSET_SCOPE
# Ansible Receptor
RECEPTOR_ENABLED = CONFIG.RECEPTOR_ENABLED
ANSIBLE_RECEPTOR_GATEWAY_PROXY_HOST = CONFIG.ANSIBLE_RECEPTOR_GATEWAY_PROXY_HOST
ANSIBLE_RECEPTOR_TCP_LISTEN_ADDRESS = CONFIG.ANSIBLE_RECEPTOR_TCP_LISTEN_ADDRESS
LOKI_LOG_ENABLED = CONFIG.LOKI_LOG_ENABLED LOKI_LOG_ENABLED = CONFIG.LOKI_LOG_ENABLED
LOKI_BASE_URL = CONFIG.LOKI_BASE_URL LOKI_BASE_URL = CONFIG.LOKI_BASE_URL

View File

@@ -2,6 +2,7 @@
# #
import os import os
import time import time
from .base import ( from .base import (
REDIS_SSL_CA, REDIS_SSL_CERT, REDIS_SSL_KEY, REDIS_SSL_REQUIRED, REDIS_USE_SSL, REDIS_SSL_CA, REDIS_SSL_CERT, REDIS_SSL_KEY, REDIS_SSL_REQUIRED, REDIS_USE_SSL,
REDIS_PROTOCOL, REDIS_SENTINEL_SERVICE_NAME, REDIS_SENTINELS, REDIS_SENTINEL_PASSWORD, REDIS_PROTOCOL, REDIS_SENTINEL_SERVICE_NAME, REDIS_SENTINELS, REDIS_SENTINEL_PASSWORD,
@@ -30,10 +31,11 @@ REST_FRAMEWORK = {
), ),
'DEFAULT_AUTHENTICATION_CLASSES': ( 'DEFAULT_AUTHENTICATION_CLASSES': (
# 'rest_framework.authentication.BasicAuthentication', # 'rest_framework.authentication.BasicAuthentication',
'authentication.backends.drf.AccessTokenAuthentication',
'authentication.backends.drf.PrivateTokenAuthentication',
'authentication.backends.drf.ServiceAuthentication', 'authentication.backends.drf.ServiceAuthentication',
'authentication.backends.drf.SignatureAuthentication', 'authentication.backends.drf.SignatureAuthentication',
'authentication.backends.drf.PrivateTokenAuthentication',
'authentication.backends.drf.AccessTokenAuthentication',
"oauth2_provider.contrib.rest_framework.OAuth2Authentication",
'authentication.backends.drf.SessionAuthentication', 'authentication.backends.drf.SessionAuthentication',
), ),
'DEFAULT_FILTER_BACKENDS': ( 'DEFAULT_FILTER_BACKENDS': (
@@ -222,3 +224,17 @@ PIICO_DRIVER_PATH = CONFIG.PIICO_DRIVER_PATH
LEAK_PASSWORD_DB_PATH = CONFIG.LEAK_PASSWORD_DB_PATH LEAK_PASSWORD_DB_PATH = CONFIG.LEAK_PASSWORD_DB_PATH
JUMPSERVER_UPTIME = int(time.time()) JUMPSERVER_UPTIME = int(time.time())
# OAuth2 Provider settings
OAUTH2_PROVIDER = {
'ALLOWED_REDIRECT_URI_SCHEMES': ['https', 'jms'],
'PKCE_REQUIRED': True,
'ACCESS_TOKEN_EXPIRE_SECONDS': CONFIG.OAUTH2_PROVIDER_ACCESS_TOKEN_EXPIRE_SECONDS,
'REFRESH_TOKEN_EXPIRE_SECONDS': CONFIG.OAUTH2_PROVIDER_REFRESH_TOKEN_EXPIRE_SECONDS,
}
OAUTH2_PROVIDER_CLIENT_REDIRECT_URI = 'jms://auth/callback'
OAUTH2_PROVIDER_JUMPSERVER_CLIENT_NAME = 'JumpServer Client'
if CONFIG.DEBUG_DEV:
OAUTH2_PROVIDER['ALLOWED_REDIRECT_URI_SCHEMES'].append('http')
OAUTH2_PROVIDER_CLIENT_REDIRECT_URI += ' http://127.0.0.1:14876/auth/callback'

View File

@@ -115,7 +115,11 @@ LOGGING = {
'azure': { 'azure': {
'handlers': ['null'], 'handlers': ['null'],
'level': 'ERROR' 'level': 'ERROR'
} },
'oauth2_provider': {
'handlers': ['console', 'file'],
'level': LOG_LEVEL,
},
} }
} }

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