mirror of https://github.com/jumpserver/jumpserver
commit
45331dc9e8
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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'},
|
||||
),
|
||||
]
|
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
}
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -11,5 +11,5 @@
|
|||
"""
|
||||
|
||||
|
||||
from common.permissions import IsAppUser, IsOrgAdmin, IsValidUser, IsOrgAdminOrAppUser, NeedMFAVerify
|
||||
from common.permissions import NeedMFAVerify
|
||||
from users.models import User, UserGroup
|
||||
|
|
|
@ -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'},
|
||||
),
|
||||
]
|
|
@ -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'),
|
||||
),
|
||||
]
|
|
@ -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'},
|
||||
),
|
||||
]
|
|
@ -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)
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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 *
|
||||
|
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 *
|
||||
|
|
|
@ -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()
|
|
@ -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
|
||||
|
|
|
@ -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', )
|
||||
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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']
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -11,5 +11,4 @@
|
|||
"""
|
||||
|
||||
|
||||
from common.permissions import IsAppUser, IsOrgAdmin, IsValidUser, IsOrgAdminOrAppUser
|
||||
from users.models import User, UserGroup
|
||||
|
|
|
@ -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'},
|
||||
),
|
||||
]
|
|
@ -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'),
|
||||
),
|
||||
]
|
|
@ -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'},
|
||||
),
|
||||
]
|
|
@ -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'},
|
||||
),
|
||||
]
|
|
@ -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')),
|
||||
]
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -37,3 +37,4 @@ class Label(OrgModelMixin):
|
|||
class Meta:
|
||||
db_table = "assets_label"
|
||||
unique_together = [('name', 'value', 'org_id')]
|
||||
verbose_name = _('Label')
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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')
|
||||
|
||||
]
|
||||
|
||||
|
|
|
@ -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'
|
||||
]
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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'},
|
||||
),
|
||||
]
|
|
@ -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')
|
||||
|
|
|
@ -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}'),
|
|
@ -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 = [
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,)
|
||||
|
|
@ -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):
|
||||
|
|
|
@ -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']
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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,)
|
||||
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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
|
|
@ -1,3 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
# 保证 utils 中的模块进行初始化
|
||||
from . import utils
|
||||
from .backends import *
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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:
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
from .backends import *
|
|
@ -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
|
||||
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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'])
|
||||
|
|
@ -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'),
|
||||
]
|
|
@ -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
|
|
@ -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())
|
|
@ -1,4 +0,0 @@
|
|||
"""
|
||||
使用下面的工程,进行jumpserver 的 oidc 认证
|
||||
https://github.com/BaiJiangJie/jumpserver-django-oidc-rp
|
||||
"""
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from .backends import *
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
|
@ -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'},
|
||||
),
|
||||
]
|
|
@ -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',),
|
||||
),
|
||||
]
|
|
@ -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'},
|
||||
),
|
||||
]
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
|
@ -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>
|
||||
|
|
|
@ -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')),
|
||||
]
|
||||
|
|
|
@ -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
Loading…
Reference in New Issue