Merge pull request #7796 from jumpserver/dev

v2.20.0-rc1
pull/7919/head
老广 2022-03-10 20:34:18 +08:00 committed by GitHub
commit 45331dc9e8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
380 changed files with 7767 additions and 63698 deletions

View File

@ -1,20 +1,14 @@
from common.permissions import IsOrgAdmin, HasQueryParamsUserAndIsCurrentOrgMember
from common.drf.api import JMSBulkModelViewSet
from ..models import LoginACL
from .. import serializers
from ..filters import LoginAclFilter
__all__ = ['LoginACLViewSet', ]
__all__ = ['LoginACLViewSet']
class LoginACLViewSet(JMSBulkModelViewSet):
queryset = LoginACL.objects.all()
filterset_class = LoginAclFilter
search_fields = ('name',)
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.LoginACLSerializer
def get_permissions(self):
if self.action in ["retrieve", "list"]:
self.permission_classes = (IsOrgAdmin, HasQueryParamsUserAndIsCurrentOrgMember)
return super().get_permissions()

View File

@ -1,5 +1,4 @@
from orgs.mixins.api import OrgBulkModelViewSet
from common.permissions import IsOrgAdmin
from .. import models, serializers
@ -10,5 +9,4 @@ class LoginAssetACLViewSet(OrgBulkModelViewSet):
model = models.LoginAssetACL
filterset_fields = ('name', )
search_fields = filterset_fields
permission_classes = (IsOrgAdmin, )
serializer_class = serializers.LoginAssetACLSerializer

View File

@ -1,19 +1,23 @@
from rest_framework.response import Response
from rest_framework.generics import CreateAPIView
from common.permissions import IsAppUser
from common.utils import reverse, lazyproperty
from orgs.utils import tmp_to_org
from tickets.api import GenericTicketStatusRetrieveCloseAPI
from ..models import LoginAssetACL
from .. import serializers
__all__ = ['LoginAssetCheckAPI', 'LoginAssetConfirmStatusAPI']
__all__ = ['LoginAssetCheckAPI']
class LoginAssetCheckAPI(CreateAPIView):
permission_classes = (IsAppUser,)
serializer_class = serializers.LoginAssetCheckSerializer
model = LoginAssetACL
rbac_perms = {
'POST': 'tickets.add_superticket'
}
def get_queryset(self):
return LoginAssetACL.objects.all()
def create(self, request, *args, **kwargs):
is_need_confirm, response_data = self.check_if_need_confirm()
@ -46,7 +50,7 @@ class LoginAssetCheckAPI(CreateAPIView):
org_id=self.serializer.org.id
)
confirm_status_url = reverse(
view_name='api-acls:login-asset-confirm-status',
view_name='api-tickets:super-ticket-status',
kwargs={'pk': str(ticket.id)}
)
ticket_detail_url = reverse(
@ -71,6 +75,3 @@ class LoginAssetCheckAPI(CreateAPIView):
serializer.is_valid(raise_exception=True)
return serializer
class LoginAssetConfirmStatusAPI(GenericTicketStatusRetrieveCloseAPI):
pass

View File

@ -1,5 +1,7 @@
from django.apps import AppConfig
from django.utils.translation import ugettext_lazy as _
class AclsConfig(AppConfig):
name = 'acls'
verbose_name = _('Acls')

View File

@ -0,0 +1,21 @@
# Generated by Django 3.1.13 on 2021-11-30 02:37
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('acls', '0002_auto_20210926_1047'),
]
operations = [
migrations.AlterModelOptions(
name='loginacl',
options={'ordering': ('priority', '-date_updated', 'name'), 'verbose_name': 'Login acl'},
),
migrations.AlterModelOptions(
name='loginassetacl',
options={'ordering': ('priority', '-date_updated', 'name'), 'verbose_name': 'Login asset acl'},
),
]

View File

@ -12,7 +12,6 @@ router.register(r'login-asset-acls', api.LoginAssetACLViewSet, 'login-asset-acl'
urlpatterns = [
path('login-asset/check/', api.LoginAssetCheckAPI.as_view(), name='login-asset-check'),
path('login-asset-confirm/<uuid:pk>/status/', api.LoginAssetConfirmStatusAPI.as_view(), name='login-asset-confirm-status')
]
urlpatterns += router.urls

View File

@ -6,8 +6,9 @@ from django.db.models import F, Q
from common.drf.filters import BaseFilterSet
from common.drf.api import JMSBulkModelViewSet
from rbac.permissions import RBACPermission
from ..models import Account
from ..hands import IsOrgAdminOrAppUser, IsOrgAdmin, NeedMFAVerify
from ..hands import NeedMFAVerify
from .. import serializers
@ -31,7 +32,8 @@ class AccountFilterSet(BaseFilterSet):
username = self.get_query_param('username')
if not username:
return qs
qs = qs.filter(Q(username=username) | Q(systemuser__username=username)).distinct()
q = Q(username=username) | Q(systemuser__username=username)
qs = qs.filter(q).distinct()
return qs
@ -41,7 +43,6 @@ class ApplicationAccountViewSet(JMSBulkModelViewSet):
filterset_class = AccountFilterSet
filterset_fields = ['username', 'app_display', 'type', 'category', 'app']
serializer_class = serializers.AppAccountSerializer
permission_classes = (IsOrgAdmin,)
def get_queryset(self):
queryset = Account.get_queryset()
@ -50,5 +51,9 @@ class ApplicationAccountViewSet(JMSBulkModelViewSet):
class ApplicationAccountSecretViewSet(ApplicationAccountViewSet):
serializer_class = serializers.AppAccountSecretSerializer
permission_classes = [IsOrgAdminOrAppUser, NeedMFAVerify]
permission_classes = [RBACPermission, NeedMFAVerify]
http_method_names = ['get', 'options']
rbac_perms = {
'retrieve': 'applications.view_applicationaccountsecret',
'list': 'applications.view_applicationaccountsecret',
}

View File

@ -7,7 +7,6 @@ from rest_framework.response import Response
from common.tree import TreeNodeSerializer
from common.mixins.api import SuggestionMixin
from ..hands import IsOrgAdminOrAppUser
from .. import serializers
from ..models import Application
@ -22,12 +21,15 @@ class ApplicationViewSet(SuggestionMixin, OrgBulkModelViewSet):
'type': ['exact', 'in'],
}
search_fields = ('name', 'type', 'category')
permission_classes = (IsOrgAdminOrAppUser,)
serializer_classes = {
'default': serializers.AppSerializer,
'get_tree': TreeNodeSerializer,
'suggestion': serializers.MiniAppSerializer
}
rbac_perms = {
'get_tree': 'applications.view_application',
'match': 'assets.match_application'
}
@action(methods=['GET'], detail=False, url_path='tree')
def get_tree(self, request, *args, **kwargs):

View File

@ -2,10 +2,8 @@
#
from orgs.mixins import generics
from ..hands import IsAppUser
from .. import models
from ..serializers import RemoteAppConnectionInfoSerializer
from ..permissions import IsRemoteApp
__all__ = [
@ -15,5 +13,4 @@ __all__ = [
class RemoteAppConnectionInfoApi(generics.RetrieveAPIView):
model = models.Application
permission_classes = (IsAppUser, IsRemoteApp)
serializer_class = RemoteAppConnectionInfoSerializer

View File

@ -1,7 +1,9 @@
from __future__ import unicode_literals
from django.utils.translation import ugettext_lazy as _
from django.apps import AppConfig
class ApplicationsConfig(AppConfig):
name = 'applications'
verbose_name = _('Applications')

View File

@ -1,10 +1,10 @@
# coding: utf-8
#
from django.db.models import TextChoices
from django.db import models
from django.utils.translation import ugettext_lazy as _
class AppCategory(TextChoices):
class AppCategory(models.TextChoices):
db = 'db', _('Database')
remote_app = 'remote_app', _('Remote app')
cloud = 'cloud', 'Cloud'
@ -14,14 +14,15 @@ class AppCategory(TextChoices):
return dict(cls.choices).get(category, '')
class AppType(TextChoices):
class AppType(models.TextChoices):
# db category
mysql = 'mysql', 'MySQL'
redis = 'redis', 'Redis'
oracle = 'oracle', 'Oracle'
pgsql = 'postgresql', 'PostgreSQL'
mariadb = 'mariadb', 'MariaDB'
sqlserver = 'sqlserver', 'SQLServer'
redis = 'redis', 'Redis'
mongodb = 'mongodb', 'MongoDB'
# remote-app category
chrome = 'chrome', 'Chrome'
@ -36,7 +37,7 @@ class AppType(TextChoices):
def category_types_mapper(cls):
return {
AppCategory.db: [
cls.mysql, cls.oracle, cls.redis, cls.pgsql, cls.mariadb, cls.sqlserver
cls.mysql, cls.oracle, cls.pgsql, cls.mariadb, cls.sqlserver, cls.redis, cls.mongodb
],
AppCategory.remote_app: [cls.chrome, cls.mysql_workbench, cls.vmware_client, cls.custom],
AppCategory.cloud: [cls.k8s]

View File

@ -11,5 +11,5 @@
"""
from common.permissions import IsAppUser, IsOrgAdmin, IsValidUser, IsOrgAdminOrAppUser, NeedMFAVerify
from common.permissions import NeedMFAVerify
from users.models import User, UserGroup

View File

@ -0,0 +1,25 @@
# Generated by Django 3.1.13 on 2022-02-17 13:35
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('applications', '0016_auto_20220118_1455'),
]
operations = [
migrations.AlterModelOptions(
name='account',
options={'permissions': [('view_applicationaccountsecret', 'Can view application account secret'), ('change_appplicationaccountsecret', 'Can view application account secret')], 'verbose_name': 'Application account'},
),
migrations.AlterModelOptions(
name='applicationuser',
options={'verbose_name': 'Application user'},
),
migrations.AlterModelOptions(
name='historicalaccount',
options={'get_latest_by': 'history_date', 'ordering': ('-history_date', '-history_id'), 'verbose_name': 'historical Application account'},
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 3.1.13 on 2022-02-23 07:39
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('applications', '0017_auto_20220217_2135'),
]
operations = [
migrations.AlterField(
model_name='application',
name='type',
field=models.CharField(choices=[('mysql', 'MySQL'), ('oracle', 'Oracle'), ('postgresql', 'PostgreSQL'), ('mariadb', 'MariaDB'), ('sqlserver', 'SQLServer'), ('redis', 'Redis'), ('mongodb', 'MongoDB'), ('chrome', 'Chrome'), ('mysql_workbench', 'MySQL Workbench'), ('vmware_client', 'vSphere Client'), ('custom', 'Custom'), ('k8s', 'Kubernetes')], max_length=16, verbose_name='Type'),
),
]

View File

@ -0,0 +1,17 @@
# Generated by Django 3.1.14 on 2022-03-10 10:53
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('applications', '0018_auto_20220223_1539'),
]
operations = [
migrations.AlterModelOptions(
name='application',
options={'ordering': ('name',), 'permissions': [('match_application', 'Can match application')], 'verbose_name': 'Application'},
),
]

View File

@ -20,8 +20,12 @@ class Account(BaseUser):
auth_attrs = ['username', 'password', 'private_key', 'public_key']
class Meta:
verbose_name = _('Account')
verbose_name = _('Application account')
unique_together = [('username', 'app', 'systemuser')]
permissions = [
('view_applicationaccountsecret', _('Can view application account secret')),
('change_appplicationaccountsecret', _('Can view application account secret')),
]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

View File

@ -219,6 +219,9 @@ class Application(CommonModelMixin, OrgModelMixin, ApplicationTreeNodeMixin):
verbose_name = _('Application')
unique_together = [('org_id', 'name')]
ordering = ('name',)
permissions = [
('match_application', _('Can match application')),
]
def __str__(self):
category_display = self.get_category_display()
@ -265,3 +268,4 @@ class Application(CommonModelMixin, OrgModelMixin, ApplicationTreeNodeMixin):
class ApplicationUser(SystemUser):
class Meta:
proxy = True
verbose_name = _('Application user')

View File

@ -119,7 +119,8 @@ class AppAccountSerializer(AppSerializerMixin, AuthSerializerMixin, BulkOrgResou
'username': {'default': '', 'required': False},
'password': {'write_only': True},
'app_display': {'label': _('Application display')},
'systemuser_display': {'label': _('System User')}
'systemuser_display': {'label': _('System User')},
'account': {'label': _('account')}
}
use_model_bulk_create = True
model_bulk_create_kwargs = {

View File

@ -1,10 +1,11 @@
from .mysql import *
from .redis import *
from .mariadb import *
from .oracle import *
from .pgsql import *
from .sqlserver import *
from .redis import *
from .mongodb import *
from .chrome import *
from .mysql_workbench import *

View File

@ -0,0 +1,11 @@
from rest_framework import serializers
from django.utils.translation import ugettext_lazy as _
from ..application_category import DBSerializer
__all__ = ['MongoDBSerializer']
class MongoDBSerializer(DBSerializer):
port = serializers.IntegerField(default=27017, label=_('Port'), allow_null=True)

View File

@ -25,11 +25,12 @@ category_serializer_classes_mapping = {
type_serializer_classes_mapping = {
# db
const.AppType.mysql.value: application_type.MySQLSerializer,
const.AppType.redis.value: application_type.RedisSerializer,
const.AppType.mariadb.value: application_type.MariaDBSerializer,
const.AppType.oracle.value: application_type.OracleSerializer,
const.AppType.pgsql.value: application_type.PostgreSerializer,
const.AppType.sqlserver.value: application_type.SQLServerSerializer,
const.AppType.redis.value: application_type.RedisSerializer,
const.AppType.mongodb.value: application_type.MongoDBSerializer,
# cloud
const.AppType.k8s.value: application_type.K8SSerializer
}

View File

@ -10,4 +10,4 @@ from .domain import *
from .cmd_filter import *
from .gathered_user import *
from .favorite_asset import *
from .backup import *
from .account_backup import *

View File

@ -1,11 +1,9 @@
# -*- coding: utf-8 -*-
#
from rest_framework import status, mixins, viewsets
from rest_framework import status, viewsets
from rest_framework.response import Response
from common.permissions import IsOrgAdmin
from orgs.mixins.api import OrgBulkModelViewSet
from .. import serializers
from ..tasks import execute_account_backup_plan
from ..models import (
@ -24,17 +22,13 @@ class AccountBackupPlanViewSet(OrgBulkModelViewSet):
ordering_fields = ('name',)
ordering = ('name',)
serializer_class = serializers.AccountBackupPlanSerializer
permission_classes = (IsOrgAdmin,)
class AccountBackupPlanExecutionViewSet(
mixins.CreateModelMixin, mixins.ListModelMixin,
mixins.RetrieveModelMixin, viewsets.GenericViewSet
):
class AccountBackupPlanExecutionViewSet(viewsets.ModelViewSet):
serializer_class = serializers.AccountBackupPlanExecutionSerializer
search_fields = ('trigger',)
filterset_fields = ('trigger', 'plan_id')
permission_classes = (IsOrgAdmin,)
http_method_names = ['get', 'post', 'options']
def get_queryset(self):
queryset = AccountBackupPlanExecution.objects.all()

View File

@ -1,13 +1,14 @@
from django.db.models import F, Q
from rest_framework.decorators import action
from django_filters import rest_framework as filters
from rest_framework.response import Response
from django.shortcuts import get_object_or_404
from django_filters import rest_framework as filters
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.generics import CreateAPIView
from orgs.mixins.api import OrgBulkModelViewSet
from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser, NeedMFAVerify
from rbac.permissions import RBACPermission
from common.drf.filters import BaseFilterSet
from common.permissions import NeedMFAVerify
from ..tasks.account_connectivity import test_accounts_connectivity_manual
from ..models import AuthBook, Node
from .. import serializers
@ -62,7 +63,9 @@ class AccountViewSet(OrgBulkModelViewSet):
'default': serializers.AccountSerializer,
'verify_account': serializers.AssetTaskSerializer
}
permission_classes = (IsOrgAdmin,)
rbac_perms = {
'verify_account': 'assets.add_authbook'
}
def get_queryset(self):
queryset = AuthBook.get_queryset()
@ -82,17 +85,23 @@ class AccountSecretsViewSet(AccountViewSet):
serializer_classes = {
'default': serializers.AccountSecretSerializer
}
permission_classes = (IsOrgAdmin, NeedMFAVerify)
http_method_names = ['get']
permission_classes = [RBACPermission, NeedMFAVerify]
rbac_perms = {
'list': 'assets.view_assetaccountsecret',
'retrieve': 'assets.view_assetaccountsecret',
}
class AccountTaskCreateAPI(CreateAPIView):
permission_classes = (IsOrgAdminOrAppUser,)
serializer_class = serializers.AccountTaskSerializer
filterset_fields = AccountViewSet.filterset_fields
search_fields = AccountViewSet.search_fields
filterset_class = AccountViewSet.filterset_class
def check_permissions(self, request):
return request.user.has_perm('assets.test_assetconnectivity')
def get_accounts(self):
queryset = AuthBook.objects.all()
queryset = self.filter_queryset(queryset)
@ -109,5 +118,4 @@ class AccountTaskCreateAPI(CreateAPIView):
def get_exception_handler(self):
def handler(e, context):
return Response({"error": str(e)}, status=400)
return handler

View File

@ -2,9 +2,9 @@ from django.db.models import Count
from orgs.mixins.api import OrgBulkModelViewSet
from common.utils import get_logger
from ..hands import IsOrgAdmin
from ..models import SystemUser
from .. import serializers
from rbac.permissions import RBACPermission
logger = get_logger(__file__)
@ -20,7 +20,7 @@ class AdminUserViewSet(OrgBulkModelViewSet):
filterset_fields = ("name", "username")
search_fields = filterset_fields
serializer_class = serializers.AdminUserSerializer
permission_classes = (IsOrgAdmin,)
permission_classes = (RBACPermission,)
ordering_fields = ('name',)
ordering = ('name', )

View File

@ -1,13 +1,11 @@
# -*- coding: utf-8 -*-
#
from assets.api import FilterAssetByNodeMixin
from rest_framework.viewsets import ModelViewSet
from rest_framework.generics import RetrieveAPIView, ListAPIView
from django.shortcuts import get_object_or_404
from django.db.models import Q
from common.utils import get_logger, get_object_or_none
from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser, IsSuperUser
from common.mixins.api import SuggestionMixin
from users.models import User, UserGroup
from users.serializers import UserSerializer, UserGroupSerializer
@ -17,6 +15,7 @@ from perms.serializers import AssetPermissionSerializer
from perms.filters import AssetPermissionFilter
from orgs.mixins.api import OrgBulkModelViewSet
from orgs.mixins import generics
from assets.api import FilterAssetByNodeMixin
from ..models import Asset, Node, Platform
from .. import serializers
from ..tasks import (
@ -55,7 +54,9 @@ class AssetViewSet(SuggestionMixin, FilterAssetByNodeMixin, OrgBulkModelViewSet)
'default': serializers.AssetSerializer,
'suggestion': serializers.MiniAssetSerializer
}
permission_classes = (IsOrgAdminOrAppUser,)
rbac_perms = {
'match': 'assets.match_asset'
}
extra_filter_backends = [FilterAssetByNodeFilterBackend, LabelFilterBackend, IpInFilterBackend]
def set_assets_node(self, assets):
@ -76,8 +77,10 @@ class AssetViewSet(SuggestionMixin, FilterAssetByNodeMixin, OrgBulkModelViewSet)
class AssetPlatformRetrieveApi(RetrieveAPIView):
queryset = Platform.objects.all()
permission_classes = (IsOrgAdminOrAppUser,)
serializer_class = serializers.PlatformSerializer
rbac_perms = {
'retrieve': 'assets.view_gateway'
}
def get_object(self):
asset_pk = self.kwargs.get('pk')
@ -87,16 +90,10 @@ class AssetPlatformRetrieveApi(RetrieveAPIView):
class AssetPlatformViewSet(ModelViewSet):
queryset = Platform.objects.all()
permission_classes = (IsSuperUser,)
serializer_class = serializers.PlatformSerializer
filterset_fields = ['name', 'base']
search_fields = ['name']
def get_permissions(self):
if self.request.method.lower() in ['get', 'options']:
self.permission_classes = (IsOrgAdmin,)
return super().get_permissions()
def check_object_permissions(self, request, obj):
if request.method.lower() in ['delete', 'put', 'patch'] and obj.internal:
self.permission_denied(
@ -131,7 +128,6 @@ class AssetsTaskMixin:
class AssetTaskCreateApi(AssetsTaskMixin, generics.CreateAPIView):
model = Asset
serializer_class = serializers.AssetTaskSerializer
permission_classes = (IsOrgAdmin,)
def create(self, request, *args, **kwargs):
pk = self.kwargs.get('pk')
@ -139,11 +135,26 @@ class AssetTaskCreateApi(AssetsTaskMixin, generics.CreateAPIView):
request.data['assets'] = [pk]
return super().create(request, *args, **kwargs)
def check_permissions(self, request):
action = request.data.get('action')
action_perm_require = {
'refresh': 'assets.refresh_assethardwareinfo',
'push_system_user': 'assets.push_assetsystemuser',
'test': 'assets.test_assetconnectivity',
'test_system_user': 'assets.test_assetconnectivity'
}
perm_required = action_perm_require.get(action)
has = self.request.user.has_perm(perm_required)
if not has:
self.permission_denied(request)
def perform_asset_task(self, serializer):
data = serializer.validated_data
action = data['action']
if action not in ['push_system_user', 'test_system_user']:
return
asset = data['asset']
system_users = data.get('system_users')
if not system_users:
@ -166,12 +177,23 @@ class AssetTaskCreateApi(AssetsTaskMixin, generics.CreateAPIView):
class AssetsTaskCreateApi(AssetsTaskMixin, generics.CreateAPIView):
model = Asset
serializer_class = serializers.AssetsTaskSerializer
permission_classes = (IsOrgAdmin,)
def check_permissions(self, request):
action = request.data.get('action')
action_perm_require = {
'refresh': 'assets.refresh_assethardwareinfo1',
}
perm_required = action_perm_require.get(action)
has = self.request.user.has_perm(perm_required)
if not has:
self.permission_denied(request)
class AssetGatewayListApi(generics.ListAPIView):
permission_classes = (IsOrgAdminOrAppUser,)
serializer_class = serializers.GatewayWithAuthSerializer
rbac_perms = {
'list': 'assets.view_gateway'
}
def get_queryset(self):
asset_id = self.kwargs.get('pk')
@ -183,7 +205,6 @@ class AssetGatewayListApi(generics.ListAPIView):
class BaseAssetPermUserOrUserGroupListApi(ListAPIView):
permission_classes = (IsOrgAdmin,)
def get_object(self):
asset_id = self.kwargs.get('pk')
@ -220,11 +241,13 @@ class AssetPermUserGroupListApi(BaseAssetPermUserOrUserGroupListApi):
class BaseAssetPermUserOrUserGroupPermissionsListApiMixin(generics.ListAPIView):
permission_classes = (IsOrgAdmin,)
model = AssetPermission
serializer_class = AssetPermissionSerializer
filterset_class = AssetPermissionFilter
search_fields = ('name',)
rbac_perms = {
'list': 'perms.view_assetpermission'
}
def get_object(self):
asset_id = self.kwargs.get('pk')

View File

@ -8,14 +8,11 @@ from django.shortcuts import get_object_or_404
from common.utils import reverse
from common.utils import lazyproperty
from orgs.mixins.api import OrgBulkModelViewSet
from tickets.api import GenericTicketStatusRetrieveCloseAPI
from ..hands import IsOrgAdmin, IsAppUser
from ..models import CommandFilter, CommandFilterRule
from .. import serializers
__all__ = [
'CommandFilterViewSet', 'CommandFilterRuleViewSet', 'CommandConfirmAPI',
'CommandConfirmStatusAPI'
]
@ -23,7 +20,6 @@ class CommandFilterViewSet(OrgBulkModelViewSet):
model = CommandFilter
filterset_fields = ("name",)
search_fields = filterset_fields
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.CommandFilterSerializer
@ -31,7 +27,6 @@ class CommandFilterRuleViewSet(OrgBulkModelViewSet):
model = CommandFilterRule
filterset_fields = ('content',)
search_fields = filterset_fields
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.CommandFilterRuleSerializer
def get_queryset(self):
@ -43,8 +38,10 @@ class CommandFilterRuleViewSet(OrgBulkModelViewSet):
class CommandConfirmAPI(CreateAPIView):
permission_classes = (IsAppUser,)
serializer_class = serializers.CommandConfirmSerializer
rbac_perms = {
'POST': 'tickets.add_superticket'
}
def create(self, request, *args, **kwargs):
ticket = self.create_command_confirm_ticket()
@ -56,14 +53,14 @@ class CommandConfirmAPI(CreateAPIView):
run_command=self.serializer.data.get('run_command'),
session=self.serializer.session,
cmd_filter_rule=self.serializer.cmd_filter_rule,
org_id=self.serializer.org.id
org_id=self.serializer.org.id,
)
return ticket
@staticmethod
def get_response_data(ticket):
confirm_status_url = reverse(
view_name='api-assets:command-confirm-status',
view_name='api-tickets:super-ticket-status',
kwargs={'pk': str(ticket.id)}
)
ticket_detail_url = reverse(
@ -86,6 +83,3 @@ class CommandConfirmAPI(CreateAPIView):
serializer.is_valid(raise_exception=True)
return serializer
class CommandConfirmStatusAPI(GenericTicketStatusRetrieveCloseAPI):
pass

View File

@ -6,7 +6,6 @@ from rest_framework.views import APIView, Response
from rest_framework.serializers import ValidationError
from common.utils import get_logger
from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser
from orgs.mixins.api import OrgBulkModelViewSet
from ..models import Domain, Gateway
from .. import serializers
@ -20,7 +19,6 @@ class DomainViewSet(OrgBulkModelViewSet):
model = Domain
filterset_fields = ("name", )
search_fields = filterset_fields
permission_classes = (IsOrgAdminOrAppUser,)
serializer_class = serializers.DomainSerializer
ordering_fields = ('name',)
ordering = ('name', )
@ -35,13 +33,15 @@ class GatewayViewSet(OrgBulkModelViewSet):
model = Gateway
filterset_fields = ("domain__name", "name", "username", "ip", "domain")
search_fields = ("domain__name", "name", "username", "ip")
permission_classes = (IsOrgAdminOrAppUser,)
serializer_class = serializers.GatewaySerializer
class GatewayTestConnectionApi(SingleObjectMixin, APIView):
permission_classes = (IsOrgAdmin,)
queryset = Gateway.objects.all()
object = None
rbac_perms = {
'POST': 'assets.change_gateway'
}
def post(self, request, *args, **kwargs):
self.object = self.get_object(Gateway.objects.all())

View File

@ -3,7 +3,6 @@
from orgs.mixins.api import OrgModelViewSet
from assets.models import GatheredUser
from common.permissions import IsOrgAdmin
from ..serializers import GatheredUserSerializer
from ..filters import AssetRelatedByNodeFilterBackend
@ -15,7 +14,6 @@ __all__ = ['GatheredUserViewSet']
class GatheredUserViewSet(OrgModelViewSet):
model = GatheredUser
serializer_class = GatheredUserSerializer
permission_classes = [IsOrgAdmin]
extra_filter_backends = [AssetRelatedByNodeFilterBackend]
filterset_fields = ['asset', 'username', 'present', 'asset__ip', 'asset__hostname', 'asset_id']

View File

@ -17,7 +17,6 @@ from django.db.models import Count
from common.utils import get_logger
from orgs.mixins.api import OrgBulkModelViewSet
from ..hands import IsOrgAdmin
from ..models import Label
from .. import serializers
@ -30,7 +29,6 @@ class LabelViewSet(OrgBulkModelViewSet):
model = Label
filterset_fields = ("name", "value")
search_fields = filterset_fields
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.LabelSerializer
def list(self, request, *args, **kwargs):

View File

@ -20,7 +20,6 @@ from common.tree import TreeNodeSerializer
from orgs.mixins.api import OrgBulkModelViewSet
from orgs.mixins import generics
from orgs.utils import current_org
from ..hands import IsOrgAdmin
from ..models import Node
from ..tasks import (
update_node_assets_hardware_info_manual,
@ -46,8 +45,11 @@ class NodeViewSet(SuggestionMixin, OrgBulkModelViewSet):
model = Node
filterset_fields = ('value', 'key', 'id')
search_fields = ('value', )
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.NodeSerializer
rbac_perms = {
'match': 'assets.match_node',
'check_assets_amount_task': 'assets.change_node'
}
@action(methods=[POST], detail=False, url_path='check_assets_amount_task')
def check_assets_amount_task(self, request):
@ -85,7 +87,6 @@ class NodeListAsTreeApi(generics.ListAPIView):
]
"""
model = Node
permission_classes = (IsOrgAdmin,)
serializer_class = TreeNodeSerializer
@staticmethod
@ -100,7 +101,6 @@ class NodeListAsTreeApi(generics.ListAPIView):
class NodeChildrenApi(generics.ListCreateAPIView):
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.NodeSerializer
instance = None
is_initial = False
@ -199,7 +199,6 @@ class NodeChildrenAsTreeApi(SerializeToTreeNodeMixin, NodeChildrenApi):
class NodeAssetsApi(generics.ListAPIView):
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.AssetSerializer
def get_queryset(self):
@ -214,7 +213,6 @@ class NodeAssetsApi(generics.ListAPIView):
class NodeAddChildrenApi(generics.UpdateAPIView):
model = Node
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.NodeAddChildrenSerializer
instance = None
@ -231,7 +229,6 @@ class NodeAddChildrenApi(generics.UpdateAPIView):
class NodeAddAssetsApi(generics.UpdateAPIView):
model = Node
serializer_class = serializers.NodeAssetsSerializer
permission_classes = (IsOrgAdmin,)
instance = None
def perform_update(self, serializer):
@ -243,7 +240,6 @@ class NodeAddAssetsApi(generics.UpdateAPIView):
class NodeRemoveAssetsApi(generics.UpdateAPIView):
model = Node
serializer_class = serializers.NodeAssetsSerializer
permission_classes = (IsOrgAdmin,)
instance = None
def perform_update(self, serializer):
@ -262,7 +258,6 @@ class NodeRemoveAssetsApi(generics.UpdateAPIView):
class MoveAssetsToNodeApi(generics.UpdateAPIView):
model = Node
serializer_class = serializers.NodeAssetsSerializer
permission_classes = (IsOrgAdmin,)
instance = None
def perform_update(self, serializer):
@ -305,8 +300,6 @@ class MoveAssetsToNodeApi(generics.UpdateAPIView):
class NodeTaskCreateApi(generics.CreateAPIView):
model = Node
serializer_class = serializers.NodeTaskSerializer
permission_classes = (IsOrgAdmin,)
def get_object(self):
node_id = self.kwargs.get('pk')
node = get_object_or_none(self.model, id=node_id)

View File

@ -1,16 +1,15 @@
# ~*~ coding: utf-8 ~*~
from django.shortcuts import get_object_or_404
from django.middleware import csrf
from rest_framework.response import Response
from rest_framework.decorators import action
from common.utils import get_logger, get_object_or_none
from common.utils.crypto import get_aes_crypto
from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser, IsValidUser
from common.permissions import IsValidUser
from common.mixins.api import SuggestionMixin
from orgs.mixins.api import OrgBulkModelViewSet
from orgs.mixins import generics
from common.mixins.api import SuggestionMixin
from orgs.utils import tmp_to_root_org
from rest_framework.decorators import action
from ..models import SystemUser, CommandFilterRule
from .. import serializers
from ..serializers import SystemUserWithAuthInfoSerializer, SystemUserTempAuthSerializer
@ -46,7 +45,11 @@ class SystemUserViewSet(SuggestionMixin, OrgBulkModelViewSet):
}
ordering_fields = ('name', 'protocol', 'login_mode')
ordering = ('name', )
permission_classes = (IsOrgAdminOrAppUser,)
rbac_perms = {
'su_from': 'assets.view_systemuser',
'su_to': 'assets.view_systemuser',
'match': 'assets.match_systemuser'
}
@action(methods=['get'], detail=False, url_path='su-from')
def su_from(self, request, *args, **kwargs):
@ -80,8 +83,13 @@ class SystemUserAuthInfoApi(generics.RetrieveUpdateDestroyAPIView):
Get system user auth info
"""
model = SystemUser
permission_classes = (IsOrgAdminOrAppUser,)
serializer_class = SystemUserWithAuthInfoSerializer
rbac_perms = {
'retrieve': 'assets.view_systemusersecret',
'list': 'assets.view_systemusersecret',
'change': 'assets.change_systemuser',
'destroy': 'assets.change_systemuser',
}
def destroy(self, request, *args, **kwargs):
instance = self.get_object()
@ -114,7 +122,7 @@ class SystemUserTempAuthInfoApi(generics.CreateAPIView):
with tmp_to_root_org():
instance = get_object_or_404(SystemUser, pk=pk)
instance.set_temp_auth(instance_id, self.request.user, data)
instance.set_temp_auth(instance_id, self.request.user.id, data)
return Response(serializer.data, status=201)
@ -123,7 +131,6 @@ class SystemUserAssetAuthInfoApi(generics.RetrieveAPIView):
Get system user with asset auth info
"""
model = SystemUser
permission_classes = (IsOrgAdminOrAppUser,)
serializer_class = SystemUserWithAuthInfoSerializer
def get_object(self):
@ -140,8 +147,10 @@ class SystemUserAppAuthInfoApi(generics.RetrieveAPIView):
Get system user with asset auth info
"""
model = SystemUser
permission_classes = (IsOrgAdminOrAppUser,)
serializer_class = SystemUserWithAuthInfoSerializer
rbac_perms = {
'retrieve': 'assets.view_systemusersecret',
}
def get_object(self):
instance = super().get_object()
@ -153,7 +162,6 @@ class SystemUserAppAuthInfoApi(generics.RetrieveAPIView):
class SystemUserTaskApi(generics.CreateAPIView):
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.SystemUserTaskSerializer
def do_push(self, system_user, asset_ids=None):
@ -175,6 +183,18 @@ class SystemUserTaskApi(generics.CreateAPIView):
pk = self.kwargs.get('pk')
return get_object_or_404(SystemUser, pk=pk)
def check_permissions(self, request):
action = request.data.get('action')
action_perm_require = {
'push': 'assets.push_assetsystemuser',
'test': 'assets.test_assetconnectivity'
}
perm_required = action_perm_require.get(action)
has = self.request.user.has_perm(perm_required)
if not has:
self.permission_denied(request)
def perform_create(self, serializer):
action = serializer.validated_data["action"]
asset = serializer.validated_data.get('asset')
@ -198,7 +218,9 @@ class SystemUserTaskApi(generics.CreateAPIView):
class SystemUserCommandFilterRuleListApi(generics.ListAPIView):
permission_classes = (IsOrgAdminOrAppUser,)
rbac_perms = {
'list': 'assets.view_commandfilterule'
}
def get_serializer_class(self):
from ..serializers import CommandFilterRuleSerializer
@ -224,10 +246,12 @@ class SystemUserCommandFilterRuleListApi(generics.ListAPIView):
class SystemUserAssetsListView(generics.ListAPIView):
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.AssetSimpleSerializer
filterset_fields = ("hostname", "ip")
search_fields = filterset_fields
rbac_perms = {
'list': 'assets.view_asset'
}
def get_object(self):
pk = self.kwargs.get('pk')

View File

@ -5,7 +5,6 @@ from django.db.models import F, Value, Model
from django.db.models.signals import m2m_changed
from django.db.models.functions import Concat
from common.permissions import IsOrgAdmin
from common.utils import get_logger
from orgs.mixins.api import OrgBulkModelViewSet
from orgs.utils import current_org
@ -65,13 +64,12 @@ class RelationMixin:
class BaseRelationViewSet(RelationMixin, OrgBulkModelViewSet):
pass
perm_model = models.SystemUser
class SystemUserAssetRelationViewSet(BaseRelationViewSet):
serializer_class = serializers.SystemUserAssetRelationSerializer
model = models.SystemUser.assets.through
permission_classes = (IsOrgAdmin,)
filterset_fields = [
'id', 'asset', 'systemuser',
]
@ -97,7 +95,6 @@ class SystemUserAssetRelationViewSet(BaseRelationViewSet):
class SystemUserNodeRelationViewSet(BaseRelationViewSet):
serializer_class = serializers.SystemUserNodeRelationSerializer
model = models.SystemUser.nodes.through
permission_classes = (IsOrgAdmin,)
filterset_fields = [
'id', 'node', 'systemuser',
]
@ -118,7 +115,6 @@ class SystemUserNodeRelationViewSet(BaseRelationViewSet):
class SystemUserUserRelationViewSet(BaseRelationViewSet):
serializer_class = serializers.SystemUserUserRelationSerializer
model = models.SystemUser.users.through
permission_classes = (IsOrgAdmin,)
filterset_fields = [
'id', 'user', 'systemuser',
]
@ -140,4 +136,3 @@ class SystemUserUserRelationViewSet(BaseRelationViewSet):
)
)
return queryset

View File

@ -1,11 +1,16 @@
from __future__ import unicode_literals
from django.utils.translation import ugettext_lazy as _
from django.apps import AppConfig
class AssetsConfig(AppConfig):
name = 'assets'
verbose_name = _('App assets')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def ready(self):
super().ready()
from . import signals_handler
from . import signal_handlers

View File

@ -11,5 +11,4 @@
"""
from common.permissions import IsAppUser, IsOrgAdmin, IsValidUser, IsOrgAdminOrAppUser
from users.models import User, UserGroup

View File

@ -0,0 +1,25 @@
# Generated by Django 3.1.13 on 2022-02-17 13:35
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('assets', '0085_commandfilterrule_ignore_case'),
]
operations = [
migrations.AlterModelOptions(
name='asset',
options={'ordering': ['hostname'], 'permissions': [('test_assetconnectivity', 'Can test asset connectivity'), ('push_assetsystemuser', 'Can push system user to asset')], 'verbose_name': 'Asset'},
),
migrations.AlterModelOptions(
name='authbook',
options={'permissions': [('view_assetaccountsecret', 'Can view asset account secret'), ('change_assetaccountsecret', 'Can change asset account secret')], 'verbose_name': 'AuthBook'},
),
migrations.AlterModelOptions(
name='label',
options={'verbose_name': 'Label'},
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 3.1.13 on 2022-02-23 07:39
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0086_auto_20220217_2135'),
]
operations = [
migrations.AlterField(
model_name='systemuser',
name='protocol',
field=models.CharField(choices=[('ssh', 'SSH'), ('rdp', 'RDP'), ('telnet', 'Telnet'), ('vnc', 'VNC'), ('mysql', 'MySQL'), ('oracle', 'Oracle'), ('mariadb', 'MariaDB'), ('postgresql', 'PostgreSQL'), ('sqlserver', 'SQLServer'), ('redis', 'Redis'), ('mongodb', 'MongoDB'), ('k8s', 'K8S')], default='ssh', max_length=16, verbose_name='Protocol'),
),
]

View File

@ -0,0 +1,25 @@
# Generated by Django 3.1.14 on 2022-03-03 08:12
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('assets', '0087_auto_20220223_1539'),
]
operations = [
migrations.AlterModelOptions(
name='asset',
options={'ordering': ['hostname'], 'permissions': [('refresh_assethardwareinfo', 'Can refresh asset hardware info'), ('test_assetconnectivity', 'Can test asset connectivity'), ('push_assetsystemuser', 'Can push system user to asset'), ('match_asset', 'Can match asset')], 'verbose_name': 'Asset'},
),
migrations.AlterModelOptions(
name='node',
options={'ordering': ['parent_key', 'value'], 'permissions': [('match_node', 'Can match node')], 'verbose_name': 'Node'},
),
migrations.AlterModelOptions(
name='systemuser',
options={'ordering': ['name'], 'permissions': [('match_systemuser', 'Can match system user')], 'verbose_name': 'System user'},
),
]

View File

@ -0,0 +1,29 @@
# Generated by Django 3.1.14 on 2022-03-09 22:16
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('assets', '0088_auto_20220303_1612'),
]
operations = [
migrations.AlterModelOptions(
name='authbook',
options={'permissions': [('test_authbook', 'Can test asset account connectivity'), ('view_assetaccountsecret', 'Can view asset account secret'), ('change_assetaccountsecret', 'Can change asset account secret')], 'verbose_name': 'AuthBook'},
),
migrations.AlterModelOptions(
name='systemuser',
options={'ordering': ['name'], 'permissions': [('view_systemuserasset', 'Can view system user asset'), ('add_systemuserasset', 'Can add asset to system user'), ('remove_systemuserasset', 'Can remove system user asset'), ('match_systemuser', 'Can match system user')], 'verbose_name': 'System user'},
),
migrations.AlterModelOptions(
name='asset',
options={'ordering': ['hostname'], 'permissions': [('refresh_assethardwareinfo', 'Can refresh asset hardware info'), ('test_assetconnectivity', 'Can test asset connectivity'), ('push_assetsystemuser', 'Can push system user to asset'), ('match_asset', 'Can match asset'), ('add_assettonode', 'Add asset to node'), ('move_assettonode', 'Move asset to node')], 'verbose_name': 'Asset'},
),
migrations.AlterModelOptions(
name='gateway',
options={'permissions': [('test_gateway', 'Test gateway')], 'verbose_name': 'Gateway'},
),
]

View File

@ -8,7 +8,6 @@ from functools import reduce
from collections import OrderedDict
from django.db import models
from common.db.models import TextChoices
from django.utils.translation import ugettext_lazy as _
from rest_framework.exceptions import ValidationError
@ -59,7 +58,7 @@ class AssetQuerySet(models.QuerySet):
class ProtocolsMixin:
protocols = ''
class Protocol(TextChoices):
class Protocol(models.TextChoices):
ssh = 'ssh', 'SSH'
rdp = 'rdp', 'RDP'
telnet = 'telnet', 'Telnet'
@ -355,3 +354,11 @@ class Asset(AbsConnectivity, AbsHardwareInfo, ProtocolsMixin, NodesRelationMixin
unique_together = [('org_id', 'hostname')]
verbose_name = _("Asset")
ordering = ["hostname", ]
permissions = [
('refresh_assethardwareinfo', _('Can refresh asset hardware info')),
('test_assetconnectivity', _('Can test asset connectivity')),
('push_assetsystemuser', _('Can push system user to asset')),
('match_asset', _('Can match asset')),
('add_assettonode', _('Add asset to node')),
('move_assettonode', _('Move asset to node')),
]

View File

@ -26,6 +26,11 @@ class AuthBook(BaseUser, AbsConnectivity):
class Meta:
verbose_name = _('AuthBook')
unique_together = [('username', 'asset', 'systemuser')]
permissions = [
('test_authbook', _('Can test asset account connectivity')),
('view_assetaccountsecret', _('Can view asset account secret')),
('change_assetaccountsecret', _('Can change asset account secret'))
]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

View File

@ -186,13 +186,15 @@ class CommandFilterRule(OrgModelMixin):
return ticket
@classmethod
def get_queryset(cls, user_id=None, user_group_id=None, system_user_id=None, asset_id=None, application_id=None):
def get_queryset(cls, user_id=None, user_group_id=None, system_user_id=None,
asset_id=None, application_id=None, org_id=None):
user_groups = []
user = get_object_or_none(User, pk=user_id)
if user:
user_groups.extend(list(user.groups.all()))
user_group = get_object_or_none(UserGroup, pk=user_group_id)
if user_group:
org_id = user_group.org_id
user_groups.append(user_group)
system_user = get_object_or_none(SystemUser, pk=system_user_id)
asset = get_object_or_none(Asset, pk=asset_id)
@ -203,13 +205,18 @@ class CommandFilterRule(OrgModelMixin):
if user_groups:
q |= Q(user_groups__in=set(user_groups))
if system_user:
org_id = system_user.org_id
q |= Q(system_users=system_user)
if asset:
org_id = asset.org_id
q |= Q(assets=asset)
if application:
org_id = application.org_id
q |= Q(applications=application)
if q:
cmd_filters = CommandFilter.objects.filter(q).filter(is_active=True)
if org_id:
cmd_filters = cmd_filters.filter(org_id=org_id)
rule_ids = cmd_filters.values_list('rules', flat=True)
rules = cls.objects.filter(id__in=rule_ids)
else:

View File

@ -7,7 +7,6 @@ import random
from django.core.cache import cache
import paramiko
from django.db import models
from django.db.models import TextChoices
from django.utils.translation import ugettext_lazy as _
from common.utils import get_logger
@ -55,7 +54,7 @@ class Gateway(BaseUser):
UNCONNECTIVE_SILENCE_PERIOD_KEY_TMPL = 'asset_unconnective_gateway_silence_period_{}'
UNCONNECTIVE_SILENCE_PERIOD_BEGIN_VALUE = 60 * 5
class Protocol(TextChoices):
class Protocol(models.TextChoices):
ssh = 'ssh', 'SSH'
ip = models.CharField(max_length=128, verbose_name=_('IP'), db_index=True)
@ -71,6 +70,9 @@ class Gateway(BaseUser):
class Meta:
unique_together = [('name', 'org_id')]
verbose_name = _("Gateway")
permissions = [
('test_gateway', _('Test gateway'))
]
def set_unconnective(self):
unconnective_key = self.UNCONNECTIVE_KEY_TMPL.format(self.id)

View File

@ -37,3 +37,4 @@ class Label(OrgModelMixin):
class Meta:
db_table = "assets_label"
unique_together = [('name', 'value', 'org_id')]
verbose_name = _('Label')

View File

@ -558,6 +558,9 @@ class Node(OrgModelMixin, SomeNodesMixin, FamilyMixin, NodeAssetsMixin):
class Meta:
verbose_name = _("Node")
ordering = ['parent_key', 'value']
permissions = [
('match_node', _('Can match node')),
]
def __str__(self):
return self.full_value

View File

@ -5,13 +5,11 @@
import logging
from django.db import models
from django.db.models import Q
from django.utils.translation import ugettext_lazy as _
from django.core.validators import MinValueValidator, MaxValueValidator
from django.core.cache import cache
from common.utils import signer, get_object_or_none
from common.db.models import TextChoices
from .base import BaseUser
from .asset import Asset
from .authbook import AuthBook
@ -24,17 +22,18 @@ logger = logging.getLogger(__name__)
class ProtocolMixin:
protocol: str
class Protocol(TextChoices):
class Protocol(models.TextChoices):
ssh = 'ssh', 'SSH'
rdp = 'rdp', 'RDP'
telnet = 'telnet', 'Telnet'
vnc = 'vnc', 'VNC'
mysql = 'mysql', 'MySQL'
redis = 'redis', 'Redis'
oracle = 'oracle', 'Oracle'
mariadb = 'mariadb', 'MariaDB'
postgresql = 'postgresql', 'PostgreSQL'
sqlserver = 'sqlserver', 'SQLServer'
redis = 'redis', 'Redis'
mongodb = 'mongodb', 'MongoDB'
k8s = 'k8s', 'K8S'
SUPPORT_PUSH_PROTOCOLS = [Protocol.ssh, Protocol.rdp]
@ -46,8 +45,9 @@ class ProtocolMixin:
Protocol.rdp
]
APPLICATION_CATEGORY_DB_PROTOCOLS = [
Protocol.mysql, Protocol.redis, Protocol.oracle,
Protocol.mariadb, Protocol.postgresql, Protocol.sqlserver
Protocol.mysql, Protocol.mariadb, Protocol.oracle,
Protocol.postgresql, Protocol.sqlserver,
Protocol.redis, Protocol.mongodb
]
APPLICATION_CATEGORY_CLOUD_PROTOCOLS = [
Protocol.k8s
@ -217,7 +217,7 @@ class SystemUser(ProtocolMixin, AuthMixin, BaseUser):
(LOGIN_MANUAL, _('Manually input'))
)
class Type(TextChoices):
class Type(models.TextChoices):
common = 'common', _('Common user')
admin = 'admin', _('Admin user')
@ -323,9 +323,15 @@ class SystemUser(ProtocolMixin, AuthMixin, BaseUser):
ordering = ['name']
unique_together = [('name', 'org_id')]
verbose_name = _("System user")
permissions = [
('view_systemuserasset', _('Can view system user asset')),
('add_systemuserasset', _('Can add asset to system user')),
('remove_systemuserasset', _('Can remove system user asset')),
('match_systemuser', _('Can match system user')),
]
# Todo: 准备废弃
# Deprecated: 准备废弃
class AdminUser(BaseUser):
"""
A privileged user that ansible can use it to push system user and so on

View File

@ -5,7 +5,6 @@ from django.utils.translation import ugettext as _
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from ..models import Asset, Node
__all__ = [
'NodeSerializer', "NodeAddChildrenSerializer",
"NodeAssetsSerializer", "NodeTaskSerializer",
@ -45,7 +44,6 @@ class NodeSerializer(BulkOrgResourceModelSerializer):
def create(self, validated_data):
full_value = validated_data.get('full_value')
value = validated_data.get('value')
# 直接多层级创建
if full_value:
@ -53,7 +51,8 @@ class NodeSerializer(BulkOrgResourceModelSerializer):
# 根据 value 在 root 下创建
else:
key = Node.org_root().get_next_child_key()
node = Node.objects.create(key=key, value=value)
validated_data['key'] = key
node = Node.objects.create(**validated_data)
return node

View File

@ -88,7 +88,7 @@ class AssetAccountHandler(BaseAccountHandler):
for k, v in df_dict.items():
df_dict[k] = pd.DataFrame(v)
logger.info('\n\033[33m- 共收集{}条资产账号\033[0m'.format(accounts.count()))
logger.info('\n\033[33m- 共收集 {} 条资产账号\033[0m'.format(accounts.count()))
return df_dict

View File

@ -26,8 +26,8 @@ router.register(r'favorite-assets', api.FavoriteAssetViewSet, 'favorite-asset')
router.register(r'system-users-assets-relations', api.SystemUserAssetRelationViewSet, 'system-users-assets-relation')
router.register(r'system-users-nodes-relations', api.SystemUserNodeRelationViewSet, 'system-users-nodes-relation')
router.register(r'system-users-users-relations', api.SystemUserUserRelationViewSet, 'system-users-users-relation')
router.register(r'backup', api.AccountBackupPlanViewSet, 'backup')
router.register(r'backup-execution', api.AccountBackupPlanExecutionViewSet, 'backup-execution')
router.register(r'account-backup-plans', api.AccountBackupPlanViewSet, 'account-backup')
router.register(r'account-backup-plan-executions', api.AccountBackupPlanExecutionViewSet, 'account-backup-execution')
cmd_filter_router = routers.NestedDefaultRouter(router, r'cmd-filters', lookup='filter')
cmd_filter_router.register(r'rules', api.CommandFilterRuleViewSet, 'cmd-filter-rule')
@ -68,7 +68,6 @@ urlpatterns = [
path('gateways/<uuid:pk>/test-connective/', api.GatewayTestConnectionApi.as_view(), name='test-gateway-connective'),
path('cmd-filters/command-confirm/', api.CommandConfirmAPI.as_view(), name='command-confirm'),
path('cmd-filters/command-confirm/<uuid:pk>/status/', api.CommandConfirmStatusAPI.as_view(), name='command-confirm-status')
]

View File

@ -3,8 +3,10 @@
from rest_framework.mixins import ListModelMixin, CreateModelMixin
from django.db.models import F, Value
from django.db.models.functions import Concat
from rest_framework.permissions import IsAuthenticated
from rest_framework import generics
from common.permissions import IsOrgAdminOrAppUser, IsOrgAuditor, IsOrgAdmin
from common.drf.api import JMSReadOnlyModelViewSet
from common.drf.filters import DatetimeRangeFilter
from common.api import CommonGenericViewSet
from orgs.mixins.api import OrgGenericViewSet, OrgBulkModelViewSet, OrgRelationMixin
@ -20,7 +22,6 @@ class FTPLogViewSet(CreateModelMixin,
OrgGenericViewSet):
model = FTPLog
serializer_class = FTPLogSerializer
permission_classes = (IsOrgAdminOrAppUser | IsOrgAuditor,)
extra_filter_backends = [DatetimeRangeFilter]
date_range_filter_fields = [
('date_start', ('date_from', 'date_to'))
@ -30,9 +31,8 @@ class FTPLogViewSet(CreateModelMixin,
ordering = ['-date_start']
class UserLoginLogViewSet(ListModelMixin, CommonGenericViewSet):
class UserLoginCommonMixin:
queryset = UserLoginLog.objects.all()
permission_classes = [IsOrgAdmin | IsOrgAuditor]
serializer_class = UserLoginLogSerializer
extra_filter_backends = [DatetimeRangeFilter]
date_range_filter_fields = [
@ -41,6 +41,9 @@ class UserLoginLogViewSet(ListModelMixin, CommonGenericViewSet):
filterset_fields = ['username', 'ip', 'city', 'type', 'status', 'mfa']
search_fields = ['username', 'ip', 'city']
class UserLoginLogViewSet(UserLoginCommonMixin, ListModelMixin, CommonGenericViewSet):
@staticmethod
def get_org_members():
users = current_org.get_members().values_list('username', flat=True)
@ -55,10 +58,18 @@ class UserLoginLogViewSet(ListModelMixin, CommonGenericViewSet):
return queryset
class MyLoginLogAPIView(UserLoginCommonMixin, generics.ListAPIView):
permission_classes = [IsAuthenticated]
def get_queryset(self):
qs = super().get_queryset()
qs = qs.filter(username=self.request.user.username)
return qs
class OperateLogViewSet(ListModelMixin, OrgGenericViewSet):
model = OperateLog
serializer_class = OperateLogSerializer
permission_classes = [IsOrgAdmin | IsOrgAuditor]
extra_filter_backends = [DatetimeRangeFilter]
date_range_filter_fields = [
('datetime', ('date_from', 'date_to'))
@ -70,7 +81,6 @@ class OperateLogViewSet(ListModelMixin, OrgGenericViewSet):
class PasswordChangeLogViewSet(ListModelMixin, CommonGenericViewSet):
queryset = PasswordChangeLog.objects.all()
permission_classes = [IsOrgAdmin | IsOrgAuditor]
serializer_class = PasswordChangeLogSerializer
extra_filter_backends = [DatetimeRangeFilter]
date_range_filter_fields = [
@ -91,7 +101,6 @@ class PasswordChangeLogViewSet(ListModelMixin, CommonGenericViewSet):
class CommandExecutionViewSet(ListModelMixin, OrgGenericViewSet):
model = CommandExecution
serializer_class = CommandExecutionSerializer
permission_classes = [IsOrgAdmin | IsOrgAuditor]
extra_filter_backends = [DatetimeRangeFilter]
date_range_filter_fields = [
('date_start', ('date_from', 'date_to'))
@ -117,7 +126,6 @@ class CommandExecutionViewSet(ListModelMixin, OrgGenericViewSet):
class CommandExecutionHostRelationViewSet(OrgRelationMixin, OrgBulkModelViewSet):
serializer_class = CommandExecutionHostsRelationSerializer
m2m_field = CommandExecution.hosts.field
permission_classes = [IsOrgAdmin | IsOrgAuditor]
filterset_fields = [
'id', 'asset', 'commandexecution'
]

View File

@ -1,12 +1,14 @@
from django.apps import AppConfig
from django.conf import settings
from django.utils.translation import ugettext_lazy as _
from django.db.models.signals import post_save
class AuditsConfig(AppConfig):
name = 'audits'
verbose_name = _('Audits')
def ready(self):
from . import signals_handler
from . import signal_handlers
if settings.SYSLOG_ENABLE:
post_save.connect(signals_handler.on_audits_log_create)
post_save.connect(signal_handlers.on_audits_log_create)

View File

@ -0,0 +1,29 @@
# Generated by Django 3.1.13 on 2021-11-30 02:37
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('audits', '0012_auto_20210414_1443'),
]
operations = [
migrations.AlterModelOptions(
name='ftplog',
options={'verbose_name': 'File transfer log'},
),
migrations.AlterModelOptions(
name='operatelog',
options={'verbose_name': 'Operate log'},
),
migrations.AlterModelOptions(
name='passwordchangelog',
options={'verbose_name': 'Password change log'},
),
migrations.AlterModelOptions(
name='userloginlog',
options={'ordering': ['-datetime', 'username'], 'verbose_name': 'User login log'},
),
]

View File

@ -43,6 +43,9 @@ class FTPLog(OrgModelMixin):
is_success = models.BooleanField(default=True, verbose_name=_("Success"))
date_start = models.DateTimeField(auto_now_add=True, verbose_name=_('Date start'))
class Meta:
verbose_name = _("File transfer log")
class OperateLog(OrgModelMixin):
ACTION_CREATE = 'create'
@ -73,6 +76,9 @@ class OperateLog(OrgModelMixin):
self.org_id = Organization.ROOT_ID
return super(OperateLog, self).save(*args, **kwargs)
class Meta:
verbose_name = _("Operate log")
class PasswordChangeLog(models.Model):
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
@ -84,6 +90,9 @@ class PasswordChangeLog(models.Model):
def __str__(self):
return "{} change {}'s password".format(self.change_by, self.user)
class Meta:
verbose_name = _('Password change log')
class UserLoginLog(models.Model):
LOGIN_TYPE_CHOICE = (
@ -155,3 +164,4 @@ class UserLoginLog(models.Model):
class Meta:
ordering = ['-datetime', 'username']
verbose_name = _('User login log')

View File

@ -102,11 +102,6 @@ def create_operate_log(action, sender, resource):
M2M_NEED_RECORD = {
'OrganizationMember': (
_('User and Organization'),
_('{User} JOINED {Organization}'),
_('{User} LEFT {Organization}')
),
User.groups.through._meta.object_name: (
_('User and Group'),
_('{User} JOINED {UserGroup}'),

View File

@ -1,7 +1,7 @@
# ~*~ coding: utf-8 ~*~
from __future__ import unicode_literals
from django.urls.conf import re_path
from django.urls.conf import re_path, path
from rest_framework.routers import DefaultRouter
from common import api as capi
@ -20,6 +20,7 @@ router.register(r'command-executions-hosts-relations', api.CommandExecutionHostR
urlpatterns = [
path('my-login-logs/', api.MyLoginLogAPIView.as_view(), name='my-login-log'),
]
old_version_urlpatterns = [

View File

@ -24,12 +24,10 @@ from applications.models import Application
from authentication.signals import post_auth_failed
from common.utils import get_logger, random_string
from common.mixins.api import SerializerMixin
from common.permissions import IsSuperUserOrAppUser, IsValidUser, IsSuperUser
from common.utils.common import get_file_by_arch
from orgs.mixins.api import RootOrgViewMixin
from common.http import is_true
from perms.models.base import Action
from perms.utils.application.permission import validate_permission as app_validate_permission
from perms.utils.application.permission import get_application_actions
from perms.utils.asset.permission import get_asset_actions
@ -42,6 +40,14 @@ __all__ = ['UserConnectionTokenViewSet']
class ClientProtocolMixin:
"""
下载客户端支持的连接文件里面包含了 token 其他连接信息
- [x] RDP
- [ ] KoKo
本质上这里还是暴露出 token 进行使用
"""
request: Request
get_serializer: Callable
create_token: Callable
@ -99,7 +105,7 @@ class ClientProtocolMixin:
width = self.request.query_params.get('width')
full_screen = is_true(self.request.query_params.get('full_screen'))
drives_redirect = is_true(self.request.query_params.get('drives_redirect'))
token = self.create_token(user, asset, application, system_user)
token, secret = self.create_token(user, asset, application, system_user)
# 设置磁盘挂载
if drives_redirect:
@ -167,7 +173,7 @@ class ClientProtocolMixin:
rst = rst.decode('ascii')
return rst
@action(methods=['POST', 'GET'], detail=False, url_path='rdp/file', permission_classes=[IsValidUser])
@action(methods=['POST', 'GET'], detail=False, url_path='rdp/file')
def get_rdp_file(self, request, *args, **kwargs):
if self.request.method == 'GET':
data = self.request.query_params
@ -214,7 +220,7 @@ class ClientProtocolMixin:
}
return data
@action(methods=['POST', 'GET'], detail=False, url_path='client-url', permission_classes=[IsValidUser])
@action(methods=['POST', 'GET'], detail=False, url_path='client-url')
def get_client_protocol_url(self, request, *args, **kwargs):
serializer = self.get_valid_serializer()
try:
@ -255,6 +261,7 @@ class SecretDetailMixin:
'asset': asset,
'application': application,
'gateway': gateway,
'domain': domain,
'remote_app': remote_app,
}
@ -267,12 +274,19 @@ class SecretDetailMixin:
return {
'asset': asset,
'application': None,
'domain': asset.domain,
'gateway': gateway,
'remote_app': None,
}
@action(methods=['POST'], detail=False, permission_classes=[IsSuperUserOrAppUser], url_path='secret-info/detail')
@action(methods=['POST'], detail=False, url_path='secret-info/detail')
def get_secret_detail(self, request, *args, **kwargs):
perm_required = 'authentication.view_connectiontokensecret'
# 非常重要的 api再逻辑层再判断一下双重保险
if not request.user.has_perm(perm_required):
raise PermissionDenied('Not allow to view secret')
token = request.data.get('token', '')
try:
value, user, system_user, asset, app, expired_at, actions = self.valid_token(token)
@ -288,16 +302,26 @@ class SecretDetailMixin:
user=user, system_user=system_user,
expired_at=expired_at, actions=actions
)
cmd_filter_kwargs = {
'system_user_id': system_user.id,
'user_id': user.id,
}
if asset:
asset_detail = self._get_asset_secret_detail(asset)
system_user.load_asset_more_auth(asset.id, user.username, user.id)
data['type'] = 'asset'
data.update(asset_detail)
cmd_filter_kwargs['asset_id'] = asset.id
else:
app_detail = self._get_application_secret_detail(app)
system_user.load_app_more_auth(app.id, user.username, user.id)
data['type'] = 'application'
data.update(app_detail)
cmd_filter_kwargs['application_id'] = app.id
from assets.models import CommandFilterRule
cmd_filter_rules = CommandFilterRule.get_queryset(**cmd_filter_kwargs)
data['cmd_filter_rules'] = cmd_filter_rules
serializer = self.get_serializer(data)
return Response(data=serializer.data, status=200)
@ -307,12 +331,18 @@ class UserConnectionTokenViewSet(
RootOrgViewMixin, SerializerMixin, ClientProtocolMixin,
SecretDetailMixin, GenericViewSet
):
permission_classes = (IsSuperUserOrAppUser,)
serializer_classes = {
'default': ConnectionTokenSerializer,
'get_secret_detail': ConnectionTokenSecretSerializer,
}
CACHE_KEY_PREFIX = 'CONNECTION_TOKEN_{}'
rbac_perms = {
'GET': 'authentication.view_connectiontoken',
'create': 'authentication.add_connectiontoken',
'get_secret_detail': 'authentication.view_connectiontokensecret',
'get_rdp_file': 'authentication.add_connectiontoken',
'get_client_protocol_url': 'authentication.add_connectiontoken',
}
@staticmethod
def check_resource_permission(user, asset, application, system_user):
@ -330,8 +360,10 @@ class UserConnectionTokenViewSet(
return True
def create_token(self, user, asset, application, system_user, ttl=5 * 60):
if not self.request.user.is_superuser and user != self.request.user:
raise PermissionDenied('Only super user can create user token')
# 再次强调一下权限
perm_required = 'authentication.add_superconnectiontoken'
if user != self.request.user and not self.request.user.has_perm(perm_required):
raise PermissionDenied('Only can create user token')
self.check_resource_permission(user, asset, application, system_user)
token = random_string(36)
secret = random_string(16)
@ -361,15 +393,20 @@ class UserConnectionTokenViewSet(
key = self.CACHE_KEY_PREFIX.format(token)
cache.set(key, value, timeout=ttl)
return token
return token, secret
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
asset, application, system_user, user = self.get_request_resource(serializer)
token = self.create_token(user, asset, application, system_user)
return Response({"token": token}, status=201)
token, secret = self.create_token(user, asset, application, system_user)
tp = 'app' if application else 'asset'
data = {
"id": token, 'secret': secret,
'type': tp, 'protocol': system_user.protocol
}
return Response(data, status=201)
def valid_token(self, token):
from users.models import User
@ -403,14 +440,6 @@ class UserConnectionTokenViewSet(
raise serializers.ValidationError('Permission expired or invalid')
return value, user, system_user, asset, app, expired_at, actions
def get_permissions(self):
if self.action in ["create", "get_rdp_file"]:
if self.request.data.get('user', None):
self.permission_classes = (IsSuperUser,)
else:
self.permission_classes = (IsValidUser,)
return super().get_permissions()
def get(self, request):
token = request.query_params.get('token')
key = self.CACHE_KEY_PREFIX.format(token)

View File

@ -5,7 +5,6 @@ from rest_framework.response import Response
from users.permissions import IsAuthPasswdTimeValid
from users.models import User
from common.utils import get_logger
from common.permissions import IsOrgAdmin
from common.mixins.api import RoleUserMixin, RoleAdminMixin
from authentication import errors
@ -32,4 +31,4 @@ class DingTalkQRUnBindForUserApi(RoleUserMixin, DingTalkQRUnBindBase):
class DingTalkQRUnBindForAdminApi(RoleAdminMixin, DingTalkQRUnBindBase):
user_id_url_kwarg = 'user_id'
permission_classes = (IsOrgAdmin,)

View File

@ -5,7 +5,6 @@ from rest_framework.response import Response
from users.permissions import IsAuthPasswdTimeValid
from users.models import User
from common.utils import get_logger
from common.permissions import IsOrgAdmin
from common.mixins.api import RoleUserMixin, RoleAdminMixin
from authentication import errors
@ -32,7 +31,6 @@ class FeiShuQRUnBindForUserApi(RoleUserMixin, FeiShuQRUnBindBase):
class FeiShuQRUnBindForAdminApi(RoleAdminMixin, FeiShuQRUnBindBase):
user_id_url_kwarg = 'user_id'
permission_classes = (IsOrgAdmin,)
class FeiShuEventSubscriptionCallback(APIView):

View File

@ -1,13 +1,10 @@
# -*- coding: utf-8 -*-
#
from rest_framework.generics import UpdateAPIView
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.permissions import AllowAny
from django.shortcuts import get_object_or_404
from common.utils import get_logger
from common.permissions import IsOrgAdmin
from .. import errors, mixins
__all__ = ['TicketStatusApi']

View File

@ -39,14 +39,6 @@ class MFASendCodeApi(AuthMixin, CreateAPIView):
username = ''
ip = ''
def get_user_from_db(self, username):
try:
user = get_object_or_404(User, username=username)
return user
except Exception as e:
self.incr_mfa_failed_time(username, self.ip)
raise e
def get_user_from_db(self, username):
"""避免暴力测试用户名"""
ip = self.get_request_ip()

View File

@ -13,7 +13,7 @@ from common.utils.timezone import utc_now
from common.const.http import POST, GET
from common.drf.api import JMSGenericViewSet
from common.drf.serializers import EmptySerializer
from common.permissions import IsSuperUser
from common.permissions import OnlySuperUser
from common.utils import reverse
from users.models import User
from ..serializers import SSOTokenSerializer
@ -32,9 +32,8 @@ class SSOViewSet(AuthMixin, JMSGenericViewSet):
'login_url': SSOTokenSerializer,
'login': EmptySerializer
}
permission_classes = (IsSuperUser,)
@action(methods=[POST], detail=False, permission_classes=[IsSuperUser], url_path='login-url')
@action(methods=[POST], detail=False, permission_classes=[OnlySuperUser], url_path='login-url')
def login_url(self, request, *args, **kwargs):
if not settings.AUTH_SSO:
raise SSOAuthClosed()

View File

@ -5,7 +5,6 @@ from rest_framework.response import Response
from users.permissions import IsAuthPasswdTimeValid
from users.models import User
from common.utils import get_logger
from common.permissions import IsOrgAdmin
from common.mixins.api import RoleUserMixin, RoleAdminMixin
from authentication import errors
@ -32,4 +31,4 @@ class WeComQRUnBindForUserApi(RoleUserMixin, WeComQRUnBindBase):
class WeComQRUnBindForAdminApi(RoleAdminMixin, WeComQRUnBindBase):
user_id_url_kwarg = 'user_id'
permission_classes = (IsOrgAdmin,)

View File

@ -1,11 +1,13 @@
from django.apps import AppConfig
from django.utils.translation import ugettext_lazy as _
class AuthenticationConfig(AppConfig):
name = 'authentication'
verbose_name = _('Authentication')
def ready(self):
from . import signals_handlers
from . import signal_handlers
from . import notifications
super().ready()

View File

@ -0,0 +1,54 @@
from django.contrib.auth.backends import BaseBackend
from django.contrib.auth.backends import ModelBackend
from users.models import User
from common.utils import get_logger
logger = get_logger(__file__)
class JMSBaseAuthBackend:
@staticmethod
def is_enabled():
return True
def has_perm(self, user_obj, perm, obj=None):
return False
def user_can_authenticate(self, user):
"""
Reject users with is_valid=False. Custom user models that don't have
that attribute are allowed.
"""
is_valid = getattr(user, 'is_valid', None)
return is_valid or is_valid is None
# allow user to authenticate
def username_allow_authenticate(self, username):
return self.allow_authenticate(username=username)
def user_allow_authenticate(self, user):
return self.allow_authenticate(user=user)
def allow_authenticate(self, user=None, username=None):
if user:
allowed_backend_paths = user.get_allowed_auth_backend_paths()
else:
allowed_backend_paths = User.get_user_allowed_auth_backend_paths(username)
if allowed_backend_paths is None:
# 特殊值 None 表示没有限制
return True
backend_name = self.__class__.__name__
allowed_backend_names = [path.split('.')[-1] for path in allowed_backend_paths]
allow = backend_name in allowed_backend_names
if not allow:
info = 'User {} skip authentication backend {}, because it not in {}'
info = info.format(username, backend_name, ','.join(allowed_backend_names))
logger.debug(info)
return allow
class JMSModelBackend(JMSBaseAuthBackend, ModelBackend):
pass

View File

@ -1,3 +1,6 @@
# -*- coding: utf-8 -*-
#
# 保证 utils 中的模块进行初始化
from . import utils
from .backends import *

View File

@ -1,11 +1,14 @@
# -*- coding: utf-8 -*-
#
from django_cas_ng.backends import CASBackend as _CASBackend
from django.conf import settings
from ..base import JMSBaseAuthBackend
__all__ = ['CASBackend']
class CASBackend(_CASBackend):
def user_can_authenticate(self, user):
return True
class CASBackend(JMSBaseAuthBackend, _CASBackend):
@staticmethod
def is_enabled():
return settings.AUTH_CAS

View File

@ -0,0 +1,32 @@
from django_cas_ng import utils
from django_cas_ng.utils import (
django_settings, get_protocol,
urllib_parse, REDIRECT_FIELD_NAME, get_redirect_url
)
def get_service_url(request, redirect_to=None):
"""
重写 get_service url 方法, CAS_ROOT_PROXIED_AS 为空时, 支持跳转回当前访问的域名地址
"""
"""Generates application django service URL for CAS"""
if getattr(django_settings, 'CAS_ROOT_PROXIED_AS', None):
service = django_settings.CAS_ROOT_PROXIED_AS + request.path
else:
protocol = get_protocol(request)
host = request.get_host()
service = urllib_parse.urlunparse(
(protocol, host, request.path, '', '', ''),
)
if not django_settings.CAS_STORE_NEXT:
if '?' in service:
service += '&'
else:
service += '?'
service += urllib_parse.urlencode({
REDIRECT_FIELD_NAME: redirect_to or get_redirect_url(request)
})
return service
utils.get_service_url = get_service_url

View File

@ -8,13 +8,14 @@ from django.core.cache import cache
from django.utils.translation import ugettext as _
from six import text_type
from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend
from rest_framework import HTTP_HEADER_ENCODING
from rest_framework import authentication, exceptions
from common.auth import signature
from common.utils import get_object_or_none, make_signature, http_to_unixtime
from ..models import AccessKey, PrivateToken
from .base import JMSBaseAuthBackend, JMSModelBackend
UserModel = get_user_model()
@ -28,18 +29,6 @@ def get_request_date_header(request):
return date
class JMSModelBackend(ModelBackend):
def user_can_authenticate(self, user):
return True
def get_user(self, user_id):
try:
user = UserModel._default_manager.get(pk=user_id)
except UserModel.DoesNotExist:
return None
return user if user.is_valid else None
class AccessKeyAuthentication(authentication.BaseAuthentication):
"""App使用Access key进行签名认证, 目前签名算法比较简单,
app注册或者手动建立后,会生成 access_key_id access_key_secret,
@ -164,7 +153,7 @@ class AccessTokenAuthentication(authentication.BaseAuthentication):
return self.keyword
class PrivateTokenAuthentication(authentication.TokenAuthentication):
class PrivateTokenAuthentication(JMSBaseAuthBackend, authentication.TokenAuthentication):
model = PrivateToken
@ -212,46 +201,3 @@ class SignatureAuthentication(signature.SignatureAuthentication):
except AccessKey.DoesNotExist:
return None, None
class SSOAuthentication(JMSModelBackend):
"""
什么也不做呀😺
"""
def authenticate(self, request, sso_token=None, **kwargs):
pass
class WeComAuthentication(JMSModelBackend):
"""
什么也不做呀😺
"""
def authenticate(self, request, **kwargs):
pass
class DingTalkAuthentication(JMSModelBackend):
"""
什么也不做呀😺
"""
def authenticate(self, request, **kwargs):
pass
class FeiShuAuthentication(JMSModelBackend):
"""
什么也不做呀😺
"""
def authenticate(self, request, **kwargs):
pass
class AuthorizationTokenAuthentication(JMSModelBackend):
"""
什么也不做呀😺
"""
def authenticate(self, request, **kwargs):
pass

View File

@ -1,43 +1,38 @@
# coding:utf-8
#
import warnings
import ldap
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist
from django_auth_ldap.backend import _LDAPUser, LDAPBackend, LDAPSettings
from django_auth_ldap.backend import _LDAPUser, LDAPBackend
from django_auth_ldap.config import _LDAPConfig, LDAPSearch, LDAPSearchUnion
from users.utils import construct_user_email
from common.const import LDAP_AD_ACCOUNT_DISABLE
from .base import JMSBaseAuthBackend
logger = _LDAPConfig.get_logger()
class LDAPAuthorizationBackend(LDAPBackend):
class LDAPAuthorizationBackend(JMSBaseAuthBackend, LDAPBackend):
"""
Override this class to override _LDAPUser to LDAPUser
"""
@staticmethod
def user_can_authenticate(user):
"""
Reject users with is_active=False. Custom user models that don't have
that attribute are allowed.
"""
is_valid = getattr(user, 'is_valid', None)
return is_valid or is_valid is None
def is_enabled():
return settings.AUTH_LDAP
def get_or_build_user(self, username, ldap_user):
"""
This must return a (User, built) 2-tuple for the given LDAP user.
This must return a (User, built) 2-tuple for the given LDAP user.
username is the Django-friendly username of the user. ldap_user.dn is
the user's DN and ldap_user.attrs contains all of their LDAP
attributes.
username is the Django-friendly username of the user. ldap_user.dn is
the user's DN and ldap_user.attrs contains all of their LDAP
attributes.
The returned User object may be an unsaved model instance.
The returned User object may be an unsaved model instance.
"""
"""
model = self.get_user_model()
if self.settings.USER_QUERY_FIELD:

View File

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

View File

@ -0,0 +1,290 @@
"""
OpenID Connect relying party (RP) authentication backends
=========================================================
This modules defines backends allowing to authenticate a user using a specific token endpoint
of an OpenID Connect provider (OP).
"""
import base64
import requests
from rest_framework.exceptions import ParseError
from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend
from django.core.exceptions import SuspiciousOperation
from django.db import transaction
from django.urls import reverse
from django.conf import settings
from common.utils import get_logger
from ..base import JMSBaseAuthBackend
from .utils import validate_and_return_id_token, build_absolute_uri
from .decorator import ssl_verification
from .signals import (
openid_create_or_update_user, openid_user_login_failed, openid_user_login_success
)
logger = get_logger(__file__)
__all__ = ['OIDCAuthCodeBackend', 'OIDCAuthPasswordBackend']
class UserMixin:
@transaction.atomic
def get_or_create_user_from_claims(self, request, claims):
log_prompt = "Get or Create user from claims [ActionForUser]: {}"
logger.debug(log_prompt.format('start'))
sub = claims['sub']
name = claims.get('name', sub)
username = claims.get('preferred_username', sub)
email = claims.get('email', "{}@{}".format(username, 'jumpserver.openid'))
logger.debug(
log_prompt.format(
"sub: {}|name: {}|username: {}|email: {}".format(sub, name, username, email)
)
)
user, created = get_user_model().objects.get_or_create(
username=username, defaults={"name": name, "email": email}
)
logger.debug(log_prompt.format("user: {}|created: {}".format(user, created)))
logger.debug(log_prompt.format("Send signal => openid create or update user"))
openid_create_or_update_user.send(
sender=self.__class__, request=request, user=user, created=created,
name=name, username=username, email=email
)
return user, created
class OIDCBaseBackend(UserMixin, JMSBaseAuthBackend, ModelBackend):
@staticmethod
def is_enabled():
return settings.AUTH_OPENID
class OIDCAuthCodeBackend(OIDCBaseBackend):
""" Allows to authenticate users using an OpenID Connect Provider (OP).
This authentication backend is able to authenticate users in the case of the OpenID Connect
Authorization Code flow. The ``authenticate`` method provided by this backend is likely to be
called when the callback URL is requested by the OpenID Connect Provider (OP). Thus it will
call the OIDC provider again in order to request a valid token using the authorization code that
should be available in the request parameters associated with the callback call.
"""
@ssl_verification
def authenticate(self, request, nonce=None, **kwargs):
""" Authenticates users in case of the OpenID Connect Authorization code flow. """
log_prompt = "Process authenticate [OIDCAuthCodeBackend]: {}"
logger.debug(log_prompt.format('start'))
# NOTE: the request object is mandatory to perform the authentication using an authorization
# code provided by the OIDC supplier.
if (nonce is None and settings.AUTH_OPENID_USE_NONCE) or request is None:
logger.debug(log_prompt.format('Request or nonce value is missing'))
return
# Fetches required GET parameters from the HTTP request object.
state = request.GET.get('state')
code = request.GET.get('code')
# Don't go further if the state value or the authorization code is not present in the GET
# parameters because we won't be able to get a valid token for the user in that case.
if (state is None and settings.AUTH_OPENID_USE_STATE) or code is None:
logger.debug(log_prompt.format('Authorization code or state value is missing'))
raise SuspiciousOperation('Authorization code or state value is missing')
# Prepares the token payload that will be used to request an authentication token to the
# token endpoint of the OIDC provider.
logger.debug(log_prompt.format('Prepares token payload'))
token_payload = {
'client_id': settings.AUTH_OPENID_CLIENT_ID,
'client_secret': settings.AUTH_OPENID_CLIENT_SECRET,
'grant_type': 'authorization_code',
'code': code,
'redirect_uri': build_absolute_uri(
request, path=reverse(settings.AUTH_OPENID_AUTH_LOGIN_CALLBACK_URL_NAME)
)
}
# Prepares the token headers that will be used to request an authentication token to the
# token endpoint of the OIDC provider.
logger.debug(log_prompt.format('Prepares token headers'))
basic_token = "{}:{}".format(settings.AUTH_OPENID_CLIENT_ID, settings.AUTH_OPENID_CLIENT_SECRET)
headers = {"Authorization": "Basic {}".format(base64.b64encode(basic_token.encode()).decode())}
# Calls the token endpoint.
logger.debug(log_prompt.format('Call the token endpoint'))
token_response = requests.post(
settings.AUTH_OPENID_PROVIDER_TOKEN_ENDPOINT, data=token_payload, headers=headers
)
try:
token_response.raise_for_status()
token_response_data = token_response.json()
except Exception as e:
error = "Json token response error, token response " \
"content is: {}, error is: {}".format(token_response.content, str(e))
logger.debug(log_prompt.format(error))
raise ParseError(error)
# Validates the token.
logger.debug(log_prompt.format('Validate ID Token'))
raw_id_token = token_response_data.get('id_token')
id_token = validate_and_return_id_token(raw_id_token, nonce)
if id_token is None:
logger.debug(log_prompt.format(
'ID Token is missing, raw id token is: {}'.format(raw_id_token))
)
return
# Retrieves the access token and refresh token.
access_token = token_response_data.get('access_token')
refresh_token = token_response_data.get('refresh_token')
# Stores the ID token, the related access token and the refresh token in the session.
request.session['oidc_auth_id_token'] = raw_id_token
request.session['oidc_auth_access_token'] = access_token
request.session['oidc_auth_refresh_token'] = refresh_token
# If the id_token contains userinfo scopes and claims we don't have to hit the userinfo
# endpoint.
# https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
if settings.AUTH_OPENID_ID_TOKEN_INCLUDE_CLAIMS:
logger.debug(log_prompt.format('ID Token in claims'))
claims = id_token
else:
# Fetches the claims (user information) from the userinfo endpoint provided by the OP.
logger.debug(log_prompt.format('Fetches the claims from the userinfo endpoint'))
claims_response = requests.get(
settings.AUTH_OPENID_PROVIDER_USERINFO_ENDPOINT,
headers={'Authorization': 'Bearer {0}'.format(access_token)}
)
try:
claims_response.raise_for_status()
claims = claims_response.json()
except Exception as e:
error = "Json claims response error, claims response " \
"content is: {}, error is: {}".format(claims_response.content, str(e))
logger.debug(log_prompt.format(error))
raise ParseError(error)
logger.debug(log_prompt.format('Get or create user from claims'))
user, created = self.get_or_create_user_from_claims(request, claims)
logger.debug(log_prompt.format('Update or create oidc user'))
if self.user_can_authenticate(user):
logger.debug(log_prompt.format('OpenID user login success'))
logger.debug(log_prompt.format('Send signal => openid user login success'))
openid_user_login_success.send(sender=self.__class__, request=request, user=user)
return user
else:
logger.debug(log_prompt.format('OpenID user login failed'))
logger.debug(log_prompt.format('Send signal => openid user login failed'))
openid_user_login_failed.send(
sender=self.__class__, request=request, username=user.username,
reason="User is invalid"
)
return None
class OIDCAuthPasswordBackend(OIDCBaseBackend):
@ssl_verification
def authenticate(self, request, username=None, password=None, **kwargs):
try:
return self._authenticate(request, username, password, **kwargs)
except Exception as e:
error = f'Authenticate exception: {e}'
logger.error(error, exc_info=True)
return
def _authenticate(self, request, username=None, password=None, **kwargs):
"""
https://oauth.net/2/
https://aaronparecki.com/oauth-2-simplified/#password
"""
log_prompt = "Process authenticate [OIDCAuthPasswordBackend]: {}"
logger.debug(log_prompt.format('start'))
request_timeout = 15
if not username or not password:
logger.debug(log_prompt.format('Username or password is missing'))
return
# Prepares the token payload that will be used to request an authentication token to the
# token endpoint of the OIDC provider.
logger.debug(log_prompt.format('Prepares token payload'))
token_payload = {
'client_id': settings.AUTH_OPENID_CLIENT_ID,
'client_secret': settings.AUTH_OPENID_CLIENT_SECRET,
'grant_type': 'password',
'username': username,
'password': password,
}
# Calls the token endpoint.
logger.debug(log_prompt.format('Call the token endpoint'))
token_response = requests.post(settings.AUTH_OPENID_PROVIDER_TOKEN_ENDPOINT, data=token_payload, timeout=request_timeout)
try:
token_response.raise_for_status()
token_response_data = token_response.json()
except Exception as e:
error = "Json token response error, token response " \
"content is: {}, error is: {}".format(token_response.content, str(e))
logger.debug(log_prompt.format(error))
logger.debug(log_prompt.format('Send signal => openid user login failed'))
openid_user_login_failed.send(
sender=self.__class__, request=request, username=username, reason=error
)
return
# Retrieves the access token
access_token = token_response_data.get('access_token')
# Fetches the claims (user information) from the userinfo endpoint provided by the OP.
logger.debug(log_prompt.format('Fetches the claims from the userinfo endpoint'))
claims_response = requests.get(
settings.AUTH_OPENID_PROVIDER_USERINFO_ENDPOINT,
headers={'Authorization': 'Bearer {0}'.format(access_token)},
timeout=request_timeout
)
try:
claims_response.raise_for_status()
claims = claims_response.json()
except Exception as e:
error = "Json claims response error, claims response " \
"content is: {}, error is: {}".format(claims_response.content, str(e))
logger.debug(log_prompt.format(error))
logger.debug(log_prompt.format('Send signal => openid user login failed'))
openid_user_login_failed.send(
sender=self.__class__, request=request, username=username, reason=error
)
return
logger.debug(log_prompt.format('Get or create user from claims'))
user, created = self.get_or_create_user_from_claims(request, claims)
logger.debug(log_prompt.format('Update or create oidc user'))
if self.user_can_authenticate(user):
logger.debug(log_prompt.format('OpenID user login success'))
logger.debug(log_prompt.format('Send signal => openid user login success'))
openid_user_login_success.send(
sender=self.__class__, request=request, user=user
)
return user
else:
logger.debug(log_prompt.format('OpenID user login failed'))
logger.debug(log_prompt.format('Send signal => openid user login failed'))
openid_user_login_failed.send(
sender=self.__class__, request=request, username=username, reason="User is invalid"
)
return None

View File

@ -0,0 +1,60 @@
# coding: utf-8
#
import warnings
import contextlib
import requests
from django.conf import settings
from urllib3.exceptions import InsecureRequestWarning
from .utils import get_logger
__all__ = ['ssl_verification']
old_merge_environment_settings = requests.Session.merge_environment_settings
logger = get_logger(__file__)
@contextlib.contextmanager
def no_ssl_verification():
"""
https://stackoverflow.com/questions/15445981/
how-do-i-disable-the-security-certificate-check-in-python-requests
"""
opened_adapters = set()
def merge_environment_settings(self, url, proxies, stream, verify, cert):
# Verification happens only once per connection so we need to close
# all the opened adapters once we're done. Otherwise, the effects of
# verify=False persist beyond the end of this context manager.
opened_adapters.add(self.get_adapter(url))
_settings = old_merge_environment_settings(
self, url, proxies, stream, verify, cert
)
_settings['verify'] = False
return _settings
requests.Session.merge_environment_settings = merge_environment_settings
try:
with warnings.catch_warnings():
warnings.simplefilter('ignore', InsecureRequestWarning)
yield
finally:
requests.Session.merge_environment_settings = old_merge_environment_settings
for adapter in opened_adapters:
try:
adapter.close()
except:
pass
def ssl_verification(func):
def wrapper(*args, **kwargs):
if not settings.AUTH_OPENID_IGNORE_SSL_VERIFICATION:
return func(*args, **kwargs)
with no_ssl_verification():
return func(*args, **kwargs)
return wrapper

View File

@ -1,10 +1,106 @@
from jms_oidc_rp.middleware import OIDCRefreshIDTokenMiddleware as _OIDCRefreshIDTokenMiddleware
import time
import requests
import requests.exceptions
from django.core.exceptions import MiddlewareNotUsed
from django.conf import settings
from django.contrib import auth
from common.utils import get_logger
from .utils import validate_and_return_id_token
from .decorator import ssl_verification
class OIDCRefreshIDTokenMiddleware(_OIDCRefreshIDTokenMiddleware):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
logger = get_logger(__file__)
class OIDCRefreshIDTokenMiddleware:
""" Allows to periodically refresh the ID token associated with the authenticated user. """
def __init__(self, get_response):
if not settings.AUTH_OPENID:
raise MiddlewareNotUsed
self.get_response = get_response
def __call__(self, request):
# Refreshes tokens only in the applicable cases.
if request.method == 'GET' and not request.is_ajax() and request.user.is_authenticated and settings.AUTH_OPENID:
self.refresh_token(request)
response = self.get_response(request)
return response
@ssl_verification
def refresh_token(self, request):
""" Refreshes the token of the current user. """
log_prompt = "Process refresh Token: {}"
# logger.debug(log_prompt.format('Start'))
# NOTE: SHARE_SESSION is False means that the user does not share sessions
# with other applications
if not settings.AUTH_OPENID_SHARE_SESSION:
logger.debug(log_prompt.format('Not share session'))
return
# NOTE: no refresh token in the session means that the user wasn't authentified using the
# OpenID Connect provider (OP).
refresh_token = request.session.get('oidc_auth_refresh_token')
if refresh_token is None:
logger.debug(log_prompt.format('Refresh token is missing'))
return
id_token_exp_timestamp = request.session.get('oidc_auth_id_token_exp_timestamp', None)
now_timestamp = time.time()
# Returns immediately if the token is still valid.
if id_token_exp_timestamp is not None and id_token_exp_timestamp > now_timestamp:
# logger.debug(log_prompt.format('Returns immediately because token is still valid'))
return
# Prepares the token payload that will be used to request a new token from the token
# endpoint.
refresh_token = request.session.pop('oidc_auth_refresh_token')
token_payload = {
'client_id': settings.AUTH_OPENID_CLIENT_ID,
'client_secret': settings.AUTH_OPENID_CLIENT_SECRET,
'grant_type': 'refresh_token',
'refresh_token': refresh_token,
'scope': settings.AUTH_OPENID_SCOPES,
}
# Calls the token endpoint.
logger.debug(log_prompt.format('Calls the token endpoint'))
token_response = requests.post(settings.AUTH_OPENID_PROVIDER_TOKEN_ENDPOINT, data=token_payload)
try:
token_response.raise_for_status()
except requests.exceptions.HTTPError as e:
logger.debug(log_prompt.format('Request exception http error: {}'.format(str(e))))
logger.debug(log_prompt.format('Logout'))
auth.logout(request)
return
token_response_data = token_response.json()
# Validates the token.
logger.debug(log_prompt.format('Validate ID Token'))
raw_id_token = token_response_data.get('id_token')
id_token = validate_and_return_id_token(raw_id_token, validate_nonce=False)
# If the token cannot be validated we have to log out the current user.
if id_token is None:
logger.debug(log_prompt.format('ID Token is None'))
auth.logout(request)
logger.debug(log_prompt.format('Logout'))
return
# Retrieves the access token and refresh token.
access_token = token_response_data.get('access_token')
refresh_token = token_response_data.get('refresh_token')
# Stores the ID token, the related access token and the refresh token in the session.
request.session['oidc_auth_id_token'] = raw_id_token
request.session['oidc_auth_access_token'] = access_token
request.session['oidc_auth_refresh_token'] = refresh_token
# Saves the new expiration timestamp.
request.session['oidc_auth_id_token_exp_timestamp'] = \
time.time() + settings.AUTH_OPENID_ID_TOKEN_MAX_AGE

View File

@ -0,0 +1,18 @@
"""
OpenID Connect relying party (RP) signals
=========================================
This modules defines Django signals that can be triggered during the OpenID Connect
authentication process.
"""
from django.dispatch import Signal
openid_create_or_update_user = Signal(
providing_args=['request', 'user', 'created', 'name', 'username', 'email']
)
openid_user_login_success = Signal(providing_args=['request', 'user'])
openid_user_login_failed = Signal(providing_args=['request', 'username', 'reason'])

View File

@ -0,0 +1,20 @@
"""
OpenID Connect relying party (RP) URLs
======================================
This modules defines the URLs allowing to perform OpenID Connect flows on a Relying Party (RP).
It defines three main endpoints: the authentication request endpoint, the authentication
callback endpoint and the end session endpoint.
"""
from django.urls import path
from . import views
urlpatterns = [
path('login/', views.OIDCAuthRequestView.as_view(), name='login'),
path('callback/', views.OIDCAuthCallbackView.as_view(), name='login-callback'),
path('logout/', views.OIDCEndSessionView.as_view(), name='logout'),
]

View File

@ -0,0 +1,126 @@
"""
OpenID Connect relying party (RP) utilities
===========================================
This modules defines utilities allowing to manipulate ID tokens and other common helpers.
"""
import datetime as dt
from calendar import timegm
from urllib.parse import urlparse, urljoin
from django.core.exceptions import SuspiciousOperation
from django.utils.encoding import force_bytes, smart_bytes
from jwkest import JWKESTException
from jwkest.jwk import KEYS
from jwkest.jws import JWS
from django.conf import settings
from common.utils import get_logger
logger = get_logger(__file__)
def validate_and_return_id_token(jws, nonce=None, validate_nonce=True):
""" Validates the id_token according to the OpenID Connect specification. """
log_prompt = "Validate ID Token: {}"
logger.debug(log_prompt.format('Get shared key'))
shared_key = settings.AUTH_OPENID_CLIENT_ID \
if settings.AUTH_OPENID_PROVIDER_SIGNATURE_ALG == 'HS256' \
else settings.AUTH_OPENID_PROVIDER_SIGNATURE_KEY # RS256
try:
# Decodes the JSON Web Token and raise an error if the signature is invalid.
logger.debug(log_prompt.format('Verify compact jwk'))
id_token = JWS().verify_compact(force_bytes(jws), _get_jwks_keys(shared_key))
except JWKESTException as e:
logger.debug(log_prompt.format('Verify compact jwkest exception: {}'.format(str(e))))
return
# Validates the claims embedded in the id_token.
logger.debug(log_prompt.format('Validate claims'))
_validate_claims(id_token, nonce=nonce, validate_nonce=validate_nonce)
return id_token
def _get_jwks_keys(shared_key):
""" Returns JWKS keys used to decrypt id_token values. """
# The OpenID Connect Provider (OP) uses RSA keys to sign/enrypt ID tokens and generate public
# keys allowing to decrypt them. These public keys are exposed through the 'jwks_uri' and should
# be used to decrypt the JWS - JSON Web Signature.
log_prompt = "Get jwks keys: {}"
logger.debug(log_prompt.format('Start'))
jwks_keys = KEYS()
logger.debug(log_prompt.format('Load from provider jwks endpoint'))
jwks_keys.load_from_url(settings.AUTH_OPENID_PROVIDER_JWKS_ENDPOINT)
# Adds the shared key (which can correspond to the client_secret) as an oct key so it can be
# used for HMAC signatures.
logger.debug(log_prompt.format('Add key'))
jwks_keys.add({'key': smart_bytes(shared_key), 'kty': 'oct'})
logger.debug(log_prompt.format('End'))
return jwks_keys
def _validate_claims(id_token, nonce=None, validate_nonce=True):
""" Validates the claims embedded in the JSON Web Token. """
log_prompt = "Validate claims: {}"
logger.debug(log_prompt.format('Start'))
iss_parsed_url = urlparse(id_token['iss'])
provider_parsed_url = urlparse(settings.AUTH_OPENID_PROVIDER_ENDPOINT)
if iss_parsed_url.netloc != provider_parsed_url.netloc:
logger.debug(log_prompt.format('Invalid issuer'))
raise SuspiciousOperation('Invalid issuer')
if isinstance(id_token['aud'], str):
id_token['aud'] = [id_token['aud']]
if settings.AUTH_OPENID_CLIENT_ID not in id_token['aud']:
logger.debug(log_prompt.format('Invalid audience'))
raise SuspiciousOperation('Invalid audience')
if len(id_token['aud']) > 1 and 'azp' not in id_token:
logger.debug(log_prompt.format('Incorrect id_token: azp'))
raise SuspiciousOperation('Incorrect id_token: azp')
if 'azp' in id_token and id_token['azp'] != settings.AUTH_OPENID_CLIENT_ID:
raise SuspiciousOperation('Incorrect id_token: azp')
utc_timestamp = timegm(dt.datetime.utcnow().utctimetuple())
if utc_timestamp > id_token['exp']:
logger.debug(log_prompt.format('Signature has expired'))
raise SuspiciousOperation('Signature has expired')
if 'nbf' in id_token and utc_timestamp < id_token['nbf']:
logger.debug(log_prompt.format('Incorrect id_token: nbf'))
raise SuspiciousOperation('Incorrect id_token: nbf')
# Verifies that the token was issued in the allowed timeframe.
if utc_timestamp > id_token['iat'] + settings.AUTH_OPENID_ID_TOKEN_MAX_AGE:
logger.debug(log_prompt.format('Incorrect id_token: iat'))
raise SuspiciousOperation('Incorrect id_token: iat')
# Validate the nonce to ensure the request was not modified if applicable.
id_token_nonce = id_token.get('nonce', None)
if validate_nonce and settings.AUTH_OPENID_USE_NONCE and id_token_nonce != nonce:
logger.debug(log_prompt.format('Incorrect id_token: nonce'))
raise SuspiciousOperation('Incorrect id_token: nonce')
logger.debug(log_prompt.format('End'))
def build_absolute_uri(request, path=None):
"""
Build absolute redirect uri
"""
if path is None:
path = '/'
if settings.BASE_SITE_URL:
redirect_uri = urljoin(settings.BASE_SITE_URL, path)
else:
redirect_uri = request.build_absolute_uri(path)
return redirect_uri

View File

@ -0,0 +1,222 @@
"""
OpenID Connect relying party (RP) views
=======================================
This modules defines views allowing to start the authorization and authentication process in
order to authenticate a specific user. The most important views are: the "login" allowing to
authenticate the users using the OP and get an authorizartion code, the callback view allowing
to retrieve a valid token for the considered user and the logout view.
"""
import time
from django.conf import settings
from django.contrib import auth
from django.core.exceptions import SuspiciousOperation
from django.http import HttpResponseRedirect, QueryDict
from django.urls import reverse
from django.utils.crypto import get_random_string
from django.utils.http import is_safe_url, urlencode
from django.views.generic import View
from .utils import get_logger, build_absolute_uri
logger = get_logger(__file__)
class OIDCAuthRequestView(View):
""" Allows to start the authorization flow in order to authenticate the end-user.
This view acts as the main endpoint to trigger the authentication process involving the OIDC
provider (OP). It prepares an authentication request that will be sent to the authorization
server in order to authenticate the end-user.
"""
http_method_names = ['get', ]
def get(self, request):
""" Processes GET requests. """
log_prompt = "Process GET requests [OIDCAuthRequestView]: {}"
logger.debug(log_prompt.format('Start'))
# Defines common parameters used to bootstrap the authentication request.
logger.debug(log_prompt.format('Construct request params'))
authentication_request_params = request.GET.dict()
authentication_request_params.update({
'scope': settings.AUTH_OPENID_SCOPES,
'response_type': 'code',
'client_id': settings.AUTH_OPENID_CLIENT_ID,
'redirect_uri': build_absolute_uri(
request, path=reverse(settings.AUTH_OPENID_AUTH_LOGIN_CALLBACK_URL_NAME)
)
})
# States should be used! They are recommended in order to maintain state between the
# authentication request and the callback.
if settings.AUTH_OPENID_USE_STATE:
logger.debug(log_prompt.format('Use state'))
state = get_random_string(settings.AUTH_OPENID_STATE_LENGTH)
authentication_request_params.update({'state': state})
request.session['oidc_auth_state'] = state
# Nonces should be used too! In that case the generated nonce is stored both in the
# authentication request parameters and in the user's session.
if settings.AUTH_OPENID_USE_NONCE:
logger.debug(log_prompt.format('Use nonce'))
nonce = get_random_string(settings.AUTH_OPENID_NONCE_LENGTH)
authentication_request_params.update({'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'] = next_url \
if is_safe_url(url=next_url, allowed_hosts=(request.get_host(), )) else None
# Redirects the user to authorization endpoint.
logger.debug(log_prompt.format('Construct redirect url'))
query = urlencode(authentication_request_params)
redirect_url = '{url}?{query}'.format(
url=settings.AUTH_OPENID_PROVIDER_AUTHORIZATION_ENDPOINT, query=query)
logger.debug(log_prompt.format('Redirect'))
return HttpResponseRedirect(redirect_url)
class OIDCAuthCallbackView(View):
""" Allows to complete the authentication process.
This view acts as the main endpoint to complete the authentication process involving the OIDC
provider (OP). It checks the request sent by the OIDC provider in order to determine whether the
considered was successfully authentified or not and authenticates the user at the current
application level if applicable.
"""
http_method_names = ['get', ]
def get(self, request):
""" Processes GET requests. """
log_prompt = "Process GET requests [OIDCAuthCallbackView]: {}"
logger.debug(log_prompt.format('Start'))
callback_params = request.GET
# Retrieve the state value that was previously generated. No state means that we cannot
# authenticate the user (so a failure should be returned).
state = request.session.get('oidc_auth_state', None)
# Retrieve the nonce that was previously generated and remove it from the current session.
# If no nonce is available (while the USE_NONCE setting is set to True) this means that the
# authentication cannot be performed and so we have redirect the user to a failure URL.
nonce = request.session.pop('oidc_auth_nonce', None)
# NOTE: a redirect to the failure page should be return if some required GET parameters are
# missing or if no state can be retrieved from the current session.
if (
((nonce and settings.AUTH_OPENID_USE_NONCE) or not settings.AUTH_OPENID_USE_NONCE)
and
(
(state and settings.AUTH_OPENID_USE_STATE and 'state' in callback_params)
or
(not settings.AUTH_OPENID_USE_STATE)
)
and
('code' in callback_params)
):
# Ensures that the passed state values is the same as the one that was previously
# generated when forging the authorization request. This is necessary to mitigate
# Cross-Site Request Forgery (CSRF, XSRF).
if settings.AUTH_OPENID_USE_STATE and callback_params['state'] != state:
logger.debug(log_prompt.format('Invalid OpenID Connect callback state value'))
raise SuspiciousOperation('Invalid OpenID Connect callback state value')
# Authenticates the end-user.
next_url = request.session.get('oidc_auth_next_url', None)
logger.debug(log_prompt.format('Process authenticate'))
user = auth.authenticate(nonce=nonce, request=request)
if user and user.is_valid:
logger.debug(log_prompt.format('Login: {}'.format(user)))
auth.login(self.request, user)
# Stores an expiration timestamp in the user's session. This value will be used if
# the project is configured to periodically refresh user's token.
self.request.session['oidc_auth_id_token_exp_timestamp'] = \
time.time() + settings.AUTH_OPENID_ID_TOKEN_MAX_AGE
# Stores the "session_state" value that can be passed by the OpenID Connect provider
# in order to maintain a consistent session state across the OP and the related
# relying parties (RP).
self.request.session['oidc_auth_session_state'] = \
callback_params.get('session_state', None)
logger.debug(log_prompt.format('Redirect'))
return HttpResponseRedirect(
next_url or settings.AUTH_OPENID_AUTHENTICATION_REDIRECT_URI
)
if 'error' in callback_params:
logger.debug(
log_prompt.format('Error in callback params: {}'.format(callback_params['error']))
)
# If we receive an error in the callback GET parameters, this means that the
# authentication could not be performed at the OP level. In that case we have to logout
# the current user because we could've obtained this error after a prompt=none hit on
# OpenID Connect Provider authenticate endpoint.
logger.debug(log_prompt.format('Logout'))
auth.logout(request)
logger.debug(log_prompt.format('Redirect'))
return HttpResponseRedirect(settings.AUTH_OPENID_AUTHENTICATION_FAILURE_REDIRECT_URI)
class OIDCEndSessionView(View):
""" Allows to end the session of any user authenticated using OpenID Connect.
This view acts as the main endpoint to end the session of an end-user that was authenticated
using the OIDC provider (OP). It calls the "end-session" endpoint provided by the provider if
applicable.
"""
http_method_names = ['get', 'post', ]
def get(self, request):
""" Processes GET requests. """
log_prompt = "Process GET requests [OIDCEndSessionView]: {}"
logger.debug(log_prompt.format('Start'))
return self.post(request)
def post(self, request):
""" Processes POST requests. """
log_prompt = "Process POST requests [OIDCEndSessionView]: {}"
logger.debug(log_prompt.format('Start'))
logout_url = settings.LOGOUT_REDIRECT_URL or '/'
# Log out the current user.
if request.user.is_authenticated:
logger.debug(log_prompt.format('Current user is authenticated'))
try:
logout_url = self.provider_end_session_url \
if settings.AUTH_OPENID_PROVIDER_END_SESSION_ENDPOINT else logout_url
except KeyError: # pragma: no cover
logout_url = logout_url
logger.debug(log_prompt.format('Log out the current user: {}'.format(request.user)))
auth.logout(request)
# Redirects the user to the appropriate URL.
logger.debug(log_prompt.format('Redirect'))
return HttpResponseRedirect(logout_url)
@property
def provider_end_session_url(self):
""" Returns the end-session URL. """
q = QueryDict(mutable=True)
q[settings.AUTH_OPENID_PROVIDER_END_SESSION_REDIRECT_URI_PARAMETER] = \
build_absolute_uri(self.request, path=settings.LOGOUT_REDIRECT_URL or '/')
q[settings.AUTH_OPENID_PROVIDER_END_SESSION_ID_TOKEN_PARAMETER] = \
self.request.session['oidc_auth_id_token']
return '{}?{}'.format(settings.AUTH_OPENID_PROVIDER_END_SESSION_ENDPOINT, q.urlencode())

View File

@ -1,4 +0,0 @@
"""
使用下面的工程进行jumpserver oidc 认证
https://github.com/BaiJiangJie/jumpserver-django-oidc-rp
"""

View File

@ -1,13 +1,20 @@
# -*- coding: utf-8 -*-
#
from django.contrib.auth import get_user_model
from django.conf import settings
from .base import JMSBaseAuthBackend
UserModel = get_user_model()
__all__ = ['PublicKeyAuthBackend']
class PublicKeyAuthBackend:
class PublicKeyAuthBackend(JMSBaseAuthBackend):
@staticmethod
def is_enabled():
return settings.TERMINAL_PUBLIC_KEY_AUTH
def authenticate(self, request, username=None, public_key=None, **kwargs):
if not public_key:
return None
@ -22,15 +29,6 @@ class PublicKeyAuthBackend:
self.user_can_authenticate(user):
return user
@staticmethod
def user_can_authenticate(user):
"""
Reject users with is_active=False. Custom user models that don't have
that attribute are allowed.
"""
is_active = getattr(user, 'is_active', None)
return is_active or is_active is None
def get_user(self, user_id):
try:
user = UserModel._default_manager.get(pk=user_id)

View File

@ -6,6 +6,8 @@ from django.contrib.auth import get_user_model
from radiusauth.backends import RADIUSBackend, RADIUSRealmBackend
from django.conf import settings
from .base import JMSBaseAuthBackend
User = get_user_model()
@ -39,11 +41,17 @@ class CreateUserMixin:
return None
class RadiusBackend(CreateUserMixin, RADIUSBackend):
class RadiusBaseBackend(CreateUserMixin, JMSBaseAuthBackend):
@staticmethod
def is_enabled():
return settings.AUTH_RADIUS
class RadiusBackend(RadiusBaseBackend, RADIUSBackend):
def authenticate(self, request, username='', password='', **kwargs):
return super().authenticate(request, username=username, password=password)
class RadiusRealmBackend(CreateUserMixin, RADIUSRealmBackend):
class RadiusRealmBackend(RadiusBaseBackend, RADIUSRealmBackend):
def authenticate(self, request, username='', password='', realm=None, **kwargs):
return super().authenticate(request, username=username, password=password, realm=realm)

View File

@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
#
from .backends import *

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
#
from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend
from django.conf import settings
from django.db import transaction
from common.utils import get_logger
@ -10,16 +10,17 @@ from .signals import (
saml2_user_authenticated, saml2_user_authentication_failed,
saml2_create_or_update_user
)
from ..base import JMSBaseAuthBackend
__all__ = ['SAML2Backend']
logger = get_logger(__file__)
class SAML2Backend(ModelBackend):
def user_can_authenticate(self, user):
is_valid = getattr(user, 'is_valid', None)
return is_valid or is_valid is None
class SAML2Backend(JMSBaseAuthBackend):
@staticmethod
def is_enabled():
return settings.AUTH_SAML2
@transaction.atomic
def get_or_create_from_saml_data(self, request, **saml_user_data):

View File

@ -0,0 +1,63 @@
from django.conf import settings
from .base import JMSBaseAuthBackend
class SSOAuthentication(JMSBaseAuthBackend):
"""
什么也不做呀😺
"""
@staticmethod
def is_enabled():
return settings.AUTH_SSO
def authenticate(self, request, sso_token=None, **kwargs):
pass
class WeComAuthentication(JMSBaseAuthBackend):
"""
什么也不做呀😺
"""
@staticmethod
def is_enabled():
return settings.AUTH_WECOM
def authenticate(self, request, **kwargs):
pass
class DingTalkAuthentication(JMSBaseAuthBackend):
"""
什么也不做呀😺
"""
@staticmethod
def is_enabled():
return settings.AUTH_DINGTALK
def authenticate(self, request, **kwargs):
pass
class FeiShuAuthentication(JMSBaseAuthBackend):
"""
什么也不做呀😺
"""
@staticmethod
def is_enabled():
return settings.AUTH_FEISHU
def authenticate(self, request, **kwargs):
pass
class AuthorizationTokenAuthentication(JMSBaseAuthBackend):
"""
什么也不做呀😺
"""
def authenticate(self, request, **kwargs):
pass

View File

@ -0,0 +1,32 @@
# Generated by Django 3.1.12 on 2022-02-11 06:01
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('authentication', '0006_auto_20211227_1059'),
]
operations = [
migrations.CreateModel(
name='ConnectionToken',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')),
('updated_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Updated by')),
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
],
options={'verbose_name': 'Connection token'},
),
migrations.AlterModelOptions(
name='accesskey',
options={'verbose_name': 'Access key'},
),
migrations.AlterModelOptions(
name='ssotoken',
options={'verbose_name': 'SSO token'},
),
]

View File

@ -0,0 +1,25 @@
# Generated by Django 3.1.14 on 2022-03-02 11:53
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('authentication', '0007_connectiontoken'),
]
operations = [
migrations.CreateModel(
name='SuperConnectionToken',
fields=[
],
options={
'verbose_name': 'Super connection token',
'proxy': True,
'indexes': [],
'constraints': [],
},
bases=('authentication.connectiontoken',),
),
]

View File

@ -0,0 +1,17 @@
# Generated by Django 3.1.14 on 2022-03-09 22:16
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('authentication', '0008_superconnectiontoken'),
]
operations = [
migrations.AlterModelOptions(
name='connectiontoken',
options={'permissions': [('view_connectiontokensecret', 'Can view connection token secret')], 'verbose_name': 'Connection token'},
),
]

View File

@ -12,9 +12,10 @@ from django.contrib import auth
from django.utils.translation import ugettext as _
from rest_framework.request import Request
from django.contrib.auth import (
BACKEND_SESSION_KEY, _get_backends,
PermissionDenied, user_login_failed, _clean_credentials
BACKEND_SESSION_KEY, load_backend,
PermissionDenied, user_login_failed, _clean_credentials,
)
from django.core.exceptions import ImproperlyConfigured
from django.shortcuts import reverse, redirect, get_object_or_404
from common.utils import get_request_ip, get_logger, bulk_get, FlashMessageUtil
@ -29,27 +30,38 @@ from .const import RSA_PRIVATE_KEY, RSA_PUBLIC_KEY
logger = get_logger(__name__)
def check_backend_can_auth(username, backend_path, allowed_auth_backends):
if allowed_auth_backends is not None and backend_path not in allowed_auth_backends:
logger.debug('Skip user auth backend: {}, {} not in'.format(
username, backend_path, ','.join(allowed_auth_backends)
))
return False
return True
def _get_backends(return_tuples=False):
backends = []
for backend_path in settings.AUTHENTICATION_BACKENDS:
backend = load_backend(backend_path)
# 检查 backend 是否启用
if not backend.is_enabled():
continue
backends.append((backend, backend_path) if return_tuples else backend)
if not backends:
raise ImproperlyConfigured(
'No authentication backends have been defined. Does '
'AUTHENTICATION_BACKENDS contain anything?'
)
return backends
auth._get_backends = _get_backends
def authenticate(request=None, **credentials):
"""
If the given credentials are valid, return a User object.
之所以 hack 这个 auticate
"""
username = credentials.get('username')
allowed_auth_backends = User.get_user_allowed_auth_backends(username)
for backend, backend_path in _get_backends(return_tuples=True):
# 预先检查,不浪费认证时间
if not check_backend_can_auth(username, backend_path, allowed_auth_backends):
# 检查用户名是否允许认证 (预先检查,不浪费认证时间)
if not backend.username_allow_authenticate(username):
continue
# 原生
backend_signature = inspect.signature(backend.authenticate)
try:
backend_signature.bind(request, **credentials)
@ -63,21 +75,17 @@ def authenticate(request=None, **credentials):
break
if user is None:
continue
# 如果是 None, 证明没有检查过, 需要再次检查
if allowed_auth_backends is None:
# 有些 authentication 参数中不带 username, 之后还要再检查
allowed_auth_backends = user.get_allowed_auth_backends()
if not check_backend_can_auth(user.username, backend_path, allowed_auth_backends):
continue
# 检查用户是否允许认证
if not backend.user_allow_authenticate(user):
continue
# Annotate the user object with the path of the backend.
user.backend = backend_path
return user
# The credentials supplied are invalid to all backends, fire signal
user_login_failed.send(
sender=__name__, credentials=_clean_credentials(credentials), request=request
)
user_login_failed.send(sender=__name__, credentials=_clean_credentials(credentials), request=request)
auth.authenticate = authenticate

View File

@ -29,6 +29,9 @@ class AccessKey(models.Model):
def __str__(self):
return str(self.id)
class Meta:
verbose_name = _("Access key")
class PrivateToken(Token):
"""Inherit from auth token, otherwise migration is boring"""
@ -45,3 +48,23 @@ class SSOToken(models.JMSBaseModel):
authkey = models.UUIDField(primary_key=True, default=uuid.uuid4, verbose_name=_('Token'))
expired = models.BooleanField(default=False, verbose_name=_('Expired'))
user = models.ForeignKey('users.User', on_delete=models.CASCADE, verbose_name=_('User'), db_constraint=False)
class Meta:
verbose_name = _('SSO token')
class ConnectionToken(models.JMSBaseModel):
# Todo: 未来可能放到这里,不记录到 redis 了,虽然方便,但是不易于审计
# Todo: add connection token 可能要授权给 普通用户, 或者放开就行
class Meta:
verbose_name = _('Connection token')
permissions = [
('view_connectiontokensecret', _('Can view connection token secret'))
]
class SuperConnectionToken(ConnectionToken):
class Meta:
proxy = True
verbose_name = _("Super connection token")

View File

@ -5,7 +5,7 @@ from rest_framework import serializers
from common.utils import get_object_or_none
from users.models import User
from assets.models import Asset, SystemUser, Gateway
from assets.models import Asset, SystemUser, Gateway, Domain, CommandFilterRule
from applications.models import Application
from users.serializers import UserProfileSerializer
from assets.serializers import ProtocolsField
@ -169,7 +169,7 @@ class ConnectionTokenAssetSerializer(serializers.ModelSerializer):
class ConnectionTokenSystemUserSerializer(serializers.ModelSerializer):
class Meta:
model = SystemUser
fields = ['id', 'name', 'username', 'password', 'private_key', 'ad_domain']
fields = ['id', 'name', 'username', 'password', 'private_key', 'protocol', 'ad_domain', 'org_id']
class ConnectionTokenGatewaySerializer(serializers.ModelSerializer):
@ -185,9 +185,30 @@ class ConnectionTokenRemoteAppSerializer(serializers.Serializer):
class ConnectionTokenApplicationSerializer(serializers.ModelSerializer):
attrs = serializers.JSONField(read_only=True)
class Meta:
model = Application
fields = ['id', 'name', 'category', 'type']
fields = ['id', 'name', 'category', 'type', 'attrs', 'org_id']
class ConnectionTokenDomainSerializer(serializers.ModelSerializer):
gateways = ConnectionTokenGatewaySerializer(many=True, read_only=True)
class Meta:
model = Domain
fields = ['id', 'name', 'gateways']
class ConnectionTokenFilterRuleSerializer(serializers.ModelSerializer):
class Meta:
model = CommandFilterRule
fields = [
'id', 'type', 'content', 'ignore_case', 'pattern',
'priority', 'action',
'date_created',
]
class ConnectionTokenSecretSerializer(serializers.Serializer):
@ -199,6 +220,8 @@ class ConnectionTokenSecretSerializer(serializers.Serializer):
remote_app = ConnectionTokenRemoteAppSerializer(read_only=True)
application = ConnectionTokenApplicationSerializer(read_only=True)
system_user = ConnectionTokenSystemUserSerializer(read_only=True)
cmd_filter_rules = ConnectionTokenFilterRuleSerializer(many=True)
domain = ConnectionTokenDomainSerializer(read_only=True)
gateway = ConnectionTokenGatewaySerializer(read_only=True)
actions = ActionsField()
expired_at = serializers.IntegerField()

View File

@ -6,8 +6,9 @@ from django.core.cache import cache
from django.dispatch import receiver
from django_cas_ng.signals import cas_user_authenticated
from jms_oidc_rp.signals import openid_user_login_failed, openid_user_login_success
from authentication.backends.oidc.signals import (
openid_user_login_failed, openid_user_login_success
)
from authentication.backends.saml2.signals import (
saml2_user_authenticated, saml2_user_authentication_failed
)
@ -16,6 +17,9 @@ from .signals import post_auth_success, post_auth_failed
@receiver(user_logged_in)
def on_user_auth_login_success(sender, user, request, **kwargs):
# 失效 perms 缓存
user.expire_perms_cache()
# 开启了 MFA且没有校验过, 可以全局校验, middleware 中可以全局管理 oidc 等第三方认证的 MFA
if settings.SECURITY_MFA_AUTH_ENABLED_FOR_THIRD_PARTY \
and user.mfa_enabled \
@ -24,12 +28,12 @@ def on_user_auth_login_success(sender, user, request, **kwargs):
# 单点登录,超过了自动退出
if settings.USER_LOGIN_SINGLE_MACHINE_ENABLED:
user_id = 'single_machine_login_' + str(user.id)
session_key = cache.get(user_id)
lock_key = 'single_machine_login_' + str(user.id)
session_key = cache.get(lock_key)
if session_key and session_key != request.session.session_key:
session = import_module(settings.SESSION_ENGINE).SessionStore(session_key)
session.delete()
cache.set(user_id, request.session.session_key, None)
cache.set(lock_key, request.session.session_key, None)
@receiver(openid_user_login_success)

View File

@ -115,10 +115,21 @@
.mfa-div {
width: 100%;
}
.login-page-language {
margin-right: -11px !important;
padding-top: 12px !important;
padding-left: 0 !important;
padding-bottom: 8px !important;
color: #666 !important;
font-weight: 350 !important;
min-height: auto !important;
}
</style>
</head>
<body>
<div class="login-content">
<div class="right-image-box">
<a href="{% if not XPACK_ENABLED %}https://github.com/jumpserver/jumpserver{% endif %}">
@ -127,6 +138,22 @@
</div>
<div class="left-form-box {% if not form.challenge and not form.captcha %} no-captcha-challenge {% endif %}">
<div style="background-color: white">
<ul class="nav navbar-top-links navbar-right">
<li class="dropdown">
<a class="dropdown-toggle login-page-language" data-toggle="dropdown" href="#" target="_blank">
<i class="fa fa-globe fa-lg" style="margin-right: 2px"></i>
{% ifequal request.COOKIES.django_language 'en' %}
<span>English<b class="caret"></b></span>
{% else %}
<span>中文(简体)<b class="caret"></b></span>
{% endifequal %}
</a>
<ul class="dropdown-menu profile-dropdown dropdown-menu-right">
<li> <a id="switch_cn" href="{% url 'i18n-switch' lang='zh-hans' %}"> <span>中文(简体)</span> </a> </li>
<li> <a id="switch_en" href="{% url 'i18n-switch' lang='en' %}"> <span>English</span> </a> </li>
</ul>
</li>
</ul>
<div class="jms-title">
<span style="font-size: 21px;font-weight:400;color: #151515;letter-spacing: 0;">{{ JMS_TITLE }}</span>
</div>

View File

@ -55,7 +55,7 @@ urlpatterns = [
# openid
path('cas/', include(('authentication.backends.cas.urls', 'authentication'), namespace='cas')),
path('openid/', include(('jms_oidc_rp.urls', 'authentication'), namespace='openid')),
path('openid/', include(('authentication.backends.oidc.urls', 'authentication'), namespace='openid')),
path('saml2/', include(('authentication.backends.saml2.urls', 'authentication'), namespace='saml2')),
path('captcha/', include('captcha.urls')),
]

View File

@ -184,9 +184,7 @@ class UserLoginView(mixins.AuthMixin, FormView):
@staticmethod
def get_forgot_password_url():
forgot_password_url = reverse('authentication:forgot-password')
has_other_auth_backend = settings.AUTHENTICATION_BACKENDS[0] != settings.AUTH_BACKEND_MODEL
if has_other_auth_backend and settings.FORGOT_PASSWORD_URL:
forgot_password_url = settings.FORGOT_PASSWORD_URL
forgot_password_url = settings.FORGOT_PASSWORD_URL or forgot_password_url
return forgot_password_url
def get_context_data(self, **kwargs):

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