Merge branch 'v3' into pr@v3@feat_support_clear_private_key

pull/9119/head
老广 2022-12-02 10:35:20 +08:00 committed by GitHub
commit bcf509ab07
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
96 changed files with 1820 additions and 1116 deletions

32
.github/workflows/jms-build-test.yml vendored Normal file
View File

@ -0,0 +1,32 @@
name: "Run Build Test"
on:
push:
branches:
- pr@*
- repr@*
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: docker/setup-qemu-action@v2
- uses: docker/setup-buildx-action@v2
- uses: docker/build-push-action@v3
with:
context: .
push: false
tags: jumpserver/core:test
file: Dockerfile
cache-from: type=gha
cache-to: type=gha,mode=max
- uses: LouisBrunner/checks-action@v1.5.0
if: always()
with:
token: ${{ secrets.GITHUB_TOKEN }}
name: Check Build
conclusion: ${{ job.status }}

View File

@ -100,6 +100,6 @@ VOLUME /opt/jumpserver/logs
ENV LANG=zh_CN.UTF-8
EXPOSE 8070
EXPOSE 8080
ENTRYPOINT ["./entrypoint.sh"]

View File

@ -91,6 +91,6 @@ VOLUME /opt/jumpserver/logs
ENV LANG=zh_CN.UTF-8
EXPOSE 8070
EXPOSE 8080
ENTRYPOINT ["./entrypoint.sh"]

View File

@ -0,0 +1,12 @@
from orgs.mixins.api import OrgBulkModelViewSet
from .. import models, serializers
__all__ = ['CommandFilterACLViewSet']
class CommandFilterACLViewSet(OrgBulkModelViewSet):
model = models.CommandFilterACL
filterset_fields = ('name', )
search_fields = filterset_fields
serializer_class = serializers.LoginAssetACLSerializer

View File

@ -1,10 +1,10 @@
from rest_framework.response import Response
from rest_framework.generics import CreateAPIView
from rest_framework.response import Response
from common.utils import reverse, lazyproperty
from orgs.utils import tmp_to_org
from ..models import LoginAssetACL
from .. import serializers
from ..models import LoginAssetACL
__all__ = ['LoginAssetCheckAPI']
@ -20,34 +20,40 @@ class LoginAssetCheckAPI(CreateAPIView):
return LoginAssetACL.objects.all()
def create(self, request, *args, **kwargs):
is_need_confirm, response_data = self.check_if_need_confirm()
return Response(data=response_data, status=200)
data = self.check_confirm()
return Response(data=data, status=200)
def check_if_need_confirm(self):
queries = {
'user': self.serializer.user, 'asset': self.serializer.asset,
'account': self.serializer.account,
'action': LoginAssetACL.ActionChoices.login_confirm
}
with tmp_to_org(self.serializer.org):
acl = LoginAssetACL.filter(**queries).valid().first()
@lazyproperty
def serializer(self):
serializer = self.get_serializer(data=self.request.data)
serializer.is_valid(raise_exception=True)
return serializer
if not acl:
is_need_confirm = False
response_data = {}
else:
is_need_confirm = True
def check_confirm(self):
with tmp_to_org(self.serializer.asset.org):
acl = LoginAssetACL.objects \
.filter(action=LoginAssetACL.ActionChoices.confirm) \
.filter_user(self.serializer.user) \
.filter_asset(self.serializer.asset) \
.filter_account(self.serializer.validated_data.get('account_username')) \
.valid() \
.first()
if acl:
need_confirm = True
response_data = self._get_response_data_of_need_confirm(acl)
response_data['need_confirm'] = is_need_confirm
return is_need_confirm, response_data
else:
need_confirm = False
response_data = {}
response_data['need_confirm'] = need_confirm
return response_data
def _get_response_data_of_need_confirm(self, acl):
def _get_response_data_of_need_confirm(self, acl) -> dict:
ticket = LoginAssetACL.create_login_asset_confirm_ticket(
user=self.serializer.user,
asset=self.serializer.asset,
account=self.serializer.account,
account_username=self.serializer.validated_data.get('account_username'),
assignees=acl.reviewers.all(),
org_id=self.serializer.org.id,
org_id=self.serializer.asset.org.id,
)
confirm_status_url = reverse(
view_name='api-tickets:super-ticket-status',
@ -68,10 +74,3 @@ class LoginAssetCheckAPI(CreateAPIView):
'ticket_id': str(ticket.id)
}
return data
@lazyproperty
def serializer(self):
serializer = self.get_serializer(data=self.request.data)
serializer.is_valid(raise_exception=True)
return serializer

View File

@ -22,7 +22,7 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='loginassetacl',
name='accounts',
field=models.JSONField(default=dict, verbose_name='Account'),
field=models.JSONField(verbose_name='Account'),
),
migrations.RunPython(migrate_system_users_to_accounts),
migrations.RemoveField(

View File

@ -0,0 +1,35 @@
# Generated by Django 3.2.14 on 2022-12-01 10:46
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('acls', '0004_auto_20220831_1658'),
]
operations = [
migrations.AlterField(
model_name='loginacl',
name='action',
field=models.CharField(choices=[('reject', 'Reject'), ('allow', 'Allow'), ('confirm', 'Confirm')], default='reject', max_length=64, verbose_name='Action'),
),
migrations.AlterField(
model_name='loginacl',
name='reviewers',
field=models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL, verbose_name='Reviewers'),
),
migrations.AlterField(
model_name='loginassetacl',
name='action',
field=models.CharField(choices=[('reject', 'Reject'), ('allow', 'Allow'), ('confirm', 'Confirm')], default='reject', max_length=64, verbose_name='Action'),
),
migrations.AlterField(
model_name='loginassetacl',
name='reviewers',
field=models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL, verbose_name='Reviewers'),
),
]

View File

@ -0,0 +1,61 @@
# Generated by Django 3.2.14 on 2022-12-01 11:39
from django.conf import settings
import django.core.validators
from django.db import migrations, models
import uuid
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('acls', '0005_auto_20221201_1846'),
]
operations = [
migrations.CreateModel(
name='CommandGroup',
fields=[
('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')),
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
('name', models.CharField(max_length=128, verbose_name='Name')),
('type', models.CharField(choices=[('command', 'Command'), ('regex', 'Regex')], default='command', max_length=16, verbose_name='Type')),
('content', models.TextField(help_text='One line one command', verbose_name='Content')),
('ignore_case', models.BooleanField(default=True, verbose_name='Ignore case')),
],
options={
'verbose_name': 'Command filter rule',
'unique_together': {('org_id', 'name')},
},
),
migrations.CreateModel(
name='CommandFilterACL',
fields=[
('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created 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')),
('name', models.CharField(max_length=128, verbose_name='Name')),
('priority', models.IntegerField(default=50, help_text='1-100, the lower the value will be match first', validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)], verbose_name='Priority')),
('action', models.CharField(choices=[('reject', 'Reject'), ('allow', 'Allow'), ('confirm', 'Confirm')], default='reject', max_length=64, verbose_name='Action')),
('is_active', models.BooleanField(default=True, verbose_name='Active')),
('comment', models.TextField(blank=True, default='', verbose_name='Comment')),
('users', models.JSONField(verbose_name='User')),
('accounts', models.JSONField(verbose_name='Account')),
('assets', models.JSONField(verbose_name='Asset')),
('commands', models.ManyToManyField(to='acls.CommandGroup', verbose_name='Commands')),
('reviewers', models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL, verbose_name='Reviewers')),
],
options={
'verbose_name': 'Command acl',
'ordering': ('priority', '-date_updated', 'name'),
'unique_together': {('name', 'org_id')},
},
),
]

View File

@ -1,2 +1,3 @@
from .login_acl import *
from .login_asset_acl import *
from .command_acl import *

View File

@ -4,7 +4,13 @@ from django.core.validators import MinValueValidator, MaxValueValidator
from common.mixins import CommonModelMixin
__all__ = ['BaseACL', 'BaseACLQuerySet']
__all__ = ['BaseACL', 'BaseACLQuerySet', 'ACLManager']
class ActionChoices(models.TextChoices):
reject = 'reject', _('Reject')
allow = 'allow', _('Allow')
confirm = 'confirm', _('Confirm')
class BaseACLQuerySet(models.QuerySet):
@ -21,6 +27,11 @@ class BaseACLQuerySet(models.QuerySet):
return self.inactive()
class ACLManager(models.Manager):
def valid(self):
return self.get_queryset().valid()
class BaseACL(CommonModelMixin):
name = models.CharField(max_length=128, verbose_name=_('Name'))
priority = models.IntegerField(
@ -28,8 +39,16 @@ class BaseACL(CommonModelMixin):
help_text=_("1-100, the lower the value will be match first"),
validators=[MinValueValidator(1), MaxValueValidator(100)]
)
action = models.CharField(
max_length=64, verbose_name=_('Action'),
choices=ActionChoices.choices, default=ActionChoices.reject
)
reviewers = models.ManyToManyField('users.User', blank=True, verbose_name=_("Reviewers"))
is_active = models.BooleanField(default=True, verbose_name=_("Active"))
comment = models.TextField(default='', blank=True, verbose_name=_('Comment'))
objects = ACLManager.from_queryset(BaseACLQuerySet)()
ActionChoices = ActionChoices
class Meta:
abstract = True

View File

@ -0,0 +1,162 @@
# -*- coding: utf-8 -*-
#
import re
from django.db import models
from django.db.models import Q
from django.utils.translation import ugettext_lazy as _
from users.models import User, UserGroup
from orgs.mixins.models import JMSOrgBaseModel
from common.utils import lazyproperty, get_logger, get_object_or_none
from orgs.mixins.models import OrgModelMixin
from .base import BaseACL
logger = get_logger(__file__)
class CommandGroup(JMSOrgBaseModel):
class Type(models.TextChoices):
command = 'command', _('Command')
regex = 'regex', _('Regex')
name = models.CharField(max_length=128, verbose_name=_("Name"))
type = models.CharField(max_length=16, default=Type.command, choices=Type.choices, verbose_name=_("Type"))
content = models.TextField(verbose_name=_("Content"), help_text=_("One line one command"))
ignore_case = models.BooleanField(default=True, verbose_name=_('Ignore case'))
class Meta:
unique_together = [('org_id', 'name')]
verbose_name = _("Command filter rule")
@lazyproperty
def pattern(self):
if self.type == 'command':
s = self.construct_command_regex(content=self.content)
else:
s = r'{0}'.format(self.content)
return s
@classmethod
def construct_command_regex(cls, content):
regex = []
content = content.replace('\r\n', '\n')
for _cmd in content.split('\n'):
cmd = re.sub(r'\s+', ' ', _cmd)
cmd = re.escape(cmd)
cmd = cmd.replace('\\ ', '\s+')
# 有空格就不能 铆钉单词了
if ' ' in _cmd:
regex.append(cmd)
continue
if not cmd:
continue
# 如果是单个字符
if cmd[-1].isalpha():
regex.append(r'\b{0}\b'.format(cmd))
else:
regex.append(r'\b{0}'.format(cmd))
s = r'{}'.format('|'.join(regex))
return s
@staticmethod
def compile_regex(regex, ignore_case):
args = []
if ignore_case:
args.append(re.IGNORECASE)
try:
pattern = re.compile(regex, *args)
except Exception as e:
error = _('The generated regular expression is incorrect: {}').format(str(e))
logger.error(error)
return False, error, None
return True, '', pattern
def match(self, data):
succeed, error, pattern = self.compile_regex(self.pattern, self.ignore_case)
if not succeed:
return False, ''
found = pattern.search(data)
if not found:
return False, ''
else:
return True, found.group()
def __str__(self):
return '{} % {}'.format(self.type, self.content)
def create_command_confirm_ticket(self, run_command, session, cmd_filter_rule, org_id):
from tickets.const import TicketType
from tickets.models import ApplyCommandTicket
data = {
'title': _('Command confirm') + ' ({})'.format(session.user),
'type': TicketType.command_confirm,
'applicant': session.user_obj,
'apply_run_user_id': session.user_id,
'apply_run_asset': str(session.asset),
'apply_run_account': str(session.account),
'apply_run_command': run_command[:4090],
'apply_from_session_id': str(session.id),
'apply_from_cmd_filter_rule_id': str(cmd_filter_rule.id),
'apply_from_cmd_filter_id': str(cmd_filter_rule.filter.id),
'org_id': org_id,
}
ticket = ApplyCommandTicket.objects.create(**data)
assignees = self.reviewers.all()
ticket.open_by_system(assignees)
return ticket
@classmethod
def get_queryset(
cls, user_id=None, user_group_id=None, account=None,
asset_id=None, org_id=None
):
from assets.models import Account
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)
asset = get_object_or_none(Asset, pk=asset_id)
q = Q()
if user:
q |= Q(users=user)
if user_groups:
q |= Q(user_groups__in=set(user_groups))
if account:
org_id = account.org_id
q |= Q(accounts__contains=account.username) | \
Q(accounts__contains=Account.AliasAccount.ALL)
if asset:
org_id = asset.org_id
q |= Q(assets=asset)
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:
rules = cls.objects.none()
return rules
class CommandFilterACL(OrgModelMixin, BaseACL):
# 条件
users = models.JSONField(verbose_name=_('User'))
accounts = models.JSONField(verbose_name=_('Account'))
assets = models.JSONField(verbose_name=_('Asset'))
commands = models.ManyToManyField(CommandGroup, verbose_name=_('Commands'))
class Meta:
unique_together = ('name', 'org_id')
ordering = ('priority', '-date_updated', 'name')
verbose_name = _('Command acl')

View File

@ -1,24 +1,14 @@
from django.db import models
from django.utils.translation import ugettext_lazy as _
from .base import BaseACL, BaseACLQuerySet
from common.utils import get_request_ip, get_ip_city
from common.utils.ip import contains_ip
from common.utils.time_period import contains_time_period
from common.utils.timezone import local_now_display
class ACLManager(models.Manager):
def valid(self):
return self.get_queryset().valid()
from .base import BaseACL
class LoginACL(BaseACL):
class ActionChoices(models.TextChoices):
reject = 'reject', _('Reject')
allow = 'allow', _('Allow')
confirm = 'confirm', _('Login confirm')
# 用户
user = models.ForeignKey(
'users.User', on_delete=models.CASCADE, verbose_name=_('User'),
@ -26,16 +16,6 @@ class LoginACL(BaseACL):
)
# 规则
rules = models.JSONField(default=dict, verbose_name=_('Rule'))
# 动作
action = models.CharField(
max_length=64, verbose_name=_('Action'),
choices=ActionChoices.choices, default=ActionChoices.reject
)
reviewers = models.ManyToManyField(
'users.User', verbose_name=_("Reviewers"),
related_name="login_confirm_acls", blank=True
)
objects = ACLManager.from_queryset(BaseACLQuerySet)()
class Meta:
ordering = ('priority', '-date_updated', 'name')

View File

@ -2,37 +2,43 @@ from django.db import models
from django.db.models import Q
from django.utils.translation import ugettext_lazy as _
from orgs.mixins.models import OrgModelMixin, OrgManager
from .base import BaseACL, BaseACLQuerySet
from .base import BaseACL, BaseACLQuerySet, ACLManager
from common.utils.ip import contains_ip
class ACLManager(OrgManager):
class ACLQuerySet(BaseACLQuerySet):
def filter_user(self, user):
return self.filter(
Q(users__username_group__contains=user.username) |
Q(users__username_group__contains='*')
)
def valid(self):
return self.get_queryset().valid()
def filter_asset(self, asset):
queryset = self.filter(
Q(assets__name_group__contains=asset.name) |
Q(assets__name_group__contains='*')
)
ids = [
q.id for q in queryset
if contains_ip(asset.address, q.assets.get('address_group', []))
]
queryset = LoginAssetACL.objects.filter(id__in=ids)
return queryset
def filter_account(self, account_username):
return self.filter(
Q(accounts__username_group__contains=account_username) |
Q(accounts__username_group__contains='*')
)
class LoginAssetACL(BaseACL, OrgModelMixin):
class ActionChoices(models.TextChoices):
login_confirm = 'login_confirm', _('Login confirm')
# 条件
users = models.JSONField(verbose_name=_('User'))
accounts = models.JSONField(verbose_name=_('Account'), default=dict)
accounts = models.JSONField(verbose_name=_('Account'))
assets = models.JSONField(verbose_name=_('Asset'))
# 动作
action = models.CharField(
max_length=64, choices=ActionChoices.choices, default=ActionChoices.login_confirm,
verbose_name=_('Action')
)
# 动作: 附加字段
# - login_confirm
reviewers = models.ManyToManyField(
'users.User', related_name='review_login_asset_acls', blank=True,
verbose_name=_("Reviewers")
)
objects = ACLManager.from_queryset(BaseACLQuerySet)()
objects = ACLManager.from_queryset(ACLQuerySet)()
class Meta:
unique_together = ('name', 'org_id')
@ -43,44 +49,7 @@ class LoginAssetACL(BaseACL, OrgModelMixin):
return self.name
@classmethod
def filter(cls, user, asset, account, action):
queryset = cls.objects.filter(action=action)
queryset = cls.filter_user(user, queryset)
queryset = cls.filter_asset(asset, queryset)
queryset = cls.filter_account(account, queryset)
return queryset
@classmethod
def filter_user(cls, user, queryset):
queryset = queryset.filter(
Q(users__username_group__contains=user.username) |
Q(users__username_group__contains='*')
)
return queryset
@classmethod
def filter_asset(cls, asset, queryset):
queryset = queryset.filter(
Q(assets__hostname_group__contains=asset.name) |
Q(assets__hostname_group__contains='*')
)
ids = [q.id for q in queryset if contains_ip(asset.address, q.assets.get('ip_group', []))]
queryset = cls.objects.filter(id__in=ids)
return queryset
@classmethod
def filter_account(cls, account, queryset):
queryset = queryset.filter(
Q(accounts__name_group__contains=account.name) |
Q(accounts__name_group__contains='*')
).filter(
Q(accounts__username_group__contains=account.username) |
Q(accounts__username_group__contains='*')
)
return queryset
@classmethod
def create_login_asset_confirm_ticket(cls, user, asset, account, assignees, org_id):
def create_login_asset_confirm_ticket(cls, user, asset, account_username, assignees, org_id):
from tickets.const import TicketType
from tickets.models import ApplyLoginAssetTicket
title = _('Login asset confirm') + ' ({})'.format(user)
@ -90,7 +59,7 @@ class LoginAssetACL(BaseACL, OrgModelMixin):
'applicant': user,
'apply_login_user': user,
'apply_login_asset': asset,
'apply_login_account': str(account),
'apply_login_account': account_username,
'type': TicketType.login_asset_confirm,
}
ticket = ApplyLoginAssetTicket.objects.create(**data)

View File

View File

@ -1,9 +1,11 @@
from rest_framework import serializers
from django.utils.translation import ugettext_lazy as _
from common.drf.fields import LabeledChoiceField
from common.drf.fields import ObjectRelatedField
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from orgs.models import Organization
from common.drf.fields import LabeledChoiceField
from users.models import User
from acls import models
@ -25,39 +27,28 @@ class LoginAssetACLUsersSerializer(serializers.Serializer):
class LoginAssetACLAssestsSerializer(serializers.Serializer):
ip_group_help_text = _(
address_group_help_text = _(
"Format for comma-delimited string, with * indicating a match all. "
"Such as: "
"192.168.10.1, 192.168.1.0/24, 10.1.1.1-10.1.1.20, 2001:db8:2de::e13, 2001:db8:1a:1110::/64"
" (Domain name support)"
)
ip_group = serializers.ListField(
default=["*"],
child=serializers.CharField(max_length=1024),
label=_("IP"),
help_text=ip_group_help_text,
)
hostname_group = serializers.ListField(
default=["*"],
child=serializers.CharField(max_length=128),
label=_("Hostname"),
help_text=common_help_text,
)
class LoginAssetACLAccountsSerializer(serializers.Serializer):
protocol_group_help_text = _(
"Format for comma-delimited string, with * indicating a match all. "
"Protocol options: {}"
)
name_group = serializers.ListField(
default=["*"],
child=serializers.CharField(max_length=128),
label=_("Name"),
help_text=common_help_text,
)
address_group = serializers.ListField(
default=["*"],
child=serializers.CharField(max_length=1024),
label=_("IP/Host"),
help_text=address_group_help_text,
)
class LoginAssetACLAccountsSerializer(serializers.Serializer):
username_group = serializers.ListField(
default=["*"],
child=serializers.CharField(max_length=128),
@ -70,9 +61,10 @@ class LoginAssetACLSerializer(BulkOrgResourceModelSerializer):
users = LoginAssetACLUsersSerializer()
assets = LoginAssetACLAssestsSerializer()
accounts = LoginAssetACLAccountsSerializer()
reviewers_amount = serializers.IntegerField(
read_only=True, source="reviewers.count"
reviewers = ObjectRelatedField(
queryset=User.objects, many=True, required=False, label=_('Reviewers')
)
reviewers_amount = serializers.IntegerField(read_only=True, source="reviewers.count")
action = LabeledChoiceField(
choices=models.LoginAssetACL.ActionChoices.choices, label=_("Action")
)

View File

@ -10,37 +10,26 @@ __all__ = ['LoginAssetCheckSerializer']
class LoginAssetCheckSerializer(serializers.Serializer):
user_id = serializers.UUIDField(required=True, allow_null=False)
asset_id = serializers.UUIDField(required=True, allow_null=False)
account_id = serializers.UUIDField(required=True, allow_null=False)
account_username = serializers.CharField(max_length=128, default='')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.user = None
self.asset = None
self.account = None
self._account_username = None
def validate_user_id(self, user_id):
self.user = self.validate_object_exist(User, user_id)
self.user = self.get_object(User, user_id)
return user_id
def validate_asset_id(self, asset_id):
self.asset = self.validate_object_exist(Asset, asset_id)
self.asset = self.get_object(Asset, asset_id)
return asset_id
def validate_account_id(self, account_id):
self.account = self.validate_object_exist(Account, account_id)
return account_id
@staticmethod
def validate_object_exist(model, field_id):
def get_object(model, pk):
with tmp_to_root_org():
obj = get_object_or_none(model, pk=field_id)
if not obj:
obj = get_object_or_none(model, pk=pk)
if obj:
return obj
error = '{} Model object does not exist'.format(model.__name__)
raise serializers.ValidationError(error)
return obj
@lazyproperty
def org(self):
return self.asset.org

View File

@ -1,6 +1,10 @@
from orgs.mixins.api import OrgBulkModelViewSet
from assets.models import AccountTemplate
from assets import serializers
from assets.models import AccountTemplate
from rbac.permissions import RBACPermission
from authentication.const import ConfirmType
from common.mixins import RecordViewLogMixin
from common.permissions import UserConfirmation
from orgs.mixins.api import OrgBulkModelViewSet
class AccountTemplateViewSet(OrgBulkModelViewSet):
@ -10,3 +14,16 @@ class AccountTemplateViewSet(OrgBulkModelViewSet):
serializer_classes = {
'default': serializers.AccountTemplateSerializer
}
class AccountTemplateSecretsViewSet(RecordViewLogMixin, AccountTemplateViewSet):
serializer_classes = {
'default': serializers.AccountTemplateSecretSerializer,
}
http_method_names = ['get', 'options']
# Todo: 记得打开
# permission_classes = [RBACPermission, UserConfirmation.require(ConfirmType.MFA)]
rbac_perms = {
'list': 'assets.view_accounttemplatesecret',
'retrieve': 'assets.view_accounttemplatesecret',
}

View File

@ -6,8 +6,8 @@ from rest_framework.decorators import action
from rest_framework.response import Response
from assets import serializers
from assets.models import Asset
from assets.filters import IpInFilterBackend, LabelFilterBackend, NodeFilterBackend
from assets.models import Asset
from assets.tasks import (
push_accounts_to_assets, test_assets_connectivity_manual,
update_assets_hardware_info_manual, verify_accounts_connectivity,
@ -24,6 +24,7 @@ __all__ = [
"AssetViewSet",
"AssetTaskCreateApi",
"AssetsTaskCreateApi",
'AssetFilterSet'
]

View File

@ -1,5 +1,5 @@
# ~*~ coding: utf-8 ~*~
from django.db.models import F
from django.views.generic.detail import SingleObjectMixin
from django.utils.translation import ugettext as _
from rest_framework.views import APIView, Response
@ -29,12 +29,13 @@ class DomainViewSet(OrgBulkModelViewSet):
class GatewayViewSet(OrgBulkModelViewSet):
perm_model = Host
filterset_fields = ("domain__name", "name", "domain")
search_fields = ("domain__name",)
serializer_class = serializers.GatewaySerializer
def get_queryset(self):
queryset = Host.get_gateway_queryset()
queryset = Domain.get_gateway_queryset()
return queryset
@ -44,17 +45,17 @@ class GatewayTestConnectionApi(SingleObjectMixin, APIView):
}
def get_queryset(self):
queryset = Host.get_gateway_queryset()
queryset = Domain.get_gateway_queryset()
return queryset
def post(self, request, *args, **kwargs):
self.object = self.get_object()
local_port = self.request.data.get('port') or self.object.port
gateway = self.get_object()
local_port = self.request.data.get('port') or gateway.port
try:
local_port = int(local_port)
except ValueError:
raise ValidationError({'port': _('Number required')})
ok, e = self.object.test_connective(local_port=local_port)
ok, e = gateway.test_connective(local_port=local_port)
if ok:
return Response("ok")
else:

View File

@ -1,10 +1,10 @@
from typing import List
from rest_framework.request import Request
from common.utils import lazyproperty, timeit
from assets.models import Node, Asset
from assets.pagination import NodeAssetTreePagination
from assets.models import Node, PlatformProtocol
from assets.utils import get_node_from_request, is_query_node_all_assets
from common.utils import lazyproperty, timeit
class SerializeToTreeNodeMixin:
@ -38,16 +38,11 @@ class SerializeToTreeNodeMixin:
]
return data
def get_platform(self, asset: Asset):
default = 'file'
icon = {'windows', 'linux'}
platform = asset.platform.type.lower()
if platform in icon:
return platform
return default
@timeit
def serialize_assets(self, assets, node_key=None):
sftp_enabled_platform = PlatformProtocol.objects \
.filter(name='ssh', setting__sftp_enabled=True) \
.values_list('platform', flat=True).distinct()
if node_key is None:
get_pid = lambda asset: getattr(asset, 'parent_key', '')
else:
@ -61,17 +56,13 @@ class SerializeToTreeNodeMixin:
'pId': get_pid(asset),
'isParent': False,
'open': False,
'iconSkin': self.get_platform(asset),
'iconSkin': asset.type,
'chkDisabled': not asset.is_active,
'meta': {
'type': 'asset',
'data': {
'id': asset.id,
'name': asset.name,
'address': asset.address,
'protocols': asset.protocols_as_list,
'platform': asset.platform.id,
'org_name': asset.org_name
'org_name': asset.org_name,
'sftp': asset.platform_id in sftp_enabled_platform,
},
}
}
@ -81,7 +72,6 @@ class SerializeToTreeNodeMixin:
class NodeFilterMixin:
# pagination_class = NodeAssetTreePagination
request: Request
@lazyproperty

View File

@ -1,7 +1,7 @@
- hosts: mysql
gather_facts: no
vars:
ansible_python_interpreter: /Users/xiaofeng/Desktop/jumpserver/venv/bin/python
ansible_python_interpreter: /usr/local/bin/python
tasks:
- name: Get info

View File

@ -0,0 +1,17 @@
# Generated by Django 3.2.14 on 2022-11-28 10:39
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('assets', '0112_gateway_to_asset'),
]
operations = [
migrations.AlterModelOptions(
name='accounttemplate',
options={'permissions': [('view_accounttemplatesecret', 'Can view asset account template secret'), ('change_accounttemplatesecret', 'Can change asset account template secret')], 'verbose_name': 'Account template'},
),
]

View File

@ -0,0 +1,14 @@
# Generated by Django 3.2.14 on 2022-11-29 05:14
from django.db import migrations, models
import django.db.models.deletion
# TODO 最后去掉这个迁移
class Migration(migrations.Migration):
dependencies = [
('assets', '0113_alter_accounttemplate_options'),
]
operations = [
]

View File

@ -0,0 +1,38 @@
# Generated by Django 3.2.14 on 2022-11-30 03:18
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0114_node_domain'),
]
operations = [
migrations.AddField(
model_name='database',
name='allow_invalid_cert',
field=models.BooleanField(default=False, verbose_name='Allow invalid cert'),
),
migrations.AddField(
model_name='database',
name='ca_cert',
field=models.TextField(blank=True, verbose_name='CA cert'),
),
migrations.AddField(
model_name='database',
name='client_cert',
field=models.TextField(blank=True, verbose_name='Client cert'),
),
migrations.AddField(
model_name='database',
name='client_key',
field=models.TextField(blank=True, verbose_name='Client key'),
),
migrations.AddField(
model_name='database',
name='use_ssl',
field=models.BooleanField(default=False, verbose_name='Use SSL'),
),
]

View File

@ -0,0 +1,16 @@
# Generated by Django 3.2.14 on 2022-12-01 07:08
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('assets', '0115_auto_20221130_1118'),
]
operations = [
migrations.DeleteModel(
name='Gateway',
),
]

View File

@ -0,0 +1,24 @@
# Generated by Django 3.2.14 on 2022-12-01 07:21
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('assets', '0116_delete_gateway'),
]
operations = [
migrations.CreateModel(
name='Gateway',
fields=[
],
options={
'proxy': True,
'indexes': [],
'constraints': [],
},
bases=('assets.host',),
),
]

View File

@ -94,6 +94,10 @@ class AccountTemplate(BaseAccount):
unique_together = (
('name', 'org_id'),
)
permissions = [
('view_accounttemplatesecret', _('Can view asset account template secret')),
('change_accounttemplatesecret', _('Can change asset account template secret')),
]
def __str__(self):
return self.username

View File

@ -160,10 +160,6 @@ class Asset(NodesRelationMixin, AbsConnectivity, JMSOrgBaseModel):
return 0
return self.primary_protocol.port
@property
def protocols_as_list(self):
return [{'name': p.name, 'port': p.port} for p in self.protocols.all()]
@lazyproperty
def type(self):
return self.platform.type
@ -211,17 +207,6 @@ class Asset(NodesRelationMixin, AbsConnectivity, JMSOrgBaseModel):
tree_node = TreeNode(**data)
return tree_node
def filter_accounts(self, account_names=None):
from perms.models import AssetPermission
if account_names is None:
return self.accounts.all()
if AssetPermission.SpecialAccount.ALL in account_names:
return self.accounts.all()
# queries = Q(name__in=account_names) | Q(username__in=account_names)
queries = Q(username__in=account_names)
accounts = self.accounts.filter(queries)
return accounts
class Meta:
unique_together = [('org_id', 'name')]
verbose_name = _("Asset")

View File

@ -6,6 +6,11 @@ from .common import Asset
class Database(Asset):
db_name = models.CharField(max_length=1024, verbose_name=_("Database"), blank=True)
use_ssl = models.BooleanField(default=False, verbose_name=_("Use SSL"))
ca_cert = models.TextField(verbose_name=_("CA cert"), blank=True)
client_cert = models.TextField(verbose_name=_("Client cert"), blank=True)
client_key = models.TextField(verbose_name=_("Client key"), blank=True)
allow_invalid_cert = models.BooleanField(default=False, verbose_name=_('Allow invalid cert'))
def __str__(self):
return '{}({}://{}/{})'.format(self.name, self.type, self.address, self.db_name)
@ -18,6 +23,11 @@ class Database(Asset):
def specific(self):
return {
'db_name': self.db_name,
'use_ssl': self.use_ssl,
'ca_cert': self.ca_cert,
'client_cert': self.client_cert,
'client_key': self.client_key,
'allow_invalid_cert': self.allow_invalid_cert,
}
class Meta:

View File

@ -3,10 +3,4 @@ from .common import Asset
class Host(Asset):
@classmethod
def get_gateway_queryset(cls):
queryset = cls.objects.filter(
platform__name=GATEWAY_NAME
)
return queryset
pass

View File

@ -1,23 +1,22 @@
# -*- coding: utf-8 -*-
#
import io
import os
import sshpubkeys
from hashlib import md5
from django.db import models
import sshpubkeys
from django.conf import settings
from django.utils import timezone
from django.db import models
from django.db.models import QuerySet
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
from assets.const import Connectivity, SecretType
from common.db import fields
from common.utils import (
ssh_key_string_to_obj, ssh_key_gen, get_logger,
random_string, ssh_pubkey_gen, lazyproperty
random_string, lazyproperty, parse_ssh_public_key_str
)
from common.db import fields
from orgs.mixins.models import JMSOrgBaseModel
from assets.const import Connectivity, SecretType
logger = get_logger(__file__)
@ -62,6 +61,10 @@ class BaseAccount(JMSOrgBaseModel):
def has_secret(self):
return bool(self.secret)
@property
def has_username(self):
return bool(self.username)
@property
def specific(self):
data = {}
@ -84,7 +87,7 @@ class BaseAccount(JMSOrgBaseModel):
@lazyproperty
def public_key(self):
if self.secret_type == SecretType.SSH_KEY and self.private_key:
return ssh_pubkey_gen(private_key=self.private_key)
return parse_ssh_public_key_str(private_key=self.private_key)
return None
@property
@ -93,7 +96,7 @@ class BaseAccount(JMSOrgBaseModel):
public_key = self.public_key
elif self.private_key:
try:
public_key = ssh_pubkey_gen(private_key=self.private_key)
public_key = parse_ssh_public_key_str(self.private_key)
except IOError as e:
return str(e)
else:
@ -125,12 +128,9 @@ class BaseAccount(JMSOrgBaseModel):
return key_path
def get_private_key(self):
if not self.private_key_obj:
if not self.private_key:
return None
string_io = io.StringIO()
self.private_key_obj.write_private_key(string_io)
private_key = string_io.getvalue()
return private_key
return self.private_key
@property
def public_key_obj(self):

View File

@ -183,7 +183,7 @@ class CommandFilterRule(OrgModelMixin):
cls, user_id=None, user_group_id=None, account=None,
asset_id=None, org_id=None
):
from perms.models.const import SpecialAccount
from assets.models import Account
user_groups = []
user = get_object_or_none(User, pk=user_id)
if user:
@ -202,7 +202,7 @@ class CommandFilterRule(OrgModelMixin):
if account:
org_id = account.org_id
q |= Q(accounts__contains=account.username) | \
Q(accounts__contains=SpecialAccount.ALL.value)
Q(accounts__contains=Account.AliasAccount.ALL)
if asset:
org_id = asset.org_id
q |= Q(assets=asset)

View File

@ -1,25 +1,21 @@
# -*- coding: utf-8 -*-
#
import uuid
import socket
import random
import paramiko
import paramiko
from django.db import models
from django.core.cache import cache
from django.db.models.query import QuerySet
from django.utils.translation import ugettext_lazy as _
from common.db import fields
from common.utils import get_logger, lazyproperty
from orgs.mixins.models import OrgModelMixin
from assets.models import Host
from .base import BaseAccount
from ..const import SecretType
from assets.models import Host, Platform
from assets.const import GATEWAY_NAME
from orgs.mixins.models import OrgManager
logger = get_logger(__file__)
__all__ = ['Domain', 'GatewayMixin']
__all__ = ['Domain', 'Gateway']
class Domain(OrgModelMixin):
@ -36,9 +32,13 @@ class Domain(OrgModelMixin):
def __str__(self):
return self.name
@classmethod
def get_gateway_queryset(cls):
return Gateway.objects.all()
@lazyproperty
def gateways(self):
return Host.get_gateway_queryset().filter(domain=self, is_active=True)
return self.get_gateway_queryset().filter(domain=self, is_active=True)
def select_gateway(self):
return self.random_gateway()
@ -53,158 +53,30 @@ class Domain(OrgModelMixin):
return random.choice(self.gateways)
class GatewayMixin:
id: uuid.UUID
port: int
address: str
accounts: QuerySet
private_key_path: str
private_key_obj: paramiko.RSAKey
UNCONNECTED_KEY_TMPL = 'asset_unconnective_gateway_{}'
UNCONNECTED_SILENCE_PERIOD_KEY_TMPL = 'asset_unconnective_gateway_silence_period_{}'
UNCONNECTED_SILENCE_PERIOD_BEGIN_VALUE = 60 * 5
class GatewayManager(OrgManager):
def get_queryset(self):
queryset = super().get_queryset()
queryset = queryset.filter(platform__name=GATEWAY_NAME)
return queryset
def set_unconnected(self):
unconnected_key = self.UNCONNECTED_KEY_TMPL.format(self.id)
unconnected_silence_period_key = self.UNCONNECTED_SILENCE_PERIOD_KEY_TMPL.format(self.id)
unconnected_silence_period = cache.get(
unconnected_silence_period_key, self.UNCONNECTED_SILENCE_PERIOD_BEGIN_VALUE
)
cache.set(unconnected_silence_period_key, unconnected_silence_period * 2)
cache.set(unconnected_key, unconnected_silence_period, unconnected_silence_period)
def set_connective(self):
unconnected_key = self.UNCONNECTED_KEY_TMPL.format(self.id)
unconnected_silence_period_key = self.UNCONNECTED_SILENCE_PERIOD_KEY_TMPL.format(self.id)
cache.delete(unconnected_key)
cache.delete(unconnected_silence_period_key)
def get_is_unconnected(self):
unconnected_key = self.UNCONNECTED_KEY_TMPL.format(self.id)
return cache.get(unconnected_key, False)
@property
def is_connective(self):
return not self.get_is_unconnected()
@is_connective.setter
def is_connective(self, value):
if value:
self.set_connective()
else:
self.set_unconnected()
def test_connective(self, local_port=None):
# TODO 走ansible runner
if local_port is None:
local_port = self.port
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
proxy = paramiko.SSHClient()
proxy.set_missing_host_key_policy(paramiko.AutoAddPolicy())
try:
proxy.connect(self.address, port=self.port,
username=self.username,
password=self.password,
pkey=self.private_key_obj)
except(paramiko.AuthenticationException,
paramiko.BadAuthenticationType,
paramiko.SSHException,
paramiko.ChannelException,
paramiko.ssh_exception.NoValidConnectionsError,
socket.gaierror) as e:
err = str(e)
if err.startswith('[Errno None] Unable to connect to port'):
err = _('Unable to connect to port {port} on {address}')
err = err.format(port=self.port, ip=self.address)
elif err == 'Authentication failed.':
err = _('Authentication failed')
elif err == 'Connect failed':
err = _('Connect failed')
self.is_connective = False
return False, err
try:
sock = proxy.get_transport().open_channel(
'direct-tcpip', ('127.0.0.1', local_port), ('127.0.0.1', 0)
)
client.connect("127.0.0.1", port=local_port,
username=self.username,
password=self.password,
key_filename=self.private_key_path,
sock=sock,
timeout=5)
except (paramiko.SSHException,
paramiko.ssh_exception.SSHException,
paramiko.ChannelException,
paramiko.AuthenticationException,
TimeoutError) as e:
err = getattr(e, 'text', str(e))
if err == 'Connect failed':
err = _('Connect failed')
self.is_connective = False
return False, err
finally:
client.close()
self.is_connective = True
return True, None
@lazyproperty
def username(self):
account = self.accounts.all().first()
if account:
return account.username
logger.error(f'Gateway {self} has no account')
return ''
def get_secret(self, secret_type):
account = self.accounts.filter(secret_type=secret_type).first()
if account:
return account.secret
logger.error(f'Gateway {self} has no {secret_type} account')
@lazyproperty
def password(self):
secret_type = SecretType.PASSWORD
return self.get_secret(secret_type)
@lazyproperty
def private_key(self):
secret_type = SecretType.SSH_KEY
return self.get_secret(secret_type)
def bulk_create(self, objs, batch_size=None, ignore_conflicts=False):
platform = Gateway().default_platform
for obj in objs:
obj.platform_id = platform.id
return super().bulk_create(objs, batch_size, ignore_conflicts)
class Gateway(BaseAccount):
class Protocol(models.TextChoices):
ssh = 'ssh', 'SSH'
name = models.CharField(max_length=128, verbose_name='Name')
ip = models.CharField(max_length=128, verbose_name=_('IP'), db_index=True)
port = models.IntegerField(default=22, verbose_name=_('Port'))
protocol = models.CharField(
choices=Protocol.choices, max_length=16, default=Protocol.ssh, verbose_name=_("Protocol")
)
domain = models.ForeignKey(Domain, on_delete=models.CASCADE, verbose_name=_("Domain"))
comment = models.CharField(max_length=128, blank=True, null=True, verbose_name=_("Comment"))
is_active = models.BooleanField(default=True, verbose_name=_("Is active"))
password = fields.EncryptCharField(max_length=256, blank=True, null=True, verbose_name=_('Password'))
private_key = fields.EncryptTextField(blank=True, null=True, verbose_name=_('SSH private key'))
public_key = fields.EncryptTextField(blank=True, null=True, verbose_name=_('SSH public key'))
secret = None
secret_type = None
privileged = None
def __str__(self):
return self.name
class Gateway(Host):
objects = GatewayManager()
class Meta:
unique_together = [('name', 'org_id')]
verbose_name = _("Gateway")
permissions = [
('test_gateway', _('Test gateway'))
]
proxy = True
@lazyproperty
def default_platform(self):
return Platform.objects.get(name=GATEWAY_NAME, internal=True)
def save(self, *args, **kwargs):
platform = self.default_platform
self.platform_id = platform.id
return super().save(*args, **kwargs)

View File

@ -554,8 +554,9 @@ class Node(OrgModelMixin, SomeNodesMixin, FamilyMixin, NodeAssetsMixin):
full_value = models.CharField(max_length=4096, verbose_name=_('Full value'), default='')
child_mark = models.IntegerField(default=0)
date_create = models.DateTimeField(auto_now_add=True)
parent_key = models.CharField(max_length=64, verbose_name=_("Parent key"),
db_index=True, default='')
parent_key = models.CharField(
max_length=64, verbose_name=_("Parent key"), db_index=True, default=''
)
assets_amount = models.IntegerField(default=0)
objects = OrgManager.from_queryset(NodeQuerySet)()

View File

@ -1,3 +1,4 @@
from common.drf.serializers import SecretReadableMixin
from assets.models import AccountTemplate
from .base import BaseAccountSerializer
@ -17,3 +18,10 @@ class AccountTemplateSerializer(BaseAccountSerializer):
# if not required_field_dict:
# return
# raise serializers.ValidationError(required_field_dict)
class AccountTemplateSecretSerializer(SecretReadableMixin, AccountTemplateSerializer):
class Meta(AccountTemplateSerializer.Meta):
extra_kwargs = {
'secret': {'write_only': False},
}

View File

@ -5,11 +5,11 @@ from rest_framework.generics import get_object_or_404
from django.utils.translation import ugettext_lazy as _
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from common.drf.serializers import SecretReadableMixin
from common.drf.serializers import SecretReadableMixin, WritableNestedModelSerializer
from common.drf.fields import ObjectRelatedField, EncryptedField
from assets.const import SecretType
from ..models import Domain, Asset, Account
from ..serializers import HostSerializer
from assets.const import SecretType, GATEWAY_NAME
from ..serializers import AssetProtocolsSerializer
from ..models import Platform, Domain, Node, Asset, Account, Host
from .utils import validate_password_for_ansible, validate_ssh_key
@ -41,7 +41,7 @@ class DomainSerializer(BulkOrgResourceModelSerializer):
return obj.gateways.count()
class GatewaySerializer(HostSerializer):
class GatewaySerializer(BulkOrgResourceModelSerializer, WritableNestedModelSerializer):
password = EncryptedField(
label=_('Password'), required=False, allow_blank=True, allow_null=True, max_length=1024,
validators=[validate_password_for_ansible], write_only=True
@ -55,13 +55,27 @@ class GatewaySerializer(HostSerializer):
max_length=512,
)
username = serializers.CharField(
label=_('Username'), allow_blank=True, max_length=128, required=True,
label=_('Username'), allow_blank=True, max_length=128, required=True, write_only=True
)
username_display = serializers.SerializerMethodField(label=_('Username'))
protocols = AssetProtocolsSerializer(many=True, required=False, label=_('Protocols'))
class Meta(HostSerializer.Meta):
fields = HostSerializer.Meta.fields + [
'username', 'password', 'private_key', 'passphrase'
class Meta:
model = Host
fields_mini = ['id', 'name', 'address']
fields_small = fields_mini + ['is_active', 'comment']
fields = fields_small + ['domain', 'protocols'] + [
'username', 'password', 'private_key', 'passphrase', 'username_display'
]
extra_kwargs = {
'name': {'label': _("Name")},
'address': {'label': _('Address')},
}
@staticmethod
def get_username_display(obj):
account = obj.accounts.order_by('-privileged').first()
return account.username if account else ''
def validate_private_key(self, secret):
if not secret:

View File

@ -1,9 +1,7 @@
from io import StringIO
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
from common.utils import ssh_private_key_gen, validate_ssh_private_key
from common.utils import validate_ssh_private_key, parse_ssh_private_key_str
def validate_password_for_ansible(password):
@ -24,9 +22,4 @@ def validate_ssh_key(ssh_key, passphrase=None):
valid = validate_ssh_private_key(ssh_key, password=passphrase)
if not valid:
raise serializers.ValidationError(_("private key invalid or passphrase error"))
ssh_key = ssh_private_key_gen(ssh_key, password=passphrase)
string_io = StringIO()
ssh_key.write_private_key(string_io)
ssh_key = string_io.getvalue()
return ssh_key
return parse_ssh_private_key_str(ssh_key, passphrase)

View File

@ -12,11 +12,14 @@ __all__ = [
@org_aware_func("assets")
def push_accounts_to_assets_util(accounts, assets):
def push_accounts_to_assets_util(accounts, assets, username=None):
from assets.models import PushAccountAutomation
task_name = gettext_noop("Push accounts to assets")
task_name = PushAccountAutomation.generate_unique_name(task_name)
if username is None:
account_usernames = list(accounts.values_list('username', flat=True))
else:
account_usernames = [username]
data = {
'name': task_name,
@ -29,10 +32,10 @@ def push_accounts_to_assets_util(accounts, assets):
@shared_task(queue="ansible", verbose_name=_('Push accounts to assets'))
def push_accounts_to_assets(account_ids, asset_ids):
def push_accounts_to_assets(account_ids, asset_ids, username=None):
from assets.models import Asset, Account
with tmp_to_root_org():
assets = Asset.objects.filter(id__in=asset_ids)
accounts = Account.objects.filter(id__in=account_ids)
return push_accounts_to_assets_util(accounts, assets)
return push_accounts_to_assets_util(accounts, assets, username)

View File

@ -16,6 +16,7 @@ router.register(r'webs', api.WebViewSet, 'web')
router.register(r'clouds', api.CloudViewSet, 'cloud')
router.register(r'accounts', api.AccountViewSet, 'account')
router.register(r'account-templates', api.AccountTemplateViewSet, 'account-template')
router.register(r'account-template-secrets', api.AccountTemplateSecretsViewSet, 'account-template-secret')
router.register(r'account-secrets', api.AccountSecretsViewSet, 'account-secret')
router.register(r'platforms', api.AssetPlatformViewSet, 'platform')
router.register(r'labels', api.LabelViewSet, 'label')

View File

@ -1,7 +1,6 @@
import base64
import json
import os
import time
import urllib.parse
from django.http import HttpResponse
@ -12,17 +11,20 @@ from rest_framework.decorators import action
from rest_framework.exceptions import PermissionDenied
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.serializers import ValidationError
from common.drf.api import JMSModelViewSet
from common.http import is_true
from common.utils import random_string
from common.utils.django import get_request_os
from orgs.mixins.api import RootOrgViewMixin
from orgs.utils import tmp_to_root_org
from perms.models import ActionChoices
from terminal.models import EndpointRule
from terminal.const import NativeClient, TerminalType
from terminal.models import EndpointRule, Applet
from ..models import ConnectionToken
from ..serializers import (
ConnectionTokenSerializer, ConnectionTokenSecretSerializer,
SuperConnectionTokenSerializer, ConnectionTokenDisplaySerializer,
SuperConnectionTokenSerializer,
)
__all__ = ['ConnectionTokenViewSet', 'SuperConnectionTokenViewSet']
@ -32,13 +34,34 @@ class RDPFileClientProtocolURLMixin:
request: Request
get_serializer: callable
@staticmethod
def set_applet_info(token, rdp_options):
# remote-app
applet = Applet.objects.filter(name=token.connect_method).first()
if not applet:
return rdp_options
cmdline = {
'app_name': applet.name,
'user_id': str(token.user.id),
'asset_id': str(token.asset.id),
'token_id': str(token.id)
}
app = '||tinker'
rdp_options['remoteapplicationmode:i'] = '1'
rdp_options['alternate shell:s'] = app
rdp_options['remoteapplicationprogram:s'] = app
rdp_options['remoteapplicationname:s'] = app
cmdline_b64 = base64.b64encode(json.dumps(cmdline).encode()).decode()
rdp_options['remoteapplicationcmdline:s'] = cmdline_b64
return rdp_options
def get_rdp_file_info(self, token: ConnectionToken):
rdp_options = {
'full address:s': '',
'username:s': '',
# 'screen mode id:i': '1',
# 'desktopwidth:i': '1280',
# 'desktopheight:i': '800',
'use multimon:i': '0',
'session bpp:i': '32',
'audiomode:i': '0',
@ -59,11 +82,6 @@ class RDPFileClientProtocolURLMixin:
'bookmarktype:i': '3',
'use redirection server name:i': '0',
'smart sizing:i': '1',
# 'drivestoredirect:s': '*',
# 'domain:s': ''
# 'alternate shell:s:': '||MySQLWorkbench',
# 'remoteapplicationname:s': 'Firefox',
# 'remoteapplicationcmdline:s': '',
}
# 设置磁盘挂载
@ -96,16 +114,11 @@ class RDPFileClientProtocolURLMixin:
rdp_options['session bpp:i'] = os.getenv('JUMPSERVER_COLOR_DEPTH', '32')
rdp_options['audiomode:i'] = self.parse_env_bool('JUMPSERVER_DISABLE_AUDIO', 'false', '2', '0')
if token.asset:
# 设置远程应用
self.set_applet_info(token, rdp_options)
# 文件名
name = token.asset.name
# remote-app
# app = '||jmservisor'
# rdp_options['remoteapplicationmode:i'] = '1'
# rdp_options['alternate shell:s'] = app
# rdp_options['remoteapplicationprogram:s'] = app
# rdp_options['remoteapplicationname:s'] = name
else:
name = '*'
prefix_name = f'{token.user.username}-{name}'
filename = self.get_connect_filename(prefix_name)
@ -129,41 +142,39 @@ class RDPFileClientProtocolURLMixin:
return true_value if is_true(os.getenv(env_key, env_default)) else false_value
def get_client_protocol_data(self, token: ConnectionToken):
protocol = token.protocol
username = token.user.username
rdp_config = ssh_token = ''
if protocol == 'rdp':
filename, rdp_config = self.get_rdp_file_info(token)
elif protocol == 'ssh':
filename, ssh_token = self.get_ssh_token(token)
else:
raise ValueError('Protocol not support: {}'.format(protocol))
_os = get_request_os(self.request)
return {
"filename": filename,
"protocol": protocol,
"username": username,
"token": ssh_token,
"config": rdp_config
}
connect_method_name = token.connect_method
connect_method_dict = TerminalType.get_connect_method(
token.connect_method, token.protocol, _os
)
if connect_method_dict is None:
raise ValueError('Connect method not support: {}'.format(connect_method_name))
def get_ssh_token(self, token: ConnectionToken):
if token.asset:
name = token.asset.name
else:
name = '*'
prefix_name = f'{token.user.username}-{name}'
filename = self.get_connect_filename(prefix_name)
endpoint = self.get_smart_endpoint(protocol='ssh', asset=token.asset)
data = {
'ip': endpoint.host,
'port': str(endpoint.ssh_port),
'username': 'JMS-{}'.format(str(token.id)),
'password': token.secret
'id': str(token.id),
'value': token.value,
'protocol': token.protocol,
'command': '',
'file': {}
}
token = json.dumps(data)
return filename, token
if connect_method_name == NativeClient.mstsc:
filename, content = self.get_rdp_file_info(token)
data.update({
'file': {
'name': filename,
'content': content,
}
})
else:
endpoint = self.get_smart_endpoint(
protocol=connect_method_dict['endpoint_protocol'],
asset=token.asset
)
cmd = NativeClient.get_launch_command(connect_method_name, token, endpoint)
data.update({'command': cmd})
return data
def get_smart_endpoint(self, protocol, asset=None):
target_ip = asset.get_target_ip() if asset else ''
@ -177,9 +188,9 @@ class ExtraActionApiMixin(RDPFileClientProtocolURLMixin):
get_serializer: callable
perform_create: callable
@action(methods=['POST', 'GET'], detail=False, url_path='rdp/file')
def get_rdp_file(self, request, *args, **kwargs):
token = self.create_connection_token()
@action(methods=['POST', 'GET'], detail=True, url_path='rdp-file')
def get_rdp_file(self, *args, **kwargs):
token = self.get_object()
token.is_valid()
filename, content = self.get_rdp_file_info(token)
filename = '{}.rdp'.format(filename)
@ -187,9 +198,9 @@ class ExtraActionApiMixin(RDPFileClientProtocolURLMixin):
response['Content-Disposition'] = 'attachment; filename*=UTF-8\'\'%s' % filename
return response
@action(methods=['POST', 'GET'], detail=False, url_path='client-url')
def get_client_protocol_url(self, request, *args, **kwargs):
token = self.create_connection_token()
@action(methods=['POST', 'GET'], detail=True, url_path='client-url')
def get_client_protocol_url(self, *args, **kwargs):
token = self.get_object()
token.is_valid()
try:
protocol_data = self.get_client_protocol_data(token)
@ -208,14 +219,6 @@ class ExtraActionApiMixin(RDPFileClientProtocolURLMixin):
instance.expire()
return Response(status=status.HTTP_204_NO_CONTENT)
def create_connection_token(self):
data = self.request.query_params if self.request.method == 'GET' else self.request.data
serializer = self.get_serializer(data=data)
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
token: ConnectionToken = serializer.instance
return token
class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelViewSet):
filterset_fields = (
@ -224,11 +227,10 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView
search_fields = filterset_fields
serializer_classes = {
'default': ConnectionTokenSerializer,
'list': ConnectionTokenDisplaySerializer,
'retrieve': ConnectionTokenDisplaySerializer,
'get_secret_detail': ConnectionTokenSecretSerializer,
}
rbac_perms = {
'list': 'authentication.view_connectiontoken',
'retrieve': 'authentication.view_connectiontoken',
'create': 'authentication.add_connectiontoken',
'expire': 'authentication.add_connectiontoken',
@ -243,18 +245,25 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView
rbac_perm = 'authentication.view_connectiontokensecret'
if not request.user.has_perm(rbac_perm):
raise PermissionDenied('Not allow to view secret')
token_id = request.data.get('token') or ''
token_id = request.data.get('id') or ''
token = get_object_or_404(ConnectionToken, pk=token_id)
if token.is_expired:
raise ValidationError({'id': 'Token is expired'})
token.is_valid()
serializer = self.get_serializer(instance=token)
expire_now = request.data.get('expire_now', True)
if expire_now:
token.expire()
return Response(serializer.data, status=status.HTTP_200_OK)
def dispatch(self, request, *args, **kwargs):
with tmp_to_root_org():
return super().dispatch(request, *args, **kwargs)
def get_queryset(self):
return ConnectionToken.objects.filter(user=self.request.user)
queryset = ConnectionToken.objects \
.filter(user=self.request.user) \
.filter(date_expired__gt=timezone.now())
return queryset
def get_user(self, serializer):
return self.request.user
@ -269,16 +278,17 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView
data = serializer.validated_data
user = self.get_user(serializer)
asset = data.get('asset')
login = data.get('login')
account_name = data.get('account_name')
data['org_id'] = asset.org_id
data['user'] = user
data['value'] = random_string(16)
util = PermAccountUtil()
permed_account = util.validate_permission(user, asset, login)
permed_account = util.validate_permission(user, asset, account_name)
if not permed_account or not permed_account.actions:
msg = 'user `{}` not has asset `{}` permission for login `{}`'.format(
user, asset, login
user, asset, account_name
)
raise PermissionDenied(msg)
@ -286,9 +296,9 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView
raise PermissionDenied('Expired')
if permed_account.has_secret:
data['secret'] = ''
data['input_secret'] = ''
if permed_account.username != '@INPUT':
data['username'] = ''
data['input_username'] = ''
return permed_account
@ -311,7 +321,7 @@ class SuperConnectionTokenViewSet(ConnectionTokenViewSet):
def renewal(self, request, *args, **kwargs):
from common.utils.timezone import as_current_tz
token_id = request.data.get('token') or ''
token_id = request.data.get('id') or ''
token = get_object_or_404(ConnectionToken, pk=token_id)
date_expired = as_current_tz(token.date_expired)
if token.is_expired:

View File

View File

@ -2,10 +2,10 @@ from django.utils import timezone
from rest_framework.response import Response
from rest_framework.decorators import action
from rbac.permissions import RBACPermission
from common.drf.api import JMSModelViewSet
from ..models import TempToken
from ..serializers import TempTokenSerializer
from rbac.permissions import RBACPermission
class TempTokenViewSet(JMSModelViewSet):

View File

@ -0,0 +1,50 @@
# Generated by Django 3.2.14 on 2022-11-25 14:40
from django.db import migrations, models
import common.db.fields
class Migration(migrations.Migration):
dependencies = [
('authentication', '0015_alter_connectiontoken_login'),
]
operations = [
migrations.RenameField(
model_name='connectiontoken',
old_name='login',
new_name='account_name'
),
migrations.RenameField(
model_name='connectiontoken',
old_name='secret',
new_name='value',
),
migrations.RenameField(
model_name='connectiontoken',
old_name='username',
new_name='input_username',
),
migrations.AlterField(
model_name='connectiontoken',
name='account_name',
field=models.CharField(max_length=128, verbose_name='Account name'),
),
migrations.AlterField(
model_name='connectiontoken',
name='value',
field=models.CharField(default='', max_length=64, verbose_name='Value'),
),
migrations.AddField(
model_name='connectiontoken',
name='input_secret',
field=common.db.fields.EncryptCharField(blank=True, default='', max_length=128,
verbose_name='Input Secret'),
),
migrations.AlterField(
model_name='connectiontoken',
name='input_username',
field=models.CharField(blank=True, default='', max_length=128, verbose_name='Input Username'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 3.2.14 on 2022-11-28 10:39
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('authentication', '0016_auto_20221125_2240'),
]
operations = [
migrations.AddField(
model_name='connectiontoken',
name='connect_method',
field=models.CharField(default='web_ui', max_length=32, verbose_name='Connect method'),
preserve_default=False,
),
]

View File

@ -0,0 +1,19 @@
# Generated by Django 3.2.14 on 2022-11-29 04:49
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('authentication', '0017_auto_20221128_1839'),
]
operations = [
migrations.AddField(
model_name='connectiontoken',
name='endpoint_protocol',
field=models.CharField(choices=[('ssh', 'SSH'), ('rdp', 'RDP'), ('telnet', 'Telnet'), ('vnc', 'VNC'), ('mysql', 'MySQL'), ('mariadb', 'MariaDB'), ('oracle', 'Oracle'), ('postgresql', 'PostgreSQL'), ('sqlserver', 'SQLServer'), ('redis', 'Redis'), ('mongodb', 'MongoDB'), ('k8s', 'K8S'), ('http', 'HTTP'), ('None', ' Settings')], default='', max_length=16, verbose_name='Endpoint protocol'),
preserve_default=False,
),
]

View File

@ -0,0 +1,17 @@
# Generated by Django 3.2.14 on 2022-11-29 13:27
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('authentication', '0018_connectiontoken_endpoint_protocol'),
]
operations = [
migrations.RemoveField(
model_name='connectiontoken',
name='endpoint_protocol',
),
]

View File

@ -1,17 +1,17 @@
import time
from datetime import timedelta
from django.conf import settings
from django.db import models
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
from django.db import models
from django.conf import settings
from rest_framework.exceptions import PermissionDenied
from orgs.mixins.models import OrgModelMixin
from assets.const import Protocol
from common.db.fields import EncryptCharField
from common.db.models import JMSBaseModel
from common.utils import lazyproperty, pretty_string
from common.utils.timezone import as_current_tz
from common.db.models import JMSBaseModel
from common.db.fields import EncryptCharField
from assets.const import Protocol
from orgs.mixins.models import OrgModelMixin
def date_expired_default():
@ -19,6 +19,7 @@ def date_expired_default():
class ConnectionToken(OrgModelMixin, JMSBaseModel):
value = models.CharField(max_length=64, default='', verbose_name=_("Value"))
user = models.ForeignKey(
'users.User', on_delete=models.SET_NULL, null=True, blank=True,
related_name='connection_tokens', verbose_name=_('User')
@ -27,12 +28,13 @@ class ConnectionToken(OrgModelMixin, JMSBaseModel):
'assets.Asset', on_delete=models.SET_NULL, null=True, blank=True,
related_name='connection_tokens', verbose_name=_('Asset'),
)
login = models.CharField(max_length=128, verbose_name=_("Login account"))
username = models.CharField(max_length=128, default='', verbose_name=_("Username"))
secret = EncryptCharField(max_length=64, default='', verbose_name=_("Secret"))
account_name = models.CharField(max_length=128, verbose_name=_("Account name")) # 登录账号Name
input_username = models.CharField(max_length=128, default='', blank=True, verbose_name=_("Input Username"))
input_secret = EncryptCharField(max_length=64, default='', blank=True, verbose_name=_("Input Secret"))
protocol = models.CharField(
choices=Protocol.choices, max_length=16, default=Protocol.ssh, verbose_name=_("Protocol")
)
connect_method = models.CharField(max_length=32, verbose_name=_("Connect method"))
user_display = models.CharField(max_length=128, default='', verbose_name=_("User display"))
asset_display = models.CharField(max_length=128, default='', verbose_name=_("Asset display"))
date_expired = models.DateTimeField(
@ -76,7 +78,7 @@ class ConnectionToken(OrgModelMixin, JMSBaseModel):
def permed_account(self):
from perms.utils import PermAccountUtil
permed_account = PermAccountUtil().validate_permission(
self.user, self.asset, self.login
self.user, self.asset, self.account_name
)
return permed_account
@ -99,13 +101,13 @@ class ConnectionToken(OrgModelMixin, JMSBaseModel):
is_valid = False
error = _('No asset or inactive asset')
return is_valid, error
if not self.login:
if not self.account_name:
error = _('No account')
raise PermissionDenied(error)
if not self.permed_account or not self.permed_account.actions:
msg = 'user `{}` not has asset `{}` permission for login `{}`'.format(
self.user, self.asset, self.login
self.user, self.asset, self.account_name
)
raise PermissionDenied(msg)
@ -122,20 +124,22 @@ class ConnectionToken(OrgModelMixin, JMSBaseModel):
if not self.asset:
return None
account = self.asset.accounts.filter(name=self.login).first()
if self.login == '@INPUT' or not account:
account = self.asset.accounts.filter(name=self.account_name).first()
if self.account_name == '@INPUT' or not account:
return {
'name': self.login,
'username': self.username,
'name': self.account_name,
'username': self.input_username,
'secret_type': 'password',
'secret': self.secret
'secret': self.input_secret,
'su_from': None
}
else:
return {
'name': account.name,
'username': account.username,
'secret_type': account.secret_type,
'secret': account.secret_type or self.secret
'secret': account.secret or self.input_secret,
'su_from': account.su_from,
}
@lazyproperty

View File

@ -1,8 +1,8 @@
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
from assets.serializers import PlatformSerializer
from assets.models import Asset, Domain, CommandFilterRule, Account, Platform
from assets.models import Asset, CommandFilterRule, Account, Platform
from assets.serializers import PlatformSerializer, AssetProtocolsSerializer
from authentication.models import ConnectionToken
from orgs.mixins.serializers import OrgResourceModelSerializerMixin
from perms.serializers.permission import ActionChoicesField
@ -15,28 +15,28 @@ __all__ = [
class ConnectionTokenSerializer(OrgResourceModelSerializerMixin):
username = serializers.CharField(max_length=128, label=_("Input username"),
allow_null=True, allow_blank=True)
expire_time = serializers.IntegerField(read_only=True, label=_('Expired time'))
class Meta:
model = ConnectionToken
fields_mini = ['id']
fields_mini = ['id', 'value']
fields_small = fields_mini + [
'protocol', 'login', 'secret', 'username',
'user', 'asset', 'account_name',
'input_username', 'input_secret',
'connect_method', 'protocol',
'actions', 'date_expired', 'date_created',
'date_updated', 'created_by',
'updated_by', 'org_id', 'org_name',
]
fields_fk = [
'user', 'asset',
]
read_only_fields = [
# 普通 Token 不支持指定 user
'user', 'expire_time',
'user_display', 'asset_display',
]
fields = fields_small + fields_fk + read_only_fields
fields = fields_small + read_only_fields
extra_kwargs = {
'value': {'read_only': True},
}
def get_request_user(self):
request = self.context.get('request')
@ -85,19 +85,30 @@ class ConnectionTokenUserSerializer(serializers.ModelSerializer):
class ConnectionTokenAssetSerializer(serializers.ModelSerializer):
""" Asset """
protocols = AssetProtocolsSerializer(many=True, required=False, label=_('Protocols'))
class Meta:
model = Asset
fields = ['id', 'name', 'address', 'protocols', 'org_id']
fields = ['id', 'name', 'address', 'protocols',
'org_id', 'specific']
class ConnectionTokenAccountSerializer(serializers.ModelSerializer):
class SimpleAccountSerializer(serializers.ModelSerializer):
""" Account """
class Meta:
model = Account
fields = ['name', 'username', 'secret_type', 'secret']
class ConnectionTokenAccountSerializer(serializers.ModelSerializer):
""" Account """
su_from = SimpleAccountSerializer(required=False, label=_('Su from'))
class Meta:
model = Account
fields = [
'name', 'username', 'secret_type', 'secret',
'name', 'username', 'secret_type', 'secret', 'su_from',
]
@ -106,16 +117,10 @@ class ConnectionTokenGatewaySerializer(serializers.ModelSerializer):
class Meta:
model = Asset
fields = ['id', 'address', 'port', 'username', 'password', 'private_key']
class ConnectionTokenDomainSerializer(serializers.ModelSerializer):
""" Domain """
gateways = ConnectionTokenGatewaySerializer(many=True, read_only=True)
class Meta:
model = Domain
fields = ['id', 'name', 'gateways']
fields = [
'id', 'address', 'port', 'username',
'password', 'private_key'
]
class ConnectionTokenCmdFilterRuleSerializer(serializers.ModelSerializer):
@ -140,11 +145,12 @@ class ConnectionTokenPlatform(PlatformSerializer):
class ConnectionTokenSecretSerializer(OrgResourceModelSerializerMixin):
expire_now = serializers.BooleanField(label=_('Expired now'), default=True)
user = ConnectionTokenUserSerializer(read_only=True)
asset = ConnectionTokenAssetSerializer(read_only=True)
platform = ConnectionTokenPlatform(read_only=True)
account = ConnectionTokenAccountSerializer(read_only=True)
gateway = ConnectionTokenGatewaySerializer(read_only=True)
platform = ConnectionTokenPlatform(read_only=True)
# cmd_filter_rules = ConnectionTokenCmdFilterRuleSerializer(many=True)
actions = ActionChoicesField()
expire_at = serializers.IntegerField()
@ -152,7 +158,10 @@ class ConnectionTokenSecretSerializer(OrgResourceModelSerializerMixin):
class Meta:
model = ConnectionToken
fields = [
'id', 'secret', 'user', 'asset', 'account',
'protocol', 'domain', 'gateway',
'actions', 'expire_at', 'platform',
'id', 'value', 'user', 'asset', 'account', 'platform',
'protocol', 'gateway', 'actions', 'expire_at', 'expire_now',
]
extra_kwargs = {
'value': {'read_only': True},
'expire_now': {'write_only': True},
}

View File

@ -6,7 +6,6 @@ from .hands import *
class Services(TextChoices):
gunicorn = 'gunicorn', 'gunicorn'
daphne = 'daphne', 'daphne'
celery_ansible = 'celery_ansible', 'celery_ansible'
celery_default = 'celery_default', 'celery_default'
beat = 'beat', 'beat'
@ -22,7 +21,6 @@ class Services(TextChoices):
from . import services
services_map = {
cls.gunicorn.value: services.GunicornService,
cls.daphne: services.DaphneService,
cls.flower: services.FlowerService,
cls.celery_default: services.CeleryDefaultService,
cls.celery_ansible: services.CeleryAnsibleService,
@ -30,13 +28,9 @@ class Services(TextChoices):
}
return services_map.get(name)
@classmethod
def ws_services(cls):
return [cls.daphne]
@classmethod
def web_services(cls):
return [cls.gunicorn, cls.daphne, cls.flower]
return [cls.gunicorn, cls.flower]
@classmethod
def celery_services(cls):

View File

@ -1,6 +1,5 @@
from .beat import *
from .celery_ansible import *
from .celery_default import *
from .daphne import *
from .flower import *
from .gunicorn import *

View File

@ -1,25 +0,0 @@
from ..hands import *
from .base import BaseService
__all__ = ['DaphneService']
class DaphneService(BaseService):
def __init__(self, **kwargs):
super().__init__(**kwargs)
@property
def cmd(self):
print("\n- Start Daphne ASGI WS Server")
cmd = [
'daphne', 'jumpserver.asgi:application',
'-b', HTTP_HOST,
'-p', str(WS_PORT),
]
return cmd
@property
def cwd(self):
return APPS_DIR

View File

@ -17,9 +17,9 @@ class GunicornService(BaseService):
log_format = '%(h)s %(t)s %(L)ss "%(r)s" %(s)s %(b)s '
bind = f'{HTTP_HOST}:{HTTP_PORT}'
cmd = [
'gunicorn', 'jumpserver.wsgi',
'gunicorn', 'jumpserver.asgi:application',
'-b', bind,
'-k', 'gthread',
'-k', 'uvicorn.workers.UvicornWorker',
'--threads', '10',
'-w', str(self.worker),
'--max-requests', '4096',

View File

@ -2,11 +2,11 @@
#
import re
from django.shortcuts import reverse as dj_reverse
from django.conf import settings
from django.utils import timezone
from django.db import models
from django.db.models.signals import post_save, pre_save
from django.shortcuts import reverse as dj_reverse
from django.utils import timezone
UUID_PATTERN = re.compile(r'[0-9a-zA-Z\-]{36}')
@ -80,3 +80,18 @@ def bulk_create_with_signal(cls: models.Model, items, **kwargs):
for i in items:
post_save.send(sender=cls, instance=i, created=True)
return result
def get_request_os(request):
"""获取请求的操作系统"""
agent = request.META.get('HTTP_USER_AGENT', '').lower()
if agent is None:
return 'unknown'
if 'windows' in agent.lower():
return 'windows'
if 'mac' in agent.lower():
return 'mac'
if 'linux' in agent.lower():
return 'linux'
return 'unknown'

View File

@ -1,24 +1,23 @@
# -*- coding: utf-8 -*-
#
import re
import json
from six import string_types
import base64
import os
import time
import hashlib
import json
import os
import re
import time
from io import StringIO
from itertools import chain
import paramiko
import sshpubkeys
from cryptography.hazmat.primitives import serialization
from django.conf import settings
from django.core.serializers.json import DjangoJSONEncoder
from itsdangerous import (
TimedJSONWebSignatureSerializer, JSONWebSignatureSerializer,
BadSignature, SignatureExpired
)
from django.conf import settings
from django.core.serializers.json import DjangoJSONEncoder
from django.db.models.fields.files import FileField
from six import string_types
from .http import http_date
@ -69,22 +68,19 @@ class Signer(metaclass=Singleton):
return None
_supported_paramiko_ssh_key_types = (paramiko.RSAKey, paramiko.DSSKey, paramiko.Ed25519Key)
def ssh_key_string_to_obj(text, password=None):
key = None
for ssh_key_type in _supported_paramiko_ssh_key_types:
if not isinstance(ssh_key_type, paramiko.PKey):
continue
try:
key = paramiko.RSAKey.from_private_key(StringIO(text), password=password)
key = ssh_key_type.from_private_key(StringIO(text), password=password)
return key
except paramiko.SSHException:
pass
else:
return key
try:
key = paramiko.DSSKey.from_private_key(StringIO(text), password=password)
except paramiko.SSHException:
pass
else:
return key
return key
@ -137,17 +133,68 @@ def ssh_key_gen(length=2048, type='rsa', password=None, username='jumpserver', h
def validate_ssh_private_key(text, password=None):
if isinstance(text, bytes):
if isinstance(text, str):
try:
text = text.decode("utf-8")
text = text.encode("utf-8")
except UnicodeDecodeError:
return False
if isinstance(password, str):
try:
password = password.encode("utf-8")
except UnicodeDecodeError:
return False
key = ssh_key_string_to_obj(text, password=password)
if key is None:
return False
else:
return True
key = parse_ssh_private_key_str(text, password=password)
return bool(key)
def parse_ssh_private_key_str(text: bytes, password=None) -> str:
private_key = _parse_ssh_private_key(text, password=password)
if private_key is None:
return ""
private_key_bytes = private_key.private_bytes(serialization.Encoding.PEM,
serialization.PrivateFormat.OpenSSH,
serialization.NoEncryption())
return private_key_bytes.decode('utf-8')
def parse_ssh_public_key_str(text: bytes = "", password=None) -> str:
private_key = _parse_ssh_private_key(text, password=password)
if private_key is None:
return ""
public_key_bytes = private_key.public_key().public_bytes(serialization.Encoding.OpenSSH,
serialization.PublicFormat.OpenSSH)
return public_key_bytes.decode('utf-8')
def _parse_ssh_private_key(text, password=None):
"""
text: bytes
password: str
return:private key types:
ec.EllipticCurvePrivateKey,
rsa.RSAPrivateKey,
dsa.DSAPrivateKey,
ed25519.Ed25519PrivateKey,
"""
if isinstance(text, str):
try:
text = text.encode("utf-8")
except UnicodeDecodeError:
return None
if password is not None:
if isinstance(password, str):
try:
password = password.encode("utf-8")
except UnicodeDecodeError:
return None
try:
private_key = serialization.load_ssh_private_key(text, password=password)
return private_key
except (ValueError, TypeError):
pass
return None
def validate_ssh_public_key(text):

View File

@ -1,9 +1,9 @@
# -*- coding: utf-8 -*-
#
import time
from email.utils import formatdate
import calendar
import threading
import time
from email.utils import formatdate
_STRPTIME_LOCK = threading.Lock()
@ -35,3 +35,6 @@ def http_to_unixtime(time_string):
def iso8601_to_unixtime(time_string):
"""把ISO8601时间字符串形如2012-02-24T06:07:48.000Z转换为UNIX时间精确到秒。"""
return to_unixtime(time_string, _ISO8601_FORMAT)

View File

@ -66,7 +66,7 @@ def contains_ip(ip, ip_group):
if in_ip_segment(ip, _ip):
return True
else:
# is domain name
# address / host
if ip == _ip:
return True

View File

@ -1,22 +1,21 @@
import datetime
import pytz
from datetime import datetime, timedelta, timezone
from django.utils import timezone as dj_timezone
from rest_framework.fields import DateTimeField
max = datetime.datetime.max.replace(tzinfo=datetime.timezone.utc)
max = datetime.max.replace(tzinfo=timezone.utc)
def astimezone(dt: datetime.datetime, tzinfo: pytz.tzinfo.DstTzInfo):
def astimezone(dt: datetime, tzinfo: pytz.tzinfo.DstTzInfo):
assert dj_timezone.is_aware(dt)
return tzinfo.normalize(dt.astimezone(tzinfo))
def as_china_cst(dt: datetime.datetime):
def as_china_cst(dt: datetime):
return astimezone(dt, pytz.timezone('Asia/Shanghai'))
def as_current_tz(dt: datetime.datetime):
def as_current_tz(dt: datetime):
return astimezone(dt, dj_timezone.get_current_timezone())
@ -36,6 +35,15 @@ def local_now_date_display(fmt='%Y-%m-%d'):
return local_now().strftime(fmt)
def local_zero_hour(fmt='%Y-%m-%d'):
return datetime.strptime(local_now().strftime(fmt), fmt)
def local_monday():
zero_hour_time = local_zero_hour()
return zero_hour_time - timedelta(zero_hour_time.weekday())
_rest_dt_field = DateTimeField()
dt_parser = _rest_dt_field.to_internal_value
dt_formatter = _rest_dt_field.to_representation

View File

@ -3,51 +3,131 @@ import time
from django.core.cache import cache
from django.utils import timezone
from django.utils.timesince import timesince
from django.db.models import Count, Max
from django.db.models import Count, Max, F
from django.http.response import JsonResponse, HttpResponse
from rest_framework.views import APIView
from rest_framework.permissions import AllowAny
from collections import Counter
from rest_framework.request import Request
from rest_framework.response import Response
from users.models import User
from assets.models import Asset
from terminal.models import Session
from assets.const import AllTypes
from terminal.models import Session, Command
from terminal.utils import ComponentsPrometheusMetricsUtil
from orgs.utils import current_org
from common.utils import lazyproperty
from audits.models import UserLoginLog, PasswordChangeLog, OperateLog
from audits.const import LoginStatusChoices
from common.utils.timezone import local_now, local_zero_hour
from orgs.caches import OrgResourceStatisticsCache
__all__ = ['IndexApi']
class DatesLoginMetricMixin:
class DateTimeMixin:
request: Request
@property
def org(self):
return current_org
@lazyproperty
def days(self):
query_params = self.request.query_params
if query_params.get('monthly'):
return 30
return 7
count = query_params.get('days')
count = int(count) if count else 0
return count
@property
def days_to_datetime(self):
days = self.days
if days == 0:
t = local_zero_hour()
else:
t = local_now() - timezone.timedelta(days=days)
return t
@lazyproperty
def sessions_queryset(self):
days = timezone.now() - timezone.timedelta(days=self.days)
sessions_queryset = Session.objects.filter(date_start__gt=days)
return sessions_queryset
@lazyproperty
def session_dates_list(self):
now = timezone.now()
def dates_list(self):
now = local_now()
dates = [(now - timezone.timedelta(days=i)).date() for i in range(self.days)]
dates.reverse()
# dates = self.sessions_queryset.dates('date_start', 'day')
return dates
def get_dates_metrics_date(self):
dates_metrics_date = [d.strftime('%m-%d') for d in self.session_dates_list] or ['0']
dates_metrics_date = [d.strftime('%m-%d') for d in self.dates_list] or ['0']
return dates_metrics_date
@lazyproperty
def users(self):
return self.org.get_members()
@lazyproperty
def sessions_queryset(self):
t = self.days_to_datetime
sessions_queryset = Session.objects.filter(date_start__gte=t)
return sessions_queryset
def get_logs_queryset(self, queryset, query_params):
query = {}
if not self.org.is_root():
if query_params == 'username':
query = {
f'{query_params}__in': self.users.values_list('username', flat=True)
}
else:
query = {
f'{query_params}__in': [str(user) for user in self.users]
}
queryset = queryset.filter(**query)
return queryset
@lazyproperty
def login_logs_queryset(self):
t = self.days_to_datetime
queryset = UserLoginLog.objects.filter(datetime__gte=t)
queryset = self.get_logs_queryset(queryset, 'username')
return queryset
@lazyproperty
def password_change_logs_queryset(self):
t = self.days_to_datetime
queryset = PasswordChangeLog.objects.filter(datetime__gte=t)
queryset = self.get_logs_queryset(queryset, 'user')
return queryset
@lazyproperty
def operate_logs_queryset(self):
t = self.days_to_datetime
queryset = OperateLog.objects.filter(datetime__gte=t)
queryset = self.get_logs_queryset(queryset, 'user')
return queryset
@lazyproperty
def ftp_logs_queryset(self):
t = self.days_to_datetime
queryset = OperateLog.objects.filter(datetime__gte=t)
queryset = self.get_logs_queryset(queryset, 'user')
return queryset
@lazyproperty
def command_queryset(self):
t = self.days_to_datetime
t = t.timestamp()
queryset = Command.objects.filter(timestamp__gte=t)
return queryset
class DatesLoginMetricMixin:
dates_list: list
command_queryset: Command.objects
sessions_queryset: Session.objects
ftp_logs_queryset: OperateLog.objects
login_logs_queryset: UserLoginLog.objects
operate_logs_queryset: OperateLog.objects
password_change_logs_queryset: PasswordChangeLog.objects
@staticmethod
def get_cache_key(date, tp):
date_str = date.strftime("%Y%m%d")
@ -86,7 +166,7 @@ class DatesLoginMetricMixin:
def get_dates_metrics_total_count_login(self):
data = []
for d in self.session_dates_list:
for d in self.dates_list:
count = self.get_date_login_count(d)
data.append(count)
if len(data) == 0:
@ -105,7 +185,7 @@ class DatesLoginMetricMixin:
def get_dates_metrics_total_count_active_users(self):
data = []
for d in self.session_dates_list:
for d in self.dates_list:
count = self.get_date_user_count(d)
data.append(count)
return data
@ -122,80 +202,61 @@ class DatesLoginMetricMixin:
def get_dates_metrics_total_count_active_assets(self):
data = []
for d in self.session_dates_list:
for d in self.dates_list:
count = self.get_date_asset_count(d)
data.append(count)
return data
@lazyproperty
def dates_total_count_active_users(self):
count = len(set(self.sessions_queryset.values_list('user_id', flat=True)))
def get_date_session_count(self, date):
tp = "SESSION"
count = self.__get_data_from_cache(date, tp)
if count is not None:
return count
ds, de = self.get_date_start_2_end(date)
count = Session.objects.filter(date_start__range=(ds, de)).count()
self.__set_data_to_cache(date, tp, count)
return count
@lazyproperty
def dates_total_count_inactive_users(self):
total = current_org.get_members().count()
active = self.dates_total_count_active_users
count = total - active
if count < 0:
count = 0
return count
def get_dates_metrics_total_count_sessions(self):
data = []
for d in self.dates_list:
count = self.get_date_session_count(d)
data.append(count)
return data
@lazyproperty
def dates_total_count_disabled_users(self):
return current_org.get_members().filter(is_active=False).count()
def get_type_to_assets(self):
result = Asset.objects.annotate(type=F('platform__type')). \
values('type').order_by('type').annotate(total=Count(1))
all_types_dict = dict(AllTypes.choices())
result = list(result)
for i in result:
tp = i['type']
i['label'] = all_types_dict[tp]
return result
@lazyproperty
def dates_total_count_active_assets(self):
return len(set(self.sessions_queryset.values_list('asset', flat=True)))
@lazyproperty
def dates_total_count_inactive_assets(self):
total = Asset.objects.all().count()
active = self.dates_total_count_active_assets
count = total - active
if count < 0:
count = 0
return count
@lazyproperty
def dates_total_count_disabled_assets(self):
return Asset.objects.filter(is_active=False).count()
# 以下是从week中而来
def get_dates_login_times_top5_users(self):
users = self.sessions_queryset.values_list('user_id', flat=True)
users = [
{'user': user, 'total': total}
for user, total in Counter(users).most_common(5)
]
return users
def get_dates_total_count_login_users(self):
return len(set(self.sessions_queryset.values_list('user_id', flat=True)))
def get_dates_total_count_login_times(self):
return self.sessions_queryset.count()
def get_dates_login_times_top10_assets(self):
def get_dates_login_times_assets(self):
assets = self.sessions_queryset.values("asset") \
.annotate(total=Count("asset")) \
.annotate(last=Max("date_start")).order_by("-total")[:10]
.annotate(last=Max("date_start")).order_by("-total")
assets = assets[:10]
for asset in assets:
asset['last'] = str(asset['last'])
return list(assets)
def get_dates_login_times_top10_users(self):
def get_dates_login_times_users(self):
users = self.sessions_queryset.values("user_id") \
.annotate(total=Count("user_id")) \
.annotate(user=Max('user')) \
.annotate(last=Max("date_start")).order_by("-total")[:10]
.annotate(last=Max("date_start")).order_by("-total")
users = users[:10]
for user in users:
user['last'] = str(user['last'])
return list(users)
def get_dates_login_record_top10_sessions(self):
sessions = self.sessions_queryset.order_by('-date_start')[:10]
def get_dates_login_record_sessions(self):
sessions = self.sessions_queryset.order_by('-date_start')
sessions = sessions[:10]
for session in sessions:
session.avatar_url = User.get_avatar_url("")
sessions = [
@ -210,8 +271,44 @@ class DatesLoginMetricMixin:
]
return sessions
@lazyproperty
def user_login_logs_amount(self):
return self.login_logs_queryset.count()
class IndexApi(DatesLoginMetricMixin, APIView):
@lazyproperty
def user_login_success_logs_amount(self):
return self.login_logs_queryset.filter(status=LoginStatusChoices.success).count()
@lazyproperty
def user_login_amount(self):
return self.login_logs_queryset.values('username').distinct().count()
@lazyproperty
def operate_logs_amount(self):
return self.operate_logs_queryset.count()
@lazyproperty
def change_password_logs_amount(self):
return self.password_change_logs_queryset.count()
@lazyproperty
def commands_amount(self):
return self.command_queryset.count()
@lazyproperty
def commands_danger_amount(self):
return self.command_queryset.filter(risk_level=Command.RISK_LEVEL_DANGEROUS).count()
@lazyproperty
def sessions_amount(self):
return self.sessions_queryset.count()
@lazyproperty
def ftp_logs_amount(self):
return self.ftp_logs_queryset.count()
class IndexApi(DateTimeMixin, DatesLoginMetricMixin, APIView):
http_method_names = ['get']
def check_permissions(self, request):
@ -222,7 +319,7 @@ class IndexApi(DatesLoginMetricMixin, APIView):
query_params = self.request.query_params
caches = OrgResourceStatisticsCache(current_org)
caches = OrgResourceStatisticsCache(self.org)
_all = query_params.get('all')
@ -236,6 +333,26 @@ class IndexApi(DatesLoginMetricMixin, APIView):
'total_count_assets': caches.assets_amount,
})
if _all or query_params.get('total_count') or query_params.get('total_count_users_this_week'):
data.update({
'total_count_users_this_week': caches.new_users_amount_this_week,
})
if _all or query_params.get('total_count') or query_params.get('total_count_assets_this_week'):
data.update({
'total_count_assets_this_week': caches.new_assets_amount_this_week,
})
if _all or query_params.get('total_count') or query_params.get('total_count_login_users'):
data.update({
'total_count_login_users': self.user_login_amount
})
if _all or query_params.get('total_count') or query_params.get('total_count_today_active_assets'):
data.update({
'total_count_today_active_assets': caches.total_count_today_active_assets,
})
if _all or query_params.get('total_count') or query_params.get('total_count_online_users'):
data.update({
'total_count_online_users': caches.total_count_online_users,
@ -246,6 +363,62 @@ class IndexApi(DatesLoginMetricMixin, APIView):
'total_count_online_sessions': caches.total_count_online_sessions,
})
if _all or query_params.get('total_count') or query_params.get('total_count_today_failed_sessions'):
data.update({
'total_count_today_failed_sessions': caches.total_count_today_failed_sessions,
})
if _all or query_params.get('total_count') or query_params.get('total_count_user_login_logs'):
data.update({
'total_count_user_login_logs': self.user_login_logs_amount,
})
if _all or query_params.get('total_count') or query_params.get('total_count_user_login_success_logs'):
data.update({
'total_count_user_login_success_logs': self.user_login_success_logs_amount,
})
if _all or query_params.get('total_count') or query_params.get('total_count_operate_logs'):
data.update({
'total_count_operate_logs': self.operate_logs_amount,
})
if _all or query_params.get('total_count') or query_params.get('total_count_change_password_logs'):
data.update({
'total_count_change_password_logs': self.change_password_logs_amount,
})
if _all or query_params.get('total_count') or query_params.get('total_count_commands'):
data.update({
'total_count_commands': self.commands_amount,
})
if _all or query_params.get('total_count') or query_params.get('total_count_commands_danger'):
data.update({
'total_count_commands_danger': self.commands_danger_amount,
})
if _all or query_params.get('total_count') or query_params.get('total_count_history_sessions'):
data.update({
'total_count_history_sessions': self.sessions_amount - caches.total_count_online_sessions,
})
if _all or query_params.get('total_count') or query_params.get('total_count_ftp_logs'):
data.update({
'total_count_ftp_logs': self.ftp_logs_amount,
})
if _all or query_params.get('total_count') or query_params.get('total_count_type_to_assets_amount'):
data.update({
'total_count_type_to_assets_amount': self.get_type_to_assets,
})
if _all or query_params.get('session_dates_metrics'):
data.update({
'dates_metrics_date': self.get_dates_metrics_date(),
'dates_metrics_total_count_session': self.get_dates_metrics_total_count_sessions(),
})
if _all or query_params.get('dates_metrics'):
data.update({
'dates_metrics_date': self.get_dates_metrics_date(),
@ -254,44 +427,19 @@ class IndexApi(DatesLoginMetricMixin, APIView):
'dates_metrics_total_count_active_assets': self.get_dates_metrics_total_count_active_assets(),
})
if _all or query_params.get('dates_total_count_users'):
data.update({
'dates_total_count_active_users': self.dates_total_count_active_users,
'dates_total_count_inactive_users': self.dates_total_count_inactive_users,
'dates_total_count_disabled_users': self.dates_total_count_disabled_users,
})
if _all or query_params.get('dates_total_count_assets'):
data.update({
'dates_total_count_active_assets': self.dates_total_count_active_assets,
'dates_total_count_inactive_assets': self.dates_total_count_inactive_assets,
'dates_total_count_disabled_assets': self.dates_total_count_disabled_assets,
})
if _all or query_params.get('dates_total_count'):
data.update({
'dates_total_count_login_users': self.get_dates_total_count_login_users(),
'dates_total_count_login_times': self.get_dates_total_count_login_times(),
})
if _all or query_params.get('dates_login_times_top5_users'):
data.update({
'dates_login_times_top5_users': self.get_dates_login_times_top5_users(),
})
if _all or query_params.get('dates_login_times_top10_assets'):
data.update({
'dates_login_times_top10_assets': self.get_dates_login_times_top10_assets(),
'dates_login_times_top10_assets': self.get_dates_login_times_assets(),
})
if _all or query_params.get('dates_login_times_top10_users'):
data.update({
'dates_login_times_top10_users': self.get_dates_login_times_top10_users(),
'dates_login_times_top10_users': self.get_dates_login_times_users(),
})
if _all or query_params.get('dates_login_record_top10_sessions'):
data.update({
'dates_login_record_top10_sessions': self.get_dates_login_record_top10_sessions()
'dates_login_record_top10_sessions': self.get_dates_login_record_sessions()
})
return JsonResponse(data, status=200)
@ -353,4 +501,3 @@ class PrometheusMetricsApi(HealthApiMixin):
util = ComponentsPrometheusMetricsUtil()
metrics_text = util.get_prometheus_metrics_text()
return HttpResponse(metrics_text, content_type='text/plain; version=0.0.4; charset=utf-8')

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:adfa9c01178d5f6490e616f62d41c71974d42f9e3bd078fcf1b3c7124384df0b
size 117024
oid sha256:4a5338177d87680e0030c77f187a06664136d5dea63c8dffc43fa686091f2da4
size 117102

View File

@ -603,14 +603,10 @@ msgid "All"
msgstr "すべて"
#: assets/models/account.py:46
#, fuzzy
#| msgid "Manually input"
msgid "Manual input"
msgstr "手動入力"
#: assets/models/account.py:47
#, fuzzy
#| msgid "Dynamic code"
msgid "Dynamic user"
msgstr "動的コード"

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:eeaa813f4ea052a1cd85b8ae5addfde6b088fd21a0261f8724d62823835512a2
size 104043
oid sha256:30ae571e06eb7d2f0fee70013a812ea3bdb8e14715e1a1f4eb5e2c92311034f8
size 104086

View File

@ -578,16 +578,12 @@ msgid "All"
msgstr "全部"
#: assets/models/account.py:46
#, fuzzy
#| msgid "Manually input"
msgid "Manual input"
msgstr "手动输入"
#: assets/models/account.py:47
#, fuzzy
#| msgid "Dynamic code"
msgid "Dynamic user"
msgstr "动态"
msgstr "动态用户"
#: assets/models/account.py:55
msgid "Su from"

View File

@ -2,6 +2,8 @@
#
from rest_framework import viewsets
from orgs.mixins.api import OrgBulkModelViewSet
from ..models import AdHoc
from ..serializers import (
AdHocSerializer
@ -12,6 +14,7 @@ __all__ = [
]
class AdHocViewSet(viewsets.ModelViewSet):
queryset = AdHoc.objects.all()
class AdHocViewSet(OrgBulkModelViewSet):
serializer_class = AdHocSerializer
permission_classes = ()
model = AdHoc

View File

@ -5,10 +5,16 @@ from ops.serializers.job import JobSerializer, JobExecutionSerializer
__all__ = ['JobViewSet', 'JobExecutionViewSet']
from ops.tasks import run_ops_job, run_ops_job_executions
from ops.tasks import run_ops_job_execution
from orgs.mixins.api import OrgBulkModelViewSet
def set_task_to_serializer_data(serializer, task):
data = getattr(serializer, "_data", {})
data["task_id"] = task.id
setattr(serializer, "_data", data)
class JobViewSet(OrgBulkModelViewSet):
serializer_class = JobSerializer
model = Job
@ -16,28 +22,32 @@ class JobViewSet(OrgBulkModelViewSet):
def get_queryset(self):
query_set = super().get_queryset()
if self.action != 'retrieve':
return query_set.filter(instant=False)
return query_set
def perform_create(self, serializer):
instance = serializer.save()
if instance.instant:
run_ops_job.delay(instance.id)
execution = instance.create_execution()
task = run_ops_job_execution.delay(execution.id)
set_task_to_serializer_data(serializer, task)
class JobExecutionViewSet(OrgBulkModelViewSet):
serializer_class = JobExecutionSerializer
http_method_names = ('get', 'post', 'head', 'options',)
# filter_fields = ('type',)
permission_classes = ()
model = JobExecution
def perform_create(self, serializer):
instance = serializer.save()
run_ops_job_executions.delay(instance.id)
task = run_ops_job_execution.delay(instance.id)
set_task_to_serializer_data(serializer, task)
def get_queryset(self):
query_set = super().get_queryset()
job_id = self.request.query_params.get('job_id')
if job_id:
self.queryset = query_set.filter(job_id=job_id)
query_set = query_set.filter(job_id=job_id)
return query_set

View File

@ -2,7 +2,8 @@ import os
import zipfile
from django.conf import settings
from rest_framework import viewsets
from orgs.mixins.api import OrgBulkModelViewSet
from ..models import Playbook
from ..serializers.playbook import PlaybookSerializer
@ -15,9 +16,10 @@ def unzip_playbook(src, dist):
fz.extract(file, dist)
class PlaybookViewSet(viewsets.ModelViewSet):
queryset = Playbook.objects.all()
class PlaybookViewSet(OrgBulkModelViewSet):
serializer_class = PlaybookSerializer
permission_classes = ()
model = Playbook
def perform_create(self, serializer):
instance = serializer.save()

View File

@ -0,0 +1,21 @@
# Generated by Django 3.2.14 on 2022-11-28 10:39
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('ops', '0035_jobexecution_org_id'),
]
operations = [
migrations.AlterModelOptions(
name='job',
options={'ordering': ['date_created']},
),
migrations.AlterModelOptions(
name='jobexecution',
options={'ordering': ['-date_created']},
),
]

View File

@ -0,0 +1,26 @@
# Generated by Django 3.2.14 on 2022-11-29 07:29
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ops', '0035_jobexecution_org_id'),
]
operations = [
migrations.AlterModelOptions(
name='job',
options={'ordering': ['date_created']},
),
migrations.AlterModelOptions(
name='jobexecution',
options={'ordering': ['-date_created']},
),
migrations.AddField(
model_name='job',
name='use_parameter_define',
field=models.BooleanField(default=False, verbose_name='Use Parameter Define'),
),
]

View File

@ -0,0 +1,26 @@
# Generated by Django 3.2.14 on 2022-11-29 11:26
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ops', '0036_auto_20221129_1529'),
]
operations = [
migrations.RenameField(
model_name='adhoc',
old_name='owner',
new_name='creator',
),
migrations.AddField(
model_name='adhoc',
name='org_id',
field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'),
),
migrations.DeleteModel(
name='AdHocExecution',
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 3.2.14 on 2022-11-29 11:31
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ops', '0037_auto_20221129_1926'),
]
operations = [
migrations.AddField(
model_name='playbook',
name='org_id',
field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'),
),
]

View File

@ -0,0 +1,25 @@
# Generated by Django 3.2.14 on 2022-11-29 11:32
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('ops', '0038_playbook_org_id'),
]
operations = [
migrations.RemoveField(
model_name='playbook',
name='owner',
),
migrations.AddField(
model_name='playbook',
name='creator',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Creator'),
),
]

View File

@ -0,0 +1,14 @@
# Generated by Django 3.2.14 on 2022-11-29 11:47
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('ops', '0036_auto_20221128_1839'),
('ops', '0039_auto_20221129_1932'),
]
operations = [
]

View File

@ -0,0 +1,23 @@
# Generated by Django 3.2.14 on 2022-11-29 11:52
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ops', '0040_merge_0036_auto_20221128_1839_0039_auto_20221129_1932'),
]
operations = [
migrations.AddField(
model_name='adhoc',
name='comment',
field=models.CharField(blank=True, default='', max_length=1024, null=True, verbose_name='Comment'),
),
migrations.AddField(
model_name='playbook',
name='comment',
field=models.CharField(blank=True, default='', max_length=1024, null=True, verbose_name='Comment'),
),
]

View File

@ -1,21 +1,18 @@
# ~*~ coding: utf-8 ~*~
import os.path
import uuid
from django.db import models
from django.utils.translation import ugettext_lazy as _
from common.db.models import BaseCreateUpdateModel
from common.utils import get_logger
from .base import BaseAnsibleJob, BaseAnsibleExecution
from ..ansible import AdHocRunner
from orgs.mixins.models import JMSOrgBaseModel
__all__ = ["AdHoc", "AdHocExecution"]
__all__ = ["AdHoc"]
logger = get_logger(__file__)
class AdHoc(BaseCreateUpdateModel):
class AdHoc(JMSOrgBaseModel):
class Modules(models.TextChoices):
shell = 'shell', _('Shell')
winshell = 'win_shell', _('Powershell')
@ -26,7 +23,9 @@ class AdHoc(BaseCreateUpdateModel):
module = models.CharField(max_length=128, choices=Modules.choices, default=Modules.shell,
verbose_name=_('Module'))
args = models.CharField(max_length=1024, default='', verbose_name=_('Args'))
owner = models.ForeignKey('users.User', verbose_name=_("Creator"), on_delete=models.SET_NULL, null=True)
creator = models.ForeignKey('users.User', verbose_name=_("Creator"), on_delete=models.SET_NULL, null=True)
comment = models.CharField(max_length=1024, default='', verbose_name=_('Comment'), null=True, blank=True)
@property
def row_count(self):
@ -41,28 +40,3 @@ class AdHoc(BaseCreateUpdateModel):
def __str__(self):
return "{}: {}".format(self.module, self.args)
class AdHocExecution(BaseAnsibleExecution):
"""
AdHoc running history.
"""
task = models.ForeignKey('AdHoc', verbose_name=_("Adhoc"), related_name='executions', on_delete=models.CASCADE)
def get_runner(self):
inv = self.task.inventory
inv.write_to_file(self.inventory_path)
runner = AdHocRunner(
self.inventory_path, self.task.module, module_args=self.task.args,
pattern=self.task.pattern, project_dir=self.private_dir
)
return runner
def task_display(self):
return str(self.task)
class Meta:
db_table = "ops_adhoc_execution"
get_latest_by = 'date_start'
verbose_name = _("AdHoc execution")

View File

@ -45,6 +45,7 @@ class Job(JMSOrgBaseModel, PeriodTaskModelMixin):
runas = models.CharField(max_length=128, default='root', verbose_name=_('Runas'))
runas_policy = models.CharField(max_length=128, choices=RunasPolicies.choices, default=RunasPolicies.skip,
verbose_name=_('Runas policy'))
use_parameter_define = models.BooleanField(default=False, verbose_name=(_('Use Parameter Define')))
parameters_define = models.JSONField(default=dict, verbose_name=_('Parameters define'))
comment = models.CharField(max_length=1024, default='', verbose_name=_('Comment'), null=True, blank=True)
@ -77,9 +78,9 @@ class Job(JMSOrgBaseModel, PeriodTaskModelMixin):
return total_cost / finished_count if finished_count else 0
def get_register_task(self):
from ..tasks import run_ops_job
from ..tasks import run_ops_job_execution
name = "run_ops_job_period_{}".format(str(self.id)[:8])
task = run_ops_job.name
task = run_ops_job_execution.name
args = (str(self.id),)
kwargs = {}
return name, task, args, kwargs
@ -91,6 +92,9 @@ class Job(JMSOrgBaseModel, PeriodTaskModelMixin):
def create_execution(self):
return self.executions.create()
class Meta:
ordering = ['date_created']
class JobExecution(JMSOrgBaseModel):
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
@ -105,6 +109,17 @@ class JobExecution(JMSOrgBaseModel):
date_start = models.DateTimeField(null=True, verbose_name=_('Date start'), db_index=True)
date_finished = models.DateTimeField(null=True, verbose_name=_("Date finished"))
@property
def job_type(self):
return self.job.type
def compile_shell(self):
if self.job.type != 'adhoc':
return
result = "{}{}{} ".format('\'', self.job.args, '\'')
result += "chdir={}".format(self.job.chdir)
return result
def get_runner(self):
inv = self.job.inventory
inv.write_to_file(self.inventory_path)
@ -114,8 +129,9 @@ class JobExecution(JMSOrgBaseModel):
extra_vars = {}
if self.job.type == 'adhoc':
args = self.compile_shell()
runner = AdHocRunner(
self.inventory_path, self.job.module, module_args=self.job.args,
self.inventory_path, self.job.module, module_args=args,
pattern="all", project_dir=self.private_dir, extra_vars=extra_vars,
)
elif self.job.type == 'playbook':
@ -198,3 +214,6 @@ class JobExecution(JMSOrgBaseModel):
except Exception as e:
logging.error(e, exc_info=True)
self.set_error(e)
class Meta:
ordering = ['-date_created']

View File

@ -5,14 +5,15 @@ from django.conf import settings
from django.db import models
from django.utils.translation import gettext_lazy as _
from common.db.models import BaseCreateUpdateModel
from orgs.mixins.models import JMSOrgBaseModel
class Playbook(BaseCreateUpdateModel):
class Playbook(JMSOrgBaseModel):
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
name = models.CharField(max_length=128, verbose_name=_('Name'), null=True)
path = models.FileField(upload_to='playbooks/')
owner = models.ForeignKey('users.User', verbose_name=_("Owner"), on_delete=models.SET_NULL, null=True)
creator = models.ForeignKey('users.User', verbose_name=_("Creator"), on_delete=models.SET_NULL, null=True)
comment = models.CharField(max_length=1024, default='', verbose_name=_('Comment'), null=True, blank=True)
@property
def work_path(self):

View File

@ -1,75 +1,19 @@
# ~*~ coding: utf-8 ~*~
from __future__ import unicode_literals
import datetime
from rest_framework import serializers
from common.drf.fields import ReadableHiddenField
from ..models import AdHoc, AdHocExecution
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from ..models import AdHoc
class AdHocSerializer(serializers.ModelSerializer):
owner = ReadableHiddenField(default=serializers.CurrentUserDefault())
class AdHocSerializer(BulkOrgResourceModelSerializer, serializers.ModelSerializer):
creator = ReadableHiddenField(default=serializers.CurrentUserDefault())
row_count = serializers.IntegerField(read_only=True)
size = serializers.IntegerField(read_only=True)
class Meta:
model = AdHoc
fields = ["id", "name", "module", "row_count", "size", "args", "owner", "date_created", "date_updated"]
class AdHocExecutionSerializer(serializers.ModelSerializer):
stat = serializers.SerializerMethodField()
last_success = serializers.ListField(source='success_hosts')
last_failure = serializers.DictField(source='failed_hosts')
class Meta:
model = AdHocExecution
fields_mini = ['id']
fields_small = fields_mini + [
'timedelta', 'result', 'summary', 'short_id',
'is_finished', 'is_success',
'date_start', 'date_finished',
]
fields_fk = ['task', 'task_display']
fields_custom = ['stat', 'last_success', 'last_failure']
fields = fields_small + fields_fk + fields_custom
@staticmethod
def get_task(obj):
return obj.task.id
@staticmethod
def get_stat(obj):
count_failed_hosts = len(obj.failed_hosts)
count_success_hosts = len(obj.success_hosts)
count_total = count_success_hosts + count_failed_hosts
return {
"total": count_total,
"success": count_success_hosts,
"failed": count_failed_hosts
}
class AdHocExecutionExcludeResultSerializer(AdHocExecutionSerializer):
class Meta:
model = AdHocExecution
fields = [
'id', 'task', 'task_display', 'hosts_amount', 'adhoc', 'date_start', 'stat',
'date_finished', 'timedelta', 'is_finished', 'is_success',
'short_id', 'adhoc_short_id', 'last_success', 'last_failure'
]
class AdHocExecutionNestSerializer(serializers.ModelSerializer):
last_success = serializers.ListField(source='success_hosts')
last_failure = serializers.DictField(source='failed_hosts')
last_run = serializers.CharField(source='short_id')
class Meta:
model = AdHocExecution
fields = (
'last_success', 'last_failure', 'last_run', 'timedelta',
'is_finished', 'is_success'
)
read_only_field = ["id", "row_count", "size", "creator", "date_created", "date_updated"]
fields = read_only_field + ["id", "name", "module", "args", "comment"]

View File

@ -15,6 +15,7 @@ class JobSerializer(BulkOrgResourceModelSerializer, PeriodTaskSerializerMixin):
read_only_fields = ["id", "date_last_run", "date_created", "date_updated", "average_time_cost"]
fields = read_only_fields + [
"name", "instant", "type", "module", "args", "playbook", "assets", "runas_policy", "runas", "owner",
"use_parameter_define",
"parameters_define",
"timeout",
"chdir",
@ -28,7 +29,7 @@ class JobExecutionSerializer(serializers.ModelSerializer):
class Meta:
model = JobExecution
read_only_fields = ["id", "task_id", "timedelta", "time_cost", 'is_finished', 'date_start', 'date_created',
'is_success', 'task_id', 'short_id']
'is_success', 'task_id', 'short_id', 'job_type']
fields = read_only_fields + [
"job", "parameters"
]

View File

@ -4,6 +4,7 @@ from rest_framework import serializers
from common.drf.fields import ReadableHiddenField
from ops.models import Playbook
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
def parse_playbook_name(path):
@ -11,8 +12,8 @@ def parse_playbook_name(path):
return file_name.split(".")[-2]
class PlaybookSerializer(serializers.ModelSerializer):
owner = ReadableHiddenField(default=serializers.CurrentUserDefault())
class PlaybookSerializer(BulkOrgResourceModelSerializer, serializers.ModelSerializer):
creator = ReadableHiddenField(default=serializers.CurrentUserDefault())
def create(self, validated_data):
name = validated_data.get('name')
@ -23,6 +24,7 @@ class PlaybookSerializer(serializers.ModelSerializer):
class Meta:
model = Playbook
fields = [
"id", "name", "path", "date_created", "owner", "date_updated"
read_only_fields = ["id", "date_created", "date_updated"]
fields = read_only_fields + [
"id", "name", "comment", "creator",
]

View File

@ -30,18 +30,11 @@ def run_ops_job(job_id):
job = get_object_or_none(Job, id=job_id)
with tmp_to_org(job.org):
execution = job.create_execution()
try:
execution.start()
except SoftTimeLimitExceeded:
execution.set_error('Run timeout')
logger.error("Run adhoc timeout")
except Exception as e:
execution.set_error(e)
logger.error("Start adhoc execution error: {}".format(e))
run_ops_job_execution(execution)
@shared_task(soft_time_limit=60, queue="ansible", verbose_name=_("Run ansible task execution"))
def run_ops_job_executions(execution_id, **kwargs):
def run_ops_job_execution(execution_id, **kwargs):
execution = get_object_or_none(JobExecution, id=execution_id)
with tmp_to_org(execution.org):
try:

View File

@ -1,10 +1,11 @@
from django.db.transaction import on_commit
from orgs.models import Organization
from orgs.tasks import refresh_org_cache_task
from orgs.utils import current_org, tmp_to_org
from common.cache import Cache, IntegerField
from common.utils import get_logger
from common.utils.timezone import local_zero_hour, local_monday
from users.models import UserGroup, User
from assets.models import Node, Domain, Asset, Account
from terminal.models import Session
@ -35,30 +36,34 @@ class OrgRelatedCache(Cache):
"""
在事务提交之后再发送信号防止因事务的隔离性导致未获得最新的数据
"""
def func():
logger.debug(f'CACHE: Send refresh task {self}.{fields}')
refresh_org_cache_task.delay(self, *fields)
on_commit(func)
def expire(self, *fields):
def func():
super(OrgRelatedCache, self).expire(*fields)
on_commit(func)
class OrgResourceStatisticsCache(OrgRelatedCache):
users_amount = IntegerField()
groups_amount = IntegerField(queryset=UserGroup.objects)
assets_amount = IntegerField()
new_users_amount_this_week = IntegerField()
new_assets_amount_this_week = IntegerField()
nodes_amount = IntegerField(queryset=Node.objects)
accounts_amount = IntegerField(queryset=Account.objects)
domains_amount = IntegerField(queryset=Domain.objects)
# gateways_amount = IntegerField(queryset=Gateway.objects)
groups_amount = IntegerField(queryset=UserGroup.objects)
accounts_amount = IntegerField(queryset=Account.objects)
asset_perms_amount = IntegerField(queryset=AssetPermission.objects)
total_count_online_users = IntegerField()
total_count_online_sessions = IntegerField()
total_count_today_active_assets = IntegerField()
total_count_today_failed_sessions = IntegerField()
def __init__(self, org):
super().__init__()
@ -70,18 +75,49 @@ class OrgResourceStatisticsCache(OrgRelatedCache):
def get_current_org(self):
return self.org
def get_users(self):
return User.get_org_users(self.org)
@staticmethod
def get_assets():
return Asset.objects.all()
def compute_users_amount(self):
amount = User.get_org_users(self.org).count()
return amount
users = self.get_users()
return users.count()
def compute_new_users_amount_this_week(self):
monday_time = local_monday()
users = self.get_users().filter(date_joined__gte=monday_time)
return users.count()
def compute_assets_amount(self):
if self.org.is_root():
return Asset.objects.all().count()
node = Node.org_root()
return node.assets_amount
assets = self.get_assets()
return assets.count()
def compute_total_count_online_users(self):
return Session.objects.filter(is_finished=False).values_list('user_id').distinct().count()
def compute_new_assets_amount_this_week(self):
monday_time = local_monday()
assets = self.get_assets().filter(date_created__gte=monday_time)
return assets.count()
def compute_total_count_online_sessions(self):
@staticmethod
def compute_total_count_online_users():
return Session.objects.filter(
is_finished=False
).values_list('user_id').distinct().count()
@staticmethod
def compute_total_count_online_sessions():
return Session.objects.filter(is_finished=False).count()
@staticmethod
def compute_total_count_today_active_assets():
t = local_zero_hour()
return Session.objects.filter(
date_start__gte=t, is_success=False
).values('asset_id').distinct().count()
@staticmethod
def compute_total_count_today_failed_sessions():
t = local_zero_hour()
return Session.objects.filter(date_start__gte=t, is_success=False).count()

View File

@ -2,8 +2,9 @@ from django.db.models.signals import post_save, pre_delete, pre_save, post_delet
from django.dispatch import receiver
from orgs.models import Organization
from assets.models import Node
from assets.models import Node, Account
from perms.models import AssetPermission
from audits.models import UserLoginLog
from users.models import UserGroup, User
from users.signals import pre_user_leave_org
from terminal.models import Session
@ -74,12 +75,14 @@ def on_user_delete_refresh_cache(sender, instance, **kwargs):
class OrgResourceStatisticsRefreshUtil:
model_cache_field_mapper = {
AssetPermission: ['asset_perms_amount'],
Domain: ['domains_amount'],
Node: ['nodes_amount'],
Asset: ['assets_amount'],
Domain: ['domains_amount'],
UserGroup: ['groups_amount'],
RoleBinding: ['users_amount']
Account: ['accounts_amount'],
RoleBinding: ['users_amount', 'new_users_amount_this_week'],
Asset: ['assets_amount', 'new_assets_amount_this_week'],
AssetPermission: ['asset_perms_amount'],
}
@classmethod
@ -88,7 +91,7 @@ class OrgResourceStatisticsRefreshUtil:
if not cache_field_name:
return
OrgResourceStatisticsCache(Organization.root()).expire(*cache_field_name)
if instance.org:
if getattr(instance, 'org', None):
OrgResourceStatisticsCache(instance.org).expire(*cache_field_name)

View File

@ -1,5 +0,0 @@
from rest_framework.viewsets import ModelViewSet
class PermTokenViewSet(ModelViewSet):
pass

View File

@ -3,58 +3,58 @@ from rest_framework.generics import ListAPIView
from common.utils import get_logger
from .mixin import (
UserAllGrantedAssetsQuerysetMixin, UserDirectGrantedAssetsQuerysetMixin, UserFavoriteGrantedAssetsMixin,
UserGrantedNodeAssetsMixin, AssetsSerializerFormatMixin, AssetsTreeFormatMixin,
AssetsTreeFormatMixin,
UserGrantedNodeAssetsMixin,
AssetSerializerFormatMixin,
UserFavoriteGrantedAssetsMixin,
UserAllGrantedAssetsQuerysetMixin,
UserDirectGrantedAssetsQuerysetMixin,
)
from ..mixin import AssetRoleAdminMixin, AssetRoleUserMixin
from ..mixin import SelfOrPKUserMixin, RebuildTreeMixin
__all__ = [
'UserDirectGrantedAssetsApi', 'MyDirectGrantedAssetsApi',
'UserDirectGrantedAssetsApi',
'UserFavoriteGrantedAssetsApi',
'MyFavoriteGrantedAssetsApi', 'UserDirectGrantedAssetsAsTreeApi',
'MyUngroupAssetsAsTreeApi',
'UserAllGrantedAssetsApi', 'MyAllGrantedAssetsApi', 'MyAllAssetsAsTreeApi',
'UserDirectGrantedAssetsAsTreeApi',
'UserUngroupAssetsAsTreeApi',
'UserAllGrantedAssetsApi',
'UserGrantedNodeAssetsApi',
'MyGrantedNodeAssetsApi',
]
logger = get_logger(__name__)
class UserDirectGrantedAssetsApi(
AssetRoleAdminMixin, UserDirectGrantedAssetsQuerysetMixin,
AssetsSerializerFormatMixin, ListAPIView
SelfOrPKUserMixin,
UserDirectGrantedAssetsQuerysetMixin,
AssetSerializerFormatMixin,
ListAPIView
):
""" 直接授权给用户的资产 """
pass
class MyDirectGrantedAssetsApi(AssetRoleUserMixin, UserDirectGrantedAssetsApi):
""" 直接授权给我的资产 """
pass
class UserFavoriteGrantedAssetsApi(
AssetRoleAdminMixin, UserFavoriteGrantedAssetsMixin,
AssetsSerializerFormatMixin, ListAPIView
SelfOrPKUserMixin,
UserFavoriteGrantedAssetsMixin,
AssetSerializerFormatMixin,
ListAPIView
):
""" 用户收藏的授权资产 """
pass
class MyFavoriteGrantedAssetsApi(AssetRoleUserMixin, UserFavoriteGrantedAssetsApi):
""" 我收藏的授权资产 """
pass
class UserDirectGrantedAssetsAsTreeApi(AssetsTreeFormatMixin, UserDirectGrantedAssetsApi):
class UserDirectGrantedAssetsAsTreeApi(
RebuildTreeMixin,
AssetsTreeFormatMixin,
UserDirectGrantedAssetsApi
):
""" 用户直接授权的资产作为树 """
pass
class MyUngroupAssetsAsTreeApi(AssetRoleUserMixin, UserDirectGrantedAssetsAsTreeApi):
""" 我的未分组节点下的资产作为树 """
class UserUngroupAssetsAsTreeApi(UserDirectGrantedAssetsAsTreeApi):
""" 用户未分组节点下的资产作为树 """
def get_queryset(self):
queryset = super().get_queryset()
if not settings.PERM_SINGLE_ASSET_TO_UNGROUP_NODE:
@ -63,31 +63,20 @@ class MyUngroupAssetsAsTreeApi(AssetRoleUserMixin, UserDirectGrantedAssetsAsTree
class UserAllGrantedAssetsApi(
AssetRoleAdminMixin, UserAllGrantedAssetsQuerysetMixin,
AssetsSerializerFormatMixin, ListAPIView
SelfOrPKUserMixin,
UserAllGrantedAssetsQuerysetMixin,
AssetSerializerFormatMixin,
ListAPIView
):
""" 授权给用户的所有资产 """
pass
class MyAllGrantedAssetsApi(AssetRoleUserMixin, UserAllGrantedAssetsApi):
""" 授权给我的所有资产 """
pass
class MyAllAssetsAsTreeApi(AssetsTreeFormatMixin, MyAllGrantedAssetsApi):
""" 授权给我的所有资产作为树 """
pass
class UserGrantedNodeAssetsApi(
AssetRoleAdminMixin, UserGrantedNodeAssetsMixin,
AssetsSerializerFormatMixin, ListAPIView
SelfOrPKUserMixin,
UserGrantedNodeAssetsMixin,
AssetSerializerFormatMixin,
ListAPIView
):
""" 授权给用户的节点资产 """
pass
class MyGrantedNodeAssetsApi(AssetRoleUserMixin, UserGrantedNodeAssetsApi):
""" 授权给我的节点资产 """
pass

View File

@ -1,19 +1,18 @@
from rest_framework.response import Response
from rest_framework.request import Request
from rest_framework.response import Response
from common.utils import get_logger
from users.models import User
from assets.api.asset.asset import AssetFilterSet
from assets.api.mixin import SerializeToTreeNodeMixin
from assets.models import Asset, Node
from perms.pagination import NodeGrantedAssetPagination, AllGrantedAssetPagination
from perms import serializers
from perms.pagination import NodeGrantedAssetPagination, AllGrantedAssetPagination
from perms.utils.user_permission import UserGrantedAssetsQueryUtils
logger = get_logger(__name__)
# 获取数据的 ------------------------------------------------------------
class UserDirectGrantedAssetsQuerysetMixin:
only_fields = serializers.AssetGrantedSerializer.Meta.only_fields
user: User
@ -32,6 +31,7 @@ class UserAllGrantedAssetsQuerysetMixin:
only_fields = serializers.AssetGrantedSerializer.Meta.only_fields
pagination_class = AllGrantedAssetPagination
ordering_fields = ("name", "address")
filterset_class = AssetFilterSet
ordering = ('name',)
user: User
@ -71,21 +71,19 @@ class UserGrantedNodeAssetsMixin:
return Asset.objects.none()
node_id = self.kwargs.get("node_id")
node, assets = UserGrantedAssetsQueryUtils(self.user).get_node_all_assets(
node_id
)
node, assets = UserGrantedAssetsQueryUtils(self.user).get_node_all_assets(node_id)
assets = assets.prefetch_related('platform').only(*self.only_fields)
self.pagination_node = node
return assets
# 控制格式的 ----------------------------------------------------
class AssetsSerializerFormatMixin:
class AssetSerializerFormatMixin:
serializer_class = serializers.AssetGrantedSerializer
filterset_fields = ['name', 'address', 'id', 'comment']
search_fields = ['name', 'address', 'comment']
filterset_class = AssetFilterSet
ordering_fields = ("name", "address")
ordering = ('name',)
class AssetsTreeFormatMixin(SerializeToTreeNodeMixin):

View File

@ -7,7 +7,6 @@ from django.utils.translation import ugettext_lazy as _
from common.http import is_true
from common.utils import is_uuid
from common.exceptions import JMSObjectDoesNotExist
from common.mixins.api import RoleAdminMixin, RoleUserMixin
from perms.utils.user_permission import UserGrantedTreeRefreshController
from rbac.permissions import RBACPermission
from users.models import User
@ -23,24 +22,6 @@ class RebuildTreeMixin:
return super().get(request, *args, **kwargs)
class AssetRoleAdminMixin(RebuildTreeMixin, RoleAdminMixin):
rbac_perms = (
('list', 'perms.view_userassets'),
('retrieve', 'perms.view_userassets'),
('get_tree', 'perms.view_userassets'),
('GET', 'perms.view_userassets'),
)
class AssetRoleUserMixin(RebuildTreeMixin, RoleUserMixin):
rbac_perms = (
('list', 'perms.view_myassets'),
('retrieve', 'perms.view_myassets'),
('get_tree', 'perms.view_myassets'),
('GET', 'perms.view_myassets'),
)
class SelfOrPKUserMixin:
kwargs: dict
request: Request
@ -59,6 +40,7 @@ class SelfOrPKUserMixin:
('retrieve', 'perms.view_myassets'),
('get_tree', 'perms.view_myassets'),
('GET', 'perms.view_myassets'),
('OPTIONS', 'perms.view_myassets'),
)
@property
@ -68,6 +50,7 @@ class SelfOrPKUserMixin:
('retrieve', 'perms.view_userassets'),
('get_tree', 'perms.view_userassets'),
('GET', 'perms.view_userassets'),
('OPTIONS', 'perms.view_userassets'),
)
@property
@ -76,6 +59,8 @@ class SelfOrPKUserMixin:
user = self.request.user
elif is_uuid(self.kwargs.get('user')):
user = get_object_or_404(User, pk=self.kwargs.get('user'))
elif hasattr(self, 'swagger_fake_view'):
user = self.request.user
else:
raise JMSObjectDoesNotExist(object_name=_('User'))
return user

View File

@ -1,31 +1,25 @@
# -*- coding: utf-8 -*-
#
import abc
from rest_framework.generics import (
ListAPIView
)
from rest_framework.response import Response
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.generics import ListAPIView
from assets.api.mixin import SerializeToTreeNodeMixin
from common.utils import get_logger
from .mixin import AssetRoleAdminMixin, AssetRoleUserMixin
from perms.hands import User
from assets.api.mixin import SerializeToTreeNodeMixin
from perms import serializers
from perms.hands import User
from perms.utils.user_permission import UserGrantedNodesQueryUtils
from .mixin import SelfOrPKUserMixin, RebuildTreeMixin
logger = get_logger(__name__)
__all__ = [
'UserGrantedNodesApi',
'MyGrantedNodesApi',
'MyGrantedNodesAsTreeApi',
'UserGrantedNodeChildrenForAdminApi',
'MyGrantedNodeChildrenApi',
'UserGrantedNodeChildrenAsTreeForAdminApi',
'MyGrantedNodeChildrenAsTreeApi',
'UserGrantedNodesAsTreeApi',
'UserGrantedNodeChildrenApi',
'UserGrantedNodeChildrenAsTreeApi',
'BaseGrantedNodeAsTreeApi',
'UserGrantedNodesMixin',
]
@ -98,35 +92,42 @@ class UserGrantedNodesMixin:
return nodes
# ------------------------------------------
# 最终的 api
class UserGrantedNodeChildrenForAdminApi(AssetRoleAdminMixin, UserGrantedNodeChildrenMixin, BaseNodeChildrenApi):
# API
class UserGrantedNodeChildrenApi(
SelfOrPKUserMixin,
UserGrantedNodeChildrenMixin,
BaseNodeChildrenApi
):
""" 用户授权的节点下的子节点"""
pass
class MyGrantedNodeChildrenApi(AssetRoleUserMixin, UserGrantedNodeChildrenMixin, BaseNodeChildrenApi):
class UserGrantedNodeChildrenAsTreeApi(
SelfOrPKUserMixin,
RebuildTreeMixin,
UserGrantedNodeChildrenMixin,
BaseNodeChildrenAsTreeApi
):
""" 用户授权的节点下的子节点树"""
pass
class UserGrantedNodeChildrenAsTreeForAdminApi(AssetRoleAdminMixin, UserGrantedNodeChildrenMixin, BaseNodeChildrenAsTreeApi):
class UserGrantedNodesApi(
SelfOrPKUserMixin,
UserGrantedNodesMixin,
BaseGrantedNodeApi
):
""" 用户授权的节点 """
pass
class MyGrantedNodeChildrenAsTreeApi(AssetRoleUserMixin, UserGrantedNodeChildrenMixin, BaseNodeChildrenAsTreeApi):
def get_permissions(self):
permissions = super().get_permissions()
return permissions
class UserGrantedNodesApi(AssetRoleAdminMixin, UserGrantedNodesMixin, BaseGrantedNodeApi):
class UserGrantedNodesAsTreeApi(
SelfOrPKUserMixin,
RebuildTreeMixin,
UserGrantedNodesMixin,
BaseGrantedNodeAsTreeApi
):
""" 用户授权的节点树 """
pass
class MyGrantedNodesApi(AssetRoleUserMixin, UserGrantedNodesApi):
pass
class MyGrantedNodesAsTreeApi(AssetRoleUserMixin, UserGrantedNodesMixin, BaseGrantedNodeAsTreeApi):
pass
# ------------------------------------------

View File

@ -1,24 +1,25 @@
# -*- coding: utf-8 -*-
#
from django.conf import settings
from django.db.models import F, Value, CharField
from rest_framework.generics import ListAPIView
from rest_framework.request import Request
from rest_framework.response import Response
from django.db.models import F, Value, CharField
from django.conf import settings
from common.utils.common import timeit
from orgs.utils import tmp_to_root_org
from common.permissions import IsValidUser
from common.utils import get_logger, get_object_or_none
from .mixin import AssetRoleUserMixin, AssetRoleAdminMixin
from common.utils.common import timeit
from common.permissions import IsValidUser
from assets.models import Asset
from assets.api import SerializeToTreeNodeMixin
from perms.hands import Node
from perms.models import AssetPermission, PermNode
from perms.utils.user_permission import (
UserGrantedTreeBuildUtils, get_user_all_asset_perm_ids,
UserGrantedNodesQueryUtils, UserGrantedAssetsQueryUtils,
)
from perms.models import AssetPermission, PermNode
from assets.models import Asset
from assets.api import SerializeToTreeNodeMixin
from perms.hands import Node
from .mixin import SelfOrPKUserMixin, RebuildTreeMixin
logger = get_logger(__name__)
@ -148,9 +149,10 @@ class GrantedNodeChildrenWithAssetsAsTreeApiMixin(SerializeToTreeNodeMixin,
return Response(data=all_tree_nodes)
class UserGrantedNodeChildrenWithAssetsAsTreeApi(AssetRoleAdminMixin, GrantedNodeChildrenWithAssetsAsTreeApiMixin):
pass
class MyGrantedNodeChildrenWithAssetsAsTreeApi(AssetRoleUserMixin, GrantedNodeChildrenWithAssetsAsTreeApiMixin):
class UserGrantedNodeChildrenWithAssetsAsTreeApi(
SelfOrPKUserMixin,
RebuildTreeMixin,
GrantedNodeChildrenWithAssetsAsTreeApiMixin
):
""" 用户授权的节点的子节点与资产树 """
pass

View File

@ -125,7 +125,7 @@ class AssetPermission(OrgModelMixin):
"""
asset_ids = self.get_all_assets(flat=True)
q = Q(asset_id__in=asset_ids)
if Account.AliasAccount.ALL in self.accounts:
if Account.AliasAccount.ALL not in self.accounts:
q &= Q(username__in=self.accounts)
accounts = Account.objects.filter(q).order_by('asset__name', 'name', 'username')
if not flat:

View File

@ -5,14 +5,14 @@ from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
from assets.const import Category, AllTypes
from assets.serializers.asset.common import AssetProtocolsSerializer
from assets.models import Node, Asset, Platform, Account
from assets.serializers.asset.common import AssetProtocolsSerializer
from common.drf.fields import ObjectRelatedField, LabeledChoiceField
from perms.serializers.permission import ActionChoicesField
__all__ = [
'NodeGrantedSerializer', 'AssetGrantedSerializer',
'ActionsSerializer', 'AccountsPermedSerializer'
'AccountsPermedSerializer'
]
@ -30,7 +30,7 @@ class AssetGrantedSerializer(serializers.ModelSerializer):
'domain', 'platform',
"comment", "org_id", "is_active",
]
fields = only_fields + ['protocols', 'category', 'type'] + ['org_name']
fields = only_fields + ['protocols', 'category', 'type', 'specific'] + ['org_name']
read_only_fields = fields
@ -43,14 +43,11 @@ class NodeGrantedSerializer(serializers.ModelSerializer):
read_only_fields = fields
class ActionsSerializer(serializers.Serializer):
actions = ActionChoicesField(read_only=True)
class AccountsPermedSerializer(serializers.ModelSerializer):
actions = ActionChoicesField(read_only=True)
class Meta:
model = Account
fields = ['id', 'name', 'username', 'secret_type', 'has_secret', 'actions']
fields = ['id', 'name', 'has_username', 'username',
'has_secret', 'secret_type', 'actions']
read_only_fields = fields

View File

@ -5,5 +5,4 @@ from .user_permission import user_permission_urlpatterns
app_name = 'perms'
urlpatterns = asset_permission_urlpatterns \
+ user_permission_urlpatterns
urlpatterns = asset_permission_urlpatterns + user_permission_urlpatterns

View File

@ -3,68 +3,54 @@ from django.urls import path, include
from .. import api
user_permission_urlpatterns = [
# 以 serializer 格式返回
path('<uuid:pk>/assets/', api.UserAllGrantedAssetsApi.as_view(), name='user-assets'),
path('assets/', api.MyAllGrantedAssetsApi.as_view(), name='my-assets'),
# Tree Node 的数据格式返回
path('<uuid:pk>/assets/tree/', api.UserDirectGrantedAssetsAsTreeApi.as_view(), name='user-assets-as-tree'),
path('assets/tree/', api.MyAllAssetsAsTreeApi.as_view(), name='my-assets-as-tree'),
path('ungroup/assets/tree/', api.MyUngroupAssetsAsTreeApi.as_view(), name='my-ungroup-assets-as-tree'),
# <str:user> such as: my | self | user.id
# 获取用户所有`直接授权的节点`与`直接授权资产`关联的节点
# 以 serializer 格式返回
path('<uuid:pk>/nodes/', api.UserGrantedNodesApi.as_view(), name='user-nodes'),
path('nodes/', api.MyGrantedNodesApi.as_view(), name='my-nodes'),
# 以 Tree Node 的数据格式返回
path('<uuid:pk>/nodes/tree/', api.MyGrantedNodesAsTreeApi.as_view(), name='user-nodes-as-tree'),
path('nodes/tree/', api.MyGrantedNodesAsTreeApi.as_view(), name='my-nodes-as-tree'),
# assets
path('<str:user>/assets/', api.UserAllGrantedAssetsApi.as_view(),
name='user-assets'),
path('<str:user>/assets/tree/', api.UserDirectGrantedAssetsAsTreeApi.as_view(),
name='user-assets-as-tree'),
path('<str:user>/ungroup/assets/tree/', api.UserUngroupAssetsAsTreeApi.as_view(),
name='user-ungroup-assets-as-tree'),
# 一层一层的获取用户授权的节点,
# 以 Serializer 的数据格式返回
path('<uuid:pk>/nodes/children/', api.UserGrantedNodeChildrenForAdminApi.as_view(), name='user-nodes-children'),
path('nodes/children/', api.MyGrantedNodeChildrenApi.as_view(), name='my-nodes-children'),
# 以 Tree Node 的数据格式返回
path('<uuid:pk>/nodes/children/tree/', api.UserGrantedNodeChildrenAsTreeForAdminApi.as_view(),
# nodes
path('<str:user>/nodes/', api.UserGrantedNodesApi.as_view(),
name='user-nodes'),
path('<str:user>/nodes/tree/', api.UserGrantedNodesAsTreeApi.as_view(),
name='user-nodes-as-tree'),
path('<str:user>/nodes/children/', api.UserGrantedNodeChildrenApi.as_view(),
name='user-nodes-children'),
path('<str:user>/nodes/children/tree/', api.UserGrantedNodeChildrenAsTreeApi.as_view(),
name='user-nodes-children-as-tree'),
# 部分调用位置
# - 普通用户 -> 我的资产 -> 展开节点 时调用
path('nodes/children/tree/', api.MyGrantedNodeChildrenAsTreeApi.as_view(), name='my-nodes-children-as-tree'),
# 此接口会返回整棵树
# 普通用户 -> 命令执行 -> 左侧树
# node-assets
path('<str:user>/nodes/<uuid:node_id>/assets/', api.UserGrantedNodeAssetsApi.as_view(),
name='user-node-assets'),
path('<str:user>/nodes/ungrouped/assets/', api.UserDirectGrantedAssetsApi.as_view(),
name='user-ungrouped-assets'),
path('<str:user>/nodes/favorite/assets/', api.UserFavoriteGrantedAssetsApi.as_view(),
name='user-ungrouped-assets'),
path('<str:user>/nodes/children-with-assets/tree/',
api.UserGrantedNodeChildrenWithAssetsAsTreeApi.as_view(),
name='user-nodes-children-with-assets-as-tree'),
path('nodes-with-assets/tree/', api.MyGrantedNodesWithAssetsAsTreeApi.as_view(),
name='my-nodes-with-assets-as-tree'),
# 主要用于 luna 页面,带资产的节点树
path('<uuid:pk>/nodes/children-with-assets/tree/', api.UserGrantedNodeChildrenWithAssetsAsTreeApi.as_view(),
name='user-nodes-children-with-assets-as-tree'),
path('nodes/children-with-assets/tree/', api.MyGrantedNodeChildrenWithAssetsAsTreeApi.as_view(),
name='my-nodes-children-with-assets-as-tree'),
# 查询授权树上某个节点的所有资产
path('<uuid:pk>/nodes/<uuid:node_id>/assets/', api.UserGrantedNodeAssetsApi.as_view(), name='user-node-assets'),
path('nodes/<uuid:node_id>/assets/', api.MyGrantedNodeAssetsApi.as_view(), name='my-node-assets'),
# 未分组的资产
path('<uuid:pk>/nodes/ungrouped/assets/', api.UserDirectGrantedAssetsApi.as_view(), name='user-ungrouped-assets'),
path('nodes/ungrouped/assets/', api.MyDirectGrantedAssetsApi.as_view(), name='my-ungrouped-assets'),
# 收藏的资产
path('<uuid:pk>/nodes/favorite/assets/', api.UserFavoriteGrantedAssetsApi.as_view(), name='user-ungrouped-assets'),
path('nodes/favorite/assets/', api.MyFavoriteGrantedAssetsApi.as_view(),
name='my-ungrouped-assets'),
# 获取授权给用户某个资产的所有账号
# user params: ['my', 'self'] or user.id
# accounts
path('<str:user>/assets/<uuid:asset_id>/accounts/', api.UserPermedAssetAccountsApi.as_view(),
name='user-permed-asset-accounts'),
]
user_group_permission_urlpatterns = [
# 查询某个用户组授权的资产和资产组
path('<uuid:pk>/assets/', api.UserGroupGrantedAssetsApi.as_view(), name='user-group-assets'),
path('<uuid:pk>/nodes/', api.UserGroupGrantedNodesApi.as_view(), name='user-group-nodes'),
path('<uuid:pk>/nodes/children/', api.UserGroupGrantedNodesApi.as_view(), name='user-group-nodes-children'),
path('<uuid:pk>/assets/', api.UserGroupGrantedAssetsApi.as_view(),
name='user-group-assets'),
path('<uuid:pk>/nodes/', api.UserGroupGrantedNodesApi.as_view(),
name='user-group-nodes'),
path('<uuid:pk>/nodes/children/', api.UserGroupGrantedNodesApi.as_view(),
name='user-group-nodes-children'),
path('<uuid:pk>/nodes/children/tree/', api.UserGroupGrantedNodeChildrenAsTreeApi.as_view(),
name='user-group-nodes-children-as-tree'),
path('<uuid:pk>/nodes/<uuid:node_id>/assets/', api.UserGroupGrantedNodeAssetsApi.as_view(),

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@ -12,6 +12,7 @@ from common.drf.api import JMSBulkModelViewSet
from common.exceptions import JMSException
from common.permissions import IsValidUser
from common.permissions import WithBootstrapToken
from common.utils import get_request_os
from terminal import serializers
from terminal.const import TerminalType
from terminal.models import Terminal
@ -77,13 +78,7 @@ class ConnectMethodListApi(generics.ListAPIView):
permission_classes = [IsValidUser]
def get_queryset(self):
user_agent = self.request.META['HTTP_USER_AGENT'].lower()
if 'macintosh' in user_agent:
os = 'macos'
elif 'windows' in user_agent:
os = 'windows'
else:
os = 'linux'
os = get_request_os(self.request)
return TerminalType.get_protocols_connect_methods(os)
def list(self, request, *args, **kwargs):

View File

@ -44,9 +44,30 @@ class ComponentLoad(TextChoices):
return set(dict(cls.choices).keys())
class HttpMethod(TextChoices):
class WebMethod(TextChoices):
web_gui = 'web_gui', 'Web GUI'
web_cli = 'web_cli', 'Web CLI'
web_sftp = 'web_sftp', 'Web SFTP'
@classmethod
def get_methods(cls):
return {
Protocol.ssh: [cls.web_cli, cls.web_sftp],
Protocol.telnet: [cls.web_cli],
Protocol.rdp: [cls.web_gui],
Protocol.vnc: [cls.web_gui],
Protocol.mysql: [cls.web_cli, cls.web_gui],
Protocol.mariadb: [cls.web_cli, cls.web_gui],
Protocol.oracle: [cls.web_cli, cls.web_gui],
Protocol.postgresql: [cls.web_cli, cls.web_gui],
Protocol.sqlserver: [cls.web_cli, cls.web_gui],
Protocol.redis: [cls.web_cli],
Protocol.mongodb: [cls.web_cli],
Protocol.k8s: [cls.web_gui],
Protocol.http: []
}
class NativeClient(TextChoices):
@ -56,17 +77,19 @@ class NativeClient(TextChoices):
xshell = 'xshell', 'Xshell'
# Magnus
mysql = 'mysql', 'mysql'
psql = 'psql', 'psql'
sqlplus = 'sqlplus', 'sqlplus'
redis = 'redis-cli', 'redis-cli'
mongodb = 'mongo', 'mongo'
mysql = 'db_client_mysql', _('DB Client')
psql = 'db_client_psql', _('DB Client')
sqlplus = 'db_client_sqlplus', _('DB Client')
redis = 'db_client_redis', _('DB Client')
mongodb = 'db_client_mongodb', _('DB Client')
# Razor
mstsc = 'mstsc', 'Remote Desktop'
@classmethod
def get_native_clients(cls):
# native client 关注的是 endpoint 的 protocol,
# 比如 telnet mysql, koko 都支持,到那时暴露的是 ssh 协议
clients = {
Protocol.ssh: {
'default': [cls.ssh],
@ -81,6 +104,15 @@ class NativeClient(TextChoices):
}
return clients
@classmethod
def get_target_protocol(cls, name, os):
for protocol, clients in cls.get_native_clients().items():
if isinstance(clients, dict):
clients = clients.get(os) or clients.get('default')
if name in clients:
return protocol
return None
@classmethod
def get_methods(cls, os='windows'):
clients_map = cls.get_native_clients()
@ -98,19 +130,19 @@ class NativeClient(TextChoices):
return methods
@classmethod
def get_launch_command(cls, name, os='windows'):
def get_launch_command(cls, name, token, endpoint, os='windows'):
username = f'JMS-{token.id}'
commands = {
'ssh': 'ssh {username}@{hostname} -p {port}',
'putty': 'putty -ssh {username}@{hostname} -P {port}',
'xshell': '-url ssh://root:passwd@192.168.10.100',
'mysql': 'mysql -h {hostname} -P {port} -u {username} -p',
'psql': {
'default': 'psql -h {hostname} -p {port} -U {username} -W',
'windows': 'psql /h {hostname} /p {port} /U {username} -W',
},
'sqlplus': 'sqlplus {username}/{password}@{hostname}:{port}',
'redis': 'redis-cli -h {hostname} -p {port} -a {password}',
'mstsc': 'mstsc /v:{hostname}:{port}',
cls.ssh: f'ssh {username}@{endpoint.host} -p {endpoint.ssh_port}',
cls.putty: f'putty.exe -ssh {username}@{endpoint.host} -P {endpoint.ssh_port}',
cls.xshell: f'xshell.exe -url ssh://{username}:{token.value}@{endpoint.host}:{endpoint.ssh_port}',
# cls.mysql: 'mysql -h {hostname} -P {port} -u {username} -p',
# cls.psql: {
# 'default': 'psql -h {hostname} -p {port} -U {username} -W',
# 'windows': 'psql /h {hostname} /p {port} /U {username} -W',
# },
# cls.sqlplus: 'sqlplus {username}/{password}@{hostname}:{port}',
# cls.redis: 'redis-cli -h {hostname} -p {port} -a {password}',
}
command = commands.get(name)
if isinstance(command, dict):
@ -154,7 +186,7 @@ class TerminalType(TextChoices):
def protocols(cls):
protocols = {
cls.koko: {
'web_method': HttpMethod.web_cli,
'web_methods': [WebMethod.web_cli, WebMethod.web_sftp],
'listen': [Protocol.ssh, Protocol.http],
'support': [
Protocol.ssh, Protocol.telnet,
@ -166,7 +198,7 @@ class TerminalType(TextChoices):
'match': 'm2m'
},
cls.omnidb: {
'web_method': HttpMethod.web_gui,
'web_methods': [WebMethod.web_gui],
'listen': [Protocol.http],
'support': [
Protocol.mysql, Protocol.postgresql, Protocol.oracle,
@ -175,7 +207,7 @@ class TerminalType(TextChoices):
'match': 'm2m'
},
cls.lion: {
'web_method': HttpMethod.web_gui,
'web_methods': [WebMethod.web_gui],
'listen': [Protocol.http],
'support': [Protocol.rdp, Protocol.vnc],
'match': 'm2m'
@ -183,8 +215,8 @@ class TerminalType(TextChoices):
cls.magnus: {
'listen': [],
'support': [
Protocol.mysql, Protocol.postgresql, Protocol.oracle,
Protocol.mariadb
Protocol.mysql, Protocol.postgresql,
Protocol.oracle, Protocol.mariadb
],
'match': 'map'
},
@ -196,9 +228,19 @@ class TerminalType(TextChoices):
}
return protocols
@classmethod
def get_connect_method(cls, name, protocol, os):
methods = cls.get_protocols_connect_methods(os)
protocol_methods = methods.get(protocol, [])
for method in protocol_methods:
if method['value'] == name:
return method
return None
@classmethod
def get_protocols_connect_methods(cls, os):
methods = defaultdict(list)
web_methods = WebMethod.get_methods()
native_methods = NativeClient.get_methods(os)
applet_methods = AppletMethod.get_methods()
@ -212,24 +254,35 @@ class TerminalType(TextChoices):
listen = component_protocol['listen']
for listen_protocol in listen:
if listen_protocol == Protocol.http:
web_protocol = component_protocol['web_method']
methods[protocol.value].append({
'value': web_protocol.value,
'label': web_protocol.label,
'type': 'web',
'component': component.value,
})
# Native method
methods[protocol.value].extend([
{'component': component.value, 'type': 'native', **method}
{
'component': component.value,
'type': 'native',
'endpoint_protocol': listen_protocol,
**method
}
for method in native_methods[listen_protocol]
])
protocol_web_methods = set(web_methods.get(protocol, [])) \
& set(component_protocol.get('web_methods', []))
print("protocol_web_methods", protocol, protocol_web_methods)
methods[protocol.value].extend([
{
'component': component.value,
'type': 'web',
'endpoint_protocol': 'http',
'value': method.value,
'label': method.label,
}
for method in protocol_web_methods
])
for protocol, applet_methods in applet_methods.items():
for method in applet_methods:
method['type'] = 'applet'
method['listen'] = 'rdp'
method['component'] = cls.tinker.value
methods[protocol].extend(applet_methods)
return methods

View File

@ -138,4 +138,6 @@ class TerminalRegistrationSerializer(serializers.ModelSerializer):
class ConnectMethodSerializer(serializers.Serializer):
value = serializers.CharField(max_length=128)
label = serializers.CharField(max_length=128)
group = serializers.CharField(max_length=128)
type = serializers.CharField(max_length=128)
listen = serializers.CharField(max_length=128)
component = serializers.CharField(max_length=128)

View File

@ -86,8 +86,6 @@ pytz==2022.1
# Runtime
django-proxy==1.2.1
channels-redis==3.4.0
channels==3.0.4
daphne==3.0.2
python-daemon==2.3.0
eventlet==0.33.1
greenlet==1.1.2
@ -96,6 +94,8 @@ celery==5.2.7
flower==1.0.0
django-celery-beat==2.3.0
kombu==5.2.4
uvicorn==0.20.0
websockets==10.4
# Auth
python-ldap==3.4.0
ldap3==2.9.1