mirror of https://github.com/jumpserver/jumpserver
Merge branch 'v3' into pr@v3@feat_support_clear_private_key
commit
bcf509ab07
|
@ -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 }}
|
|
@ -100,6 +100,6 @@ VOLUME /opt/jumpserver/logs
|
|||
|
||||
ENV LANG=zh_CN.UTF-8
|
||||
|
||||
EXPOSE 8070
|
||||
EXPOSE 8080
|
||||
|
||||
ENTRYPOINT ["./entrypoint.sh"]
|
||||
|
|
|
@ -91,6 +91,6 @@ VOLUME /opt/jumpserver/logs
|
|||
|
||||
ENV LANG=zh_CN.UTF-8
|
||||
|
||||
EXPOSE 8070
|
||||
EXPOSE 8080
|
||||
|
||||
ENTRYPOINT ["./entrypoint.sh"]
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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'),
|
||||
),
|
||||
]
|
|
@ -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')},
|
||||
},
|
||||
),
|
||||
]
|
|
@ -1,2 +1,3 @@
|
|||
from .login_acl import *
|
||||
from .login_asset_acl import *
|
||||
from .command_acl import *
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
|
@ -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')
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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")
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
]
|
||||
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'},
|
||||
),
|
||||
]
|
|
@ -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 = [
|
||||
]
|
|
@ -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'),
|
||||
),
|
||||
]
|
|
@ -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',
|
||||
),
|
||||
]
|
|
@ -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',),
|
||||
),
|
||||
]
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)()
|
||||
|
|
|
@ -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},
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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'),
|
||||
),
|
||||
]
|
|
@ -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,
|
||||
),
|
||||
]
|
|
@ -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,
|
||||
),
|
||||
]
|
|
@ -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',
|
||||
),
|
||||
]
|
|
@ -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
|
||||
|
|
|
@ -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},
|
||||
}
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
from .beat import *
|
||||
from .celery_ansible import *
|
||||
from .celery_default import *
|
||||
from .daphne import *
|
||||
from .flower import *
|
||||
from .gunicorn import *
|
||||
|
|
|
@ -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
|
|
@ -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',
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:adfa9c01178d5f6490e616f62d41c71974d42f9e3bd078fcf1b3c7124384df0b
|
||||
size 117024
|
||||
oid sha256:4a5338177d87680e0030c77f187a06664136d5dea63c8dffc43fa686091f2da4
|
||||
size 117102
|
||||
|
|
|
@ -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 "動的コード"
|
||||
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:eeaa813f4ea052a1cd85b8ae5addfde6b088fd21a0261f8724d62823835512a2
|
||||
size 104043
|
||||
oid sha256:30ae571e06eb7d2f0fee70013a812ea3bdb8e14715e1a1f4eb5e2c92311034f8
|
||||
size 104086
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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']},
|
||||
),
|
||||
]
|
|
@ -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'),
|
||||
),
|
||||
]
|
|
@ -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',
|
||||
),
|
||||
]
|
|
@ -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'),
|
||||
),
|
||||
]
|
|
@ -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'),
|
||||
),
|
||||
]
|
|
@ -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 = [
|
||||
]
|
|
@ -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'),
|
||||
),
|
||||
]
|
|
@ -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")
|
||||
|
|
|
@ -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']
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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"
|
||||
]
|
||||
|
|
|
@ -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",
|
||||
]
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
|
||||
class PermTokenViewSet(ModelViewSet):
|
||||
pass
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
# ------------------------------------------
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 |
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue