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
|
ENV LANG=zh_CN.UTF-8
|
||||||
|
|
||||||
EXPOSE 8070
|
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
|
|
||||||
ENTRYPOINT ["./entrypoint.sh"]
|
ENTRYPOINT ["./entrypoint.sh"]
|
||||||
|
|
|
@ -91,6 +91,6 @@ VOLUME /opt/jumpserver/logs
|
||||||
|
|
||||||
ENV LANG=zh_CN.UTF-8
|
ENV LANG=zh_CN.UTF-8
|
||||||
|
|
||||||
EXPOSE 8070
|
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
|
|
||||||
ENTRYPOINT ["./entrypoint.sh"]
|
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.generics import CreateAPIView
|
||||||
|
from rest_framework.response import Response
|
||||||
|
|
||||||
from common.utils import reverse, lazyproperty
|
from common.utils import reverse, lazyproperty
|
||||||
from orgs.utils import tmp_to_org
|
from orgs.utils import tmp_to_org
|
||||||
from ..models import LoginAssetACL
|
|
||||||
from .. import serializers
|
from .. import serializers
|
||||||
|
from ..models import LoginAssetACL
|
||||||
|
|
||||||
__all__ = ['LoginAssetCheckAPI']
|
__all__ = ['LoginAssetCheckAPI']
|
||||||
|
|
||||||
|
@ -20,34 +20,40 @@ class LoginAssetCheckAPI(CreateAPIView):
|
||||||
return LoginAssetACL.objects.all()
|
return LoginAssetACL.objects.all()
|
||||||
|
|
||||||
def create(self, request, *args, **kwargs):
|
def create(self, request, *args, **kwargs):
|
||||||
is_need_confirm, response_data = self.check_if_need_confirm()
|
data = self.check_confirm()
|
||||||
return Response(data=response_data, status=200)
|
return Response(data=data, status=200)
|
||||||
|
|
||||||
def check_if_need_confirm(self):
|
@lazyproperty
|
||||||
queries = {
|
def serializer(self):
|
||||||
'user': self.serializer.user, 'asset': self.serializer.asset,
|
serializer = self.get_serializer(data=self.request.data)
|
||||||
'account': self.serializer.account,
|
serializer.is_valid(raise_exception=True)
|
||||||
'action': LoginAssetACL.ActionChoices.login_confirm
|
return serializer
|
||||||
}
|
|
||||||
with tmp_to_org(self.serializer.org):
|
|
||||||
acl = LoginAssetACL.filter(**queries).valid().first()
|
|
||||||
|
|
||||||
if not acl:
|
def check_confirm(self):
|
||||||
is_need_confirm = False
|
with tmp_to_org(self.serializer.asset.org):
|
||||||
response_data = {}
|
acl = LoginAssetACL.objects \
|
||||||
else:
|
.filter(action=LoginAssetACL.ActionChoices.confirm) \
|
||||||
is_need_confirm = True
|
.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 = self._get_response_data_of_need_confirm(acl)
|
||||||
response_data['need_confirm'] = is_need_confirm
|
else:
|
||||||
return is_need_confirm, response_data
|
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(
|
ticket = LoginAssetACL.create_login_asset_confirm_ticket(
|
||||||
user=self.serializer.user,
|
user=self.serializer.user,
|
||||||
asset=self.serializer.asset,
|
asset=self.serializer.asset,
|
||||||
account=self.serializer.account,
|
account_username=self.serializer.validated_data.get('account_username'),
|
||||||
assignees=acl.reviewers.all(),
|
assignees=acl.reviewers.all(),
|
||||||
org_id=self.serializer.org.id,
|
org_id=self.serializer.asset.org.id,
|
||||||
)
|
)
|
||||||
confirm_status_url = reverse(
|
confirm_status_url = reverse(
|
||||||
view_name='api-tickets:super-ticket-status',
|
view_name='api-tickets:super-ticket-status',
|
||||||
|
@ -68,10 +74,3 @@ class LoginAssetCheckAPI(CreateAPIView):
|
||||||
'ticket_id': str(ticket.id)
|
'ticket_id': str(ticket.id)
|
||||||
}
|
}
|
||||||
return data
|
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(
|
migrations.AddField(
|
||||||
model_name='loginassetacl',
|
model_name='loginassetacl',
|
||||||
name='accounts',
|
name='accounts',
|
||||||
field=models.JSONField(default=dict, verbose_name='Account'),
|
field=models.JSONField(verbose_name='Account'),
|
||||||
),
|
),
|
||||||
migrations.RunPython(migrate_system_users_to_accounts),
|
migrations.RunPython(migrate_system_users_to_accounts),
|
||||||
migrations.RemoveField(
|
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_acl import *
|
||||||
from .login_asset_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
|
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):
|
class BaseACLQuerySet(models.QuerySet):
|
||||||
|
@ -21,6 +27,11 @@ class BaseACLQuerySet(models.QuerySet):
|
||||||
return self.inactive()
|
return self.inactive()
|
||||||
|
|
||||||
|
|
||||||
|
class ACLManager(models.Manager):
|
||||||
|
def valid(self):
|
||||||
|
return self.get_queryset().valid()
|
||||||
|
|
||||||
|
|
||||||
class BaseACL(CommonModelMixin):
|
class BaseACL(CommonModelMixin):
|
||||||
name = models.CharField(max_length=128, verbose_name=_('Name'))
|
name = models.CharField(max_length=128, verbose_name=_('Name'))
|
||||||
priority = models.IntegerField(
|
priority = models.IntegerField(
|
||||||
|
@ -28,8 +39,16 @@ class BaseACL(CommonModelMixin):
|
||||||
help_text=_("1-100, the lower the value will be match first"),
|
help_text=_("1-100, the lower the value will be match first"),
|
||||||
validators=[MinValueValidator(1), MaxValueValidator(100)]
|
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"))
|
is_active = models.BooleanField(default=True, verbose_name=_("Active"))
|
||||||
comment = models.TextField(default='', blank=True, verbose_name=_('Comment'))
|
comment = models.TextField(default='', blank=True, verbose_name=_('Comment'))
|
||||||
|
|
||||||
|
objects = ACLManager.from_queryset(BaseACLQuerySet)()
|
||||||
|
ActionChoices = ActionChoices
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
abstract = True
|
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.db import models
|
||||||
from django.utils.translation import ugettext_lazy as _
|
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 import get_request_ip, get_ip_city
|
||||||
from common.utils.ip import contains_ip
|
from common.utils.ip import contains_ip
|
||||||
from common.utils.time_period import contains_time_period
|
from common.utils.time_period import contains_time_period
|
||||||
from common.utils.timezone import local_now_display
|
from common.utils.timezone import local_now_display
|
||||||
|
from .base import BaseACL
|
||||||
|
|
||||||
class ACLManager(models.Manager):
|
|
||||||
|
|
||||||
def valid(self):
|
|
||||||
return self.get_queryset().valid()
|
|
||||||
|
|
||||||
|
|
||||||
class LoginACL(BaseACL):
|
class LoginACL(BaseACL):
|
||||||
class ActionChoices(models.TextChoices):
|
|
||||||
reject = 'reject', _('Reject')
|
|
||||||
allow = 'allow', _('Allow')
|
|
||||||
confirm = 'confirm', _('Login confirm')
|
|
||||||
|
|
||||||
# 用户
|
# 用户
|
||||||
user = models.ForeignKey(
|
user = models.ForeignKey(
|
||||||
'users.User', on_delete=models.CASCADE, verbose_name=_('User'),
|
'users.User', on_delete=models.CASCADE, verbose_name=_('User'),
|
||||||
|
@ -26,16 +16,6 @@ class LoginACL(BaseACL):
|
||||||
)
|
)
|
||||||
# 规则
|
# 规则
|
||||||
rules = models.JSONField(default=dict, verbose_name=_('Rule'))
|
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:
|
class Meta:
|
||||||
ordering = ('priority', '-date_updated', 'name')
|
ordering = ('priority', '-date_updated', 'name')
|
||||||
|
|
|
@ -2,37 +2,43 @@ from django.db import models
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from orgs.mixins.models import OrgModelMixin, OrgManager
|
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
|
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):
|
def filter_asset(self, asset):
|
||||||
return self.get_queryset().valid()
|
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 LoginAssetACL(BaseACL, OrgModelMixin):
|
||||||
class ActionChoices(models.TextChoices):
|
|
||||||
login_confirm = 'login_confirm', _('Login confirm')
|
|
||||||
|
|
||||||
# 条件
|
# 条件
|
||||||
users = models.JSONField(verbose_name=_('User'))
|
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'))
|
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:
|
class Meta:
|
||||||
unique_together = ('name', 'org_id')
|
unique_together = ('name', 'org_id')
|
||||||
|
@ -43,44 +49,7 @@ class LoginAssetACL(BaseACL, OrgModelMixin):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def filter(cls, user, asset, account, action):
|
def create_login_asset_confirm_ticket(cls, user, asset, account_username, assignees, org_id):
|
||||||
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):
|
|
||||||
from tickets.const import TicketType
|
from tickets.const import TicketType
|
||||||
from tickets.models import ApplyLoginAssetTicket
|
from tickets.models import ApplyLoginAssetTicket
|
||||||
title = _('Login asset confirm') + ' ({})'.format(user)
|
title = _('Login asset confirm') + ' ({})'.format(user)
|
||||||
|
@ -90,7 +59,7 @@ class LoginAssetACL(BaseACL, OrgModelMixin):
|
||||||
'applicant': user,
|
'applicant': user,
|
||||||
'apply_login_user': user,
|
'apply_login_user': user,
|
||||||
'apply_login_asset': asset,
|
'apply_login_asset': asset,
|
||||||
'apply_login_account': str(account),
|
'apply_login_account': account_username,
|
||||||
'type': TicketType.login_asset_confirm,
|
'type': TicketType.login_asset_confirm,
|
||||||
}
|
}
|
||||||
ticket = ApplyLoginAssetTicket.objects.create(**data)
|
ticket = ApplyLoginAssetTicket.objects.create(**data)
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from django.utils.translation import ugettext_lazy as _
|
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.mixins.serializers import BulkOrgResourceModelSerializer
|
||||||
from orgs.models import Organization
|
from orgs.models import Organization
|
||||||
from common.drf.fields import LabeledChoiceField
|
from users.models import User
|
||||||
from acls import models
|
from acls import models
|
||||||
|
|
||||||
|
|
||||||
|
@ -25,39 +27,28 @@ class LoginAssetACLUsersSerializer(serializers.Serializer):
|
||||||
|
|
||||||
|
|
||||||
class LoginAssetACLAssestsSerializer(serializers.Serializer):
|
class LoginAssetACLAssestsSerializer(serializers.Serializer):
|
||||||
ip_group_help_text = _(
|
address_group_help_text = _(
|
||||||
"Format for comma-delimited string, with * indicating a match all. "
|
"Format for comma-delimited string, with * indicating a match all. "
|
||||||
"Such as: "
|
"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"
|
"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)"
|
" (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(
|
name_group = serializers.ListField(
|
||||||
default=["*"],
|
default=["*"],
|
||||||
child=serializers.CharField(max_length=128),
|
child=serializers.CharField(max_length=128),
|
||||||
label=_("Name"),
|
label=_("Name"),
|
||||||
help_text=common_help_text,
|
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(
|
username_group = serializers.ListField(
|
||||||
default=["*"],
|
default=["*"],
|
||||||
child=serializers.CharField(max_length=128),
|
child=serializers.CharField(max_length=128),
|
||||||
|
@ -70,9 +61,10 @@ class LoginAssetACLSerializer(BulkOrgResourceModelSerializer):
|
||||||
users = LoginAssetACLUsersSerializer()
|
users = LoginAssetACLUsersSerializer()
|
||||||
assets = LoginAssetACLAssestsSerializer()
|
assets = LoginAssetACLAssestsSerializer()
|
||||||
accounts = LoginAssetACLAccountsSerializer()
|
accounts = LoginAssetACLAccountsSerializer()
|
||||||
reviewers_amount = serializers.IntegerField(
|
reviewers = ObjectRelatedField(
|
||||||
read_only=True, source="reviewers.count"
|
queryset=User.objects, many=True, required=False, label=_('Reviewers')
|
||||||
)
|
)
|
||||||
|
reviewers_amount = serializers.IntegerField(read_only=True, source="reviewers.count")
|
||||||
action = LabeledChoiceField(
|
action = LabeledChoiceField(
|
||||||
choices=models.LoginAssetACL.ActionChoices.choices, label=_("Action")
|
choices=models.LoginAssetACL.ActionChoices.choices, label=_("Action")
|
||||||
)
|
)
|
||||||
|
|
|
@ -10,37 +10,26 @@ __all__ = ['LoginAssetCheckSerializer']
|
||||||
class LoginAssetCheckSerializer(serializers.Serializer):
|
class LoginAssetCheckSerializer(serializers.Serializer):
|
||||||
user_id = serializers.UUIDField(required=True, allow_null=False)
|
user_id = serializers.UUIDField(required=True, allow_null=False)
|
||||||
asset_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='')
|
account_username = serializers.CharField(max_length=128, default='')
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.user = None
|
self.user = None
|
||||||
self.asset = None
|
self.asset = None
|
||||||
self.account = None
|
|
||||||
self._account_username = None
|
|
||||||
|
|
||||||
def validate_user_id(self, user_id):
|
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
|
return user_id
|
||||||
|
|
||||||
def validate_asset_id(self, asset_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
|
return asset_id
|
||||||
|
|
||||||
def validate_account_id(self, account_id):
|
|
||||||
self.account = self.validate_object_exist(Account, account_id)
|
|
||||||
return account_id
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def validate_object_exist(model, field_id):
|
def get_object(model, pk):
|
||||||
with tmp_to_root_org():
|
with tmp_to_root_org():
|
||||||
obj = get_object_or_none(model, pk=field_id)
|
obj = get_object_or_none(model, pk=pk)
|
||||||
if not obj:
|
if obj:
|
||||||
|
return obj
|
||||||
error = '{} Model object does not exist'.format(model.__name__)
|
error = '{} Model object does not exist'.format(model.__name__)
|
||||||
raise serializers.ValidationError(error)
|
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 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):
|
class AccountTemplateViewSet(OrgBulkModelViewSet):
|
||||||
|
@ -10,3 +14,16 @@ class AccountTemplateViewSet(OrgBulkModelViewSet):
|
||||||
serializer_classes = {
|
serializer_classes = {
|
||||||
'default': serializers.AccountTemplateSerializer
|
'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 rest_framework.response import Response
|
||||||
|
|
||||||
from assets import serializers
|
from assets import serializers
|
||||||
from assets.models import Asset
|
|
||||||
from assets.filters import IpInFilterBackend, LabelFilterBackend, NodeFilterBackend
|
from assets.filters import IpInFilterBackend, LabelFilterBackend, NodeFilterBackend
|
||||||
|
from assets.models import Asset
|
||||||
from assets.tasks import (
|
from assets.tasks import (
|
||||||
push_accounts_to_assets, test_assets_connectivity_manual,
|
push_accounts_to_assets, test_assets_connectivity_manual,
|
||||||
update_assets_hardware_info_manual, verify_accounts_connectivity,
|
update_assets_hardware_info_manual, verify_accounts_connectivity,
|
||||||
|
@ -24,6 +24,7 @@ __all__ = [
|
||||||
"AssetViewSet",
|
"AssetViewSet",
|
||||||
"AssetTaskCreateApi",
|
"AssetTaskCreateApi",
|
||||||
"AssetsTaskCreateApi",
|
"AssetsTaskCreateApi",
|
||||||
|
'AssetFilterSet'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
# ~*~ coding: utf-8 ~*~
|
# ~*~ coding: utf-8 ~*~
|
||||||
|
from django.db.models import F
|
||||||
from django.views.generic.detail import SingleObjectMixin
|
from django.views.generic.detail import SingleObjectMixin
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
from rest_framework.views import APIView, Response
|
from rest_framework.views import APIView, Response
|
||||||
|
@ -29,12 +29,13 @@ class DomainViewSet(OrgBulkModelViewSet):
|
||||||
|
|
||||||
|
|
||||||
class GatewayViewSet(OrgBulkModelViewSet):
|
class GatewayViewSet(OrgBulkModelViewSet):
|
||||||
|
perm_model = Host
|
||||||
filterset_fields = ("domain__name", "name", "domain")
|
filterset_fields = ("domain__name", "name", "domain")
|
||||||
search_fields = ("domain__name",)
|
search_fields = ("domain__name",)
|
||||||
serializer_class = serializers.GatewaySerializer
|
serializer_class = serializers.GatewaySerializer
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
queryset = Host.get_gateway_queryset()
|
queryset = Domain.get_gateway_queryset()
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
|
@ -44,17 +45,17 @@ class GatewayTestConnectionApi(SingleObjectMixin, APIView):
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
queryset = Host.get_gateway_queryset()
|
queryset = Domain.get_gateway_queryset()
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
self.object = self.get_object()
|
gateway = self.get_object()
|
||||||
local_port = self.request.data.get('port') or self.object.port
|
local_port = self.request.data.get('port') or gateway.port
|
||||||
try:
|
try:
|
||||||
local_port = int(local_port)
|
local_port = int(local_port)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise ValidationError({'port': _('Number required')})
|
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:
|
if ok:
|
||||||
return Response("ok")
|
return Response("ok")
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
|
|
||||||
from common.utils import lazyproperty, timeit
|
from assets.models import Node, PlatformProtocol
|
||||||
from assets.models import Node, Asset
|
|
||||||
from assets.pagination import NodeAssetTreePagination
|
|
||||||
from assets.utils import get_node_from_request, is_query_node_all_assets
|
from assets.utils import get_node_from_request, is_query_node_all_assets
|
||||||
|
from common.utils import lazyproperty, timeit
|
||||||
|
|
||||||
|
|
||||||
class SerializeToTreeNodeMixin:
|
class SerializeToTreeNodeMixin:
|
||||||
|
@ -38,16 +38,11 @@ class SerializeToTreeNodeMixin:
|
||||||
]
|
]
|
||||||
return data
|
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
|
@timeit
|
||||||
def serialize_assets(self, assets, node_key=None):
|
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:
|
if node_key is None:
|
||||||
get_pid = lambda asset: getattr(asset, 'parent_key', '')
|
get_pid = lambda asset: getattr(asset, 'parent_key', '')
|
||||||
else:
|
else:
|
||||||
|
@ -61,17 +56,13 @@ class SerializeToTreeNodeMixin:
|
||||||
'pId': get_pid(asset),
|
'pId': get_pid(asset),
|
||||||
'isParent': False,
|
'isParent': False,
|
||||||
'open': False,
|
'open': False,
|
||||||
'iconSkin': self.get_platform(asset),
|
'iconSkin': asset.type,
|
||||||
'chkDisabled': not asset.is_active,
|
'chkDisabled': not asset.is_active,
|
||||||
'meta': {
|
'meta': {
|
||||||
'type': 'asset',
|
'type': 'asset',
|
||||||
'data': {
|
'data': {
|
||||||
'id': asset.id,
|
'org_name': asset.org_name,
|
||||||
'name': asset.name,
|
'sftp': asset.platform_id in sftp_enabled_platform,
|
||||||
'address': asset.address,
|
|
||||||
'protocols': asset.protocols_as_list,
|
|
||||||
'platform': asset.platform.id,
|
|
||||||
'org_name': asset.org_name
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -81,7 +72,6 @@ class SerializeToTreeNodeMixin:
|
||||||
|
|
||||||
|
|
||||||
class NodeFilterMixin:
|
class NodeFilterMixin:
|
||||||
# pagination_class = NodeAssetTreePagination
|
|
||||||
request: Request
|
request: Request
|
||||||
|
|
||||||
@lazyproperty
|
@lazyproperty
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
- hosts: mysql
|
- hosts: mysql
|
||||||
gather_facts: no
|
gather_facts: no
|
||||||
vars:
|
vars:
|
||||||
ansible_python_interpreter: /Users/xiaofeng/Desktop/jumpserver/venv/bin/python
|
ansible_python_interpreter: /usr/local/bin/python
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
- name: Get info
|
- 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 = (
|
unique_together = (
|
||||||
('name', 'org_id'),
|
('name', 'org_id'),
|
||||||
)
|
)
|
||||||
|
permissions = [
|
||||||
|
('view_accounttemplatesecret', _('Can view asset account template secret')),
|
||||||
|
('change_accounttemplatesecret', _('Can change asset account template secret')),
|
||||||
|
]
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.username
|
return self.username
|
||||||
|
|
|
@ -160,10 +160,6 @@ class Asset(NodesRelationMixin, AbsConnectivity, JMSOrgBaseModel):
|
||||||
return 0
|
return 0
|
||||||
return self.primary_protocol.port
|
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
|
@lazyproperty
|
||||||
def type(self):
|
def type(self):
|
||||||
return self.platform.type
|
return self.platform.type
|
||||||
|
@ -211,17 +207,6 @@ class Asset(NodesRelationMixin, AbsConnectivity, JMSOrgBaseModel):
|
||||||
tree_node = TreeNode(**data)
|
tree_node = TreeNode(**data)
|
||||||
return tree_node
|
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:
|
class Meta:
|
||||||
unique_together = [('org_id', 'name')]
|
unique_together = [('org_id', 'name')]
|
||||||
verbose_name = _("Asset")
|
verbose_name = _("Asset")
|
||||||
|
|
|
@ -6,6 +6,11 @@ from .common import Asset
|
||||||
|
|
||||||
class Database(Asset):
|
class Database(Asset):
|
||||||
db_name = models.CharField(max_length=1024, verbose_name=_("Database"), blank=True)
|
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):
|
def __str__(self):
|
||||||
return '{}({}://{}/{})'.format(self.name, self.type, self.address, self.db_name)
|
return '{}({}://{}/{})'.format(self.name, self.type, self.address, self.db_name)
|
||||||
|
@ -18,6 +23,11 @@ class Database(Asset):
|
||||||
def specific(self):
|
def specific(self):
|
||||||
return {
|
return {
|
||||||
'db_name': self.db_name,
|
'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:
|
class Meta:
|
||||||
|
|
|
@ -3,10 +3,4 @@ from .common import Asset
|
||||||
|
|
||||||
|
|
||||||
class Host(Asset):
|
class Host(Asset):
|
||||||
|
pass
|
||||||
@classmethod
|
|
||||||
def get_gateway_queryset(cls):
|
|
||||||
queryset = cls.objects.filter(
|
|
||||||
platform__name=GATEWAY_NAME
|
|
||||||
)
|
|
||||||
return queryset
|
|
||||||
|
|
|
@ -1,23 +1,22 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
#
|
#
|
||||||
import io
|
|
||||||
import os
|
import os
|
||||||
import sshpubkeys
|
|
||||||
from hashlib import md5
|
from hashlib import md5
|
||||||
|
|
||||||
from django.db import models
|
import sshpubkeys
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils import timezone
|
from django.db import models
|
||||||
from django.db.models import QuerySet
|
from django.db.models import QuerySet
|
||||||
|
from django.utils import timezone
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
from assets.const import Connectivity, SecretType
|
||||||
|
from common.db import fields
|
||||||
from common.utils import (
|
from common.utils import (
|
||||||
ssh_key_string_to_obj, ssh_key_gen, get_logger,
|
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 orgs.mixins.models import JMSOrgBaseModel
|
||||||
from assets.const import Connectivity, SecretType
|
|
||||||
|
|
||||||
logger = get_logger(__file__)
|
logger = get_logger(__file__)
|
||||||
|
|
||||||
|
@ -62,6 +61,10 @@ class BaseAccount(JMSOrgBaseModel):
|
||||||
def has_secret(self):
|
def has_secret(self):
|
||||||
return bool(self.secret)
|
return bool(self.secret)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_username(self):
|
||||||
|
return bool(self.username)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def specific(self):
|
def specific(self):
|
||||||
data = {}
|
data = {}
|
||||||
|
@ -84,7 +87,7 @@ class BaseAccount(JMSOrgBaseModel):
|
||||||
@lazyproperty
|
@lazyproperty
|
||||||
def public_key(self):
|
def public_key(self):
|
||||||
if self.secret_type == SecretType.SSH_KEY and self.private_key:
|
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
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -93,7 +96,7 @@ class BaseAccount(JMSOrgBaseModel):
|
||||||
public_key = self.public_key
|
public_key = self.public_key
|
||||||
elif self.private_key:
|
elif self.private_key:
|
||||||
try:
|
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:
|
except IOError as e:
|
||||||
return str(e)
|
return str(e)
|
||||||
else:
|
else:
|
||||||
|
@ -125,12 +128,9 @@ class BaseAccount(JMSOrgBaseModel):
|
||||||
return key_path
|
return key_path
|
||||||
|
|
||||||
def get_private_key(self):
|
def get_private_key(self):
|
||||||
if not self.private_key_obj:
|
if not self.private_key:
|
||||||
return None
|
return None
|
||||||
string_io = io.StringIO()
|
return self.private_key
|
||||||
self.private_key_obj.write_private_key(string_io)
|
|
||||||
private_key = string_io.getvalue()
|
|
||||||
return private_key
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def public_key_obj(self):
|
def public_key_obj(self):
|
||||||
|
|
|
@ -183,7 +183,7 @@ class CommandFilterRule(OrgModelMixin):
|
||||||
cls, user_id=None, user_group_id=None, account=None,
|
cls, user_id=None, user_group_id=None, account=None,
|
||||||
asset_id=None, org_id=None
|
asset_id=None, org_id=None
|
||||||
):
|
):
|
||||||
from perms.models.const import SpecialAccount
|
from assets.models import Account
|
||||||
user_groups = []
|
user_groups = []
|
||||||
user = get_object_or_none(User, pk=user_id)
|
user = get_object_or_none(User, pk=user_id)
|
||||||
if user:
|
if user:
|
||||||
|
@ -202,7 +202,7 @@ class CommandFilterRule(OrgModelMixin):
|
||||||
if account:
|
if account:
|
||||||
org_id = account.org_id
|
org_id = account.org_id
|
||||||
q |= Q(accounts__contains=account.username) | \
|
q |= Q(accounts__contains=account.username) | \
|
||||||
Q(accounts__contains=SpecialAccount.ALL.value)
|
Q(accounts__contains=Account.AliasAccount.ALL)
|
||||||
if asset:
|
if asset:
|
||||||
org_id = asset.org_id
|
org_id = asset.org_id
|
||||||
q |= Q(assets=asset)
|
q |= Q(assets=asset)
|
||||||
|
|
|
@ -1,25 +1,21 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
#
|
#
|
||||||
import uuid
|
import uuid
|
||||||
import socket
|
|
||||||
import random
|
import random
|
||||||
import paramiko
|
|
||||||
|
|
||||||
|
import paramiko
|
||||||
from django.db import models
|
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 django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from common.db import fields
|
|
||||||
from common.utils import get_logger, lazyproperty
|
from common.utils import get_logger, lazyproperty
|
||||||
from orgs.mixins.models import OrgModelMixin
|
from orgs.mixins.models import OrgModelMixin
|
||||||
from assets.models import Host
|
from assets.models import Host, Platform
|
||||||
from .base import BaseAccount
|
from assets.const import GATEWAY_NAME
|
||||||
from ..const import SecretType
|
from orgs.mixins.models import OrgManager
|
||||||
|
|
||||||
logger = get_logger(__file__)
|
logger = get_logger(__file__)
|
||||||
|
|
||||||
__all__ = ['Domain', 'GatewayMixin']
|
__all__ = ['Domain', 'Gateway']
|
||||||
|
|
||||||
|
|
||||||
class Domain(OrgModelMixin):
|
class Domain(OrgModelMixin):
|
||||||
|
@ -36,9 +32,13 @@ class Domain(OrgModelMixin):
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_gateway_queryset(cls):
|
||||||
|
return Gateway.objects.all()
|
||||||
|
|
||||||
@lazyproperty
|
@lazyproperty
|
||||||
def gateways(self):
|
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):
|
def select_gateway(self):
|
||||||
return self.random_gateway()
|
return self.random_gateway()
|
||||||
|
@ -53,158 +53,30 @@ class Domain(OrgModelMixin):
|
||||||
return random.choice(self.gateways)
|
return random.choice(self.gateways)
|
||||||
|
|
||||||
|
|
||||||
class GatewayMixin:
|
class GatewayManager(OrgManager):
|
||||||
id: uuid.UUID
|
def get_queryset(self):
|
||||||
port: int
|
queryset = super().get_queryset()
|
||||||
address: str
|
queryset = queryset.filter(platform__name=GATEWAY_NAME)
|
||||||
accounts: QuerySet
|
return 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
|
|
||||||
|
|
||||||
def set_unconnected(self):
|
def bulk_create(self, objs, batch_size=None, ignore_conflicts=False):
|
||||||
unconnected_key = self.UNCONNECTED_KEY_TMPL.format(self.id)
|
platform = Gateway().default_platform
|
||||||
unconnected_silence_period_key = self.UNCONNECTED_SILENCE_PERIOD_KEY_TMPL.format(self.id)
|
for obj in objs:
|
||||||
unconnected_silence_period = cache.get(
|
obj.platform_id = platform.id
|
||||||
unconnected_silence_period_key, self.UNCONNECTED_SILENCE_PERIOD_BEGIN_VALUE
|
return super().bulk_create(objs, batch_size, ignore_conflicts)
|
||||||
)
|
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
class Gateway(BaseAccount):
|
class Gateway(Host):
|
||||||
class Protocol(models.TextChoices):
|
objects = GatewayManager()
|
||||||
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 Meta:
|
class Meta:
|
||||||
unique_together = [('name', 'org_id')]
|
proxy = True
|
||||||
verbose_name = _("Gateway")
|
|
||||||
permissions = [
|
@lazyproperty
|
||||||
('test_gateway', _('Test gateway'))
|
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='')
|
full_value = models.CharField(max_length=4096, verbose_name=_('Full value'), default='')
|
||||||
child_mark = models.IntegerField(default=0)
|
child_mark = models.IntegerField(default=0)
|
||||||
date_create = models.DateTimeField(auto_now_add=True)
|
date_create = models.DateTimeField(auto_now_add=True)
|
||||||
parent_key = models.CharField(max_length=64, verbose_name=_("Parent key"),
|
parent_key = models.CharField(
|
||||||
db_index=True, default='')
|
max_length=64, verbose_name=_("Parent key"), db_index=True, default=''
|
||||||
|
)
|
||||||
assets_amount = models.IntegerField(default=0)
|
assets_amount = models.IntegerField(default=0)
|
||||||
|
|
||||||
objects = OrgManager.from_queryset(NodeQuerySet)()
|
objects = OrgManager.from_queryset(NodeQuerySet)()
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
from common.drf.serializers import SecretReadableMixin
|
||||||
from assets.models import AccountTemplate
|
from assets.models import AccountTemplate
|
||||||
from .base import BaseAccountSerializer
|
from .base import BaseAccountSerializer
|
||||||
|
|
||||||
|
@ -17,3 +18,10 @@ class AccountTemplateSerializer(BaseAccountSerializer):
|
||||||
# if not required_field_dict:
|
# if not required_field_dict:
|
||||||
# return
|
# return
|
||||||
# raise serializers.ValidationError(required_field_dict)
|
# 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 django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
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 common.drf.fields import ObjectRelatedField, EncryptedField
|
||||||
from assets.const import SecretType
|
from assets.const import SecretType, GATEWAY_NAME
|
||||||
from ..models import Domain, Asset, Account
|
from ..serializers import AssetProtocolsSerializer
|
||||||
from ..serializers import HostSerializer
|
from ..models import Platform, Domain, Node, Asset, Account, Host
|
||||||
from .utils import validate_password_for_ansible, validate_ssh_key
|
from .utils import validate_password_for_ansible, validate_ssh_key
|
||||||
|
|
||||||
|
|
||||||
|
@ -41,7 +41,7 @@ class DomainSerializer(BulkOrgResourceModelSerializer):
|
||||||
return obj.gateways.count()
|
return obj.gateways.count()
|
||||||
|
|
||||||
|
|
||||||
class GatewaySerializer(HostSerializer):
|
class GatewaySerializer(BulkOrgResourceModelSerializer, WritableNestedModelSerializer):
|
||||||
password = EncryptedField(
|
password = EncryptedField(
|
||||||
label=_('Password'), required=False, allow_blank=True, allow_null=True, max_length=1024,
|
label=_('Password'), required=False, allow_blank=True, allow_null=True, max_length=1024,
|
||||||
validators=[validate_password_for_ansible], write_only=True
|
validators=[validate_password_for_ansible], write_only=True
|
||||||
|
@ -55,13 +55,27 @@ class GatewaySerializer(HostSerializer):
|
||||||
max_length=512,
|
max_length=512,
|
||||||
)
|
)
|
||||||
username = serializers.CharField(
|
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):
|
class Meta:
|
||||||
fields = HostSerializer.Meta.fields + [
|
model = Host
|
||||||
'username', 'password', 'private_key', 'passphrase'
|
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):
|
def validate_private_key(self, secret):
|
||||||
if not secret:
|
if not secret:
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
from io import StringIO
|
|
||||||
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from rest_framework import serializers
|
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):
|
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)
|
valid = validate_ssh_private_key(ssh_key, password=passphrase)
|
||||||
if not valid:
|
if not valid:
|
||||||
raise serializers.ValidationError(_("private key invalid or passphrase error"))
|
raise serializers.ValidationError(_("private key invalid or passphrase error"))
|
||||||
|
return parse_ssh_private_key_str(ssh_key, passphrase)
|
||||||
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
|
|
||||||
|
|
|
@ -12,11 +12,14 @@ __all__ = [
|
||||||
|
|
||||||
|
|
||||||
@org_aware_func("assets")
|
@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
|
from assets.models import PushAccountAutomation
|
||||||
task_name = gettext_noop("Push accounts to assets")
|
task_name = gettext_noop("Push accounts to assets")
|
||||||
task_name = PushAccountAutomation.generate_unique_name(task_name)
|
task_name = PushAccountAutomation.generate_unique_name(task_name)
|
||||||
|
if username is None:
|
||||||
account_usernames = list(accounts.values_list('username', flat=True))
|
account_usernames = list(accounts.values_list('username', flat=True))
|
||||||
|
else:
|
||||||
|
account_usernames = [username]
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
'name': task_name,
|
'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'))
|
@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
|
from assets.models import Asset, Account
|
||||||
with tmp_to_root_org():
|
with tmp_to_root_org():
|
||||||
assets = Asset.objects.filter(id__in=asset_ids)
|
assets = Asset.objects.filter(id__in=asset_ids)
|
||||||
accounts = Account.objects.filter(id__in=account_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'clouds', api.CloudViewSet, 'cloud')
|
||||||
router.register(r'accounts', api.AccountViewSet, 'account')
|
router.register(r'accounts', api.AccountViewSet, 'account')
|
||||||
router.register(r'account-templates', api.AccountTemplateViewSet, 'account-template')
|
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'account-secrets', api.AccountSecretsViewSet, 'account-secret')
|
||||||
router.register(r'platforms', api.AssetPlatformViewSet, 'platform')
|
router.register(r'platforms', api.AssetPlatformViewSet, 'platform')
|
||||||
router.register(r'labels', api.LabelViewSet, 'label')
|
router.register(r'labels', api.LabelViewSet, 'label')
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import base64
|
import base64
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import time
|
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
|
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
|
@ -12,17 +11,20 @@ from rest_framework.decorators import action
|
||||||
from rest_framework.exceptions import PermissionDenied
|
from rest_framework.exceptions import PermissionDenied
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.serializers import ValidationError
|
||||||
|
|
||||||
from common.drf.api import JMSModelViewSet
|
from common.drf.api import JMSModelViewSet
|
||||||
from common.http import is_true
|
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.mixins.api import RootOrgViewMixin
|
||||||
from orgs.utils import tmp_to_root_org
|
|
||||||
from perms.models import ActionChoices
|
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 ..models import ConnectionToken
|
||||||
from ..serializers import (
|
from ..serializers import (
|
||||||
ConnectionTokenSerializer, ConnectionTokenSecretSerializer,
|
ConnectionTokenSerializer, ConnectionTokenSecretSerializer,
|
||||||
SuperConnectionTokenSerializer, ConnectionTokenDisplaySerializer,
|
SuperConnectionTokenSerializer,
|
||||||
)
|
)
|
||||||
|
|
||||||
__all__ = ['ConnectionTokenViewSet', 'SuperConnectionTokenViewSet']
|
__all__ = ['ConnectionTokenViewSet', 'SuperConnectionTokenViewSet']
|
||||||
|
@ -32,13 +34,34 @@ class RDPFileClientProtocolURLMixin:
|
||||||
request: Request
|
request: Request
|
||||||
get_serializer: callable
|
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):
|
def get_rdp_file_info(self, token: ConnectionToken):
|
||||||
rdp_options = {
|
rdp_options = {
|
||||||
'full address:s': '',
|
'full address:s': '',
|
||||||
'username:s': '',
|
'username:s': '',
|
||||||
# 'screen mode id:i': '1',
|
|
||||||
# 'desktopwidth:i': '1280',
|
|
||||||
# 'desktopheight:i': '800',
|
|
||||||
'use multimon:i': '0',
|
'use multimon:i': '0',
|
||||||
'session bpp:i': '32',
|
'session bpp:i': '32',
|
||||||
'audiomode:i': '0',
|
'audiomode:i': '0',
|
||||||
|
@ -59,11 +82,6 @@ class RDPFileClientProtocolURLMixin:
|
||||||
'bookmarktype:i': '3',
|
'bookmarktype:i': '3',
|
||||||
'use redirection server name:i': '0',
|
'use redirection server name:i': '0',
|
||||||
'smart sizing:i': '1',
|
'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['session bpp:i'] = os.getenv('JUMPSERVER_COLOR_DEPTH', '32')
|
||||||
rdp_options['audiomode:i'] = self.parse_env_bool('JUMPSERVER_DISABLE_AUDIO', 'false', '2', '0')
|
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
|
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}'
|
prefix_name = f'{token.user.username}-{name}'
|
||||||
filename = self.get_connect_filename(prefix_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
|
return true_value if is_true(os.getenv(env_key, env_default)) else false_value
|
||||||
|
|
||||||
def get_client_protocol_data(self, token: ConnectionToken):
|
def get_client_protocol_data(self, token: ConnectionToken):
|
||||||
protocol = token.protocol
|
_os = get_request_os(self.request)
|
||||||
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))
|
|
||||||
|
|
||||||
return {
|
connect_method_name = token.connect_method
|
||||||
"filename": filename,
|
connect_method_dict = TerminalType.get_connect_method(
|
||||||
"protocol": protocol,
|
token.connect_method, token.protocol, _os
|
||||||
"username": username,
|
)
|
||||||
"token": ssh_token,
|
if connect_method_dict is None:
|
||||||
"config": rdp_config
|
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 = {
|
data = {
|
||||||
'ip': endpoint.host,
|
'id': str(token.id),
|
||||||
'port': str(endpoint.ssh_port),
|
'value': token.value,
|
||||||
'username': 'JMS-{}'.format(str(token.id)),
|
'protocol': token.protocol,
|
||||||
'password': token.secret
|
'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):
|
def get_smart_endpoint(self, protocol, asset=None):
|
||||||
target_ip = asset.get_target_ip() if asset else ''
|
target_ip = asset.get_target_ip() if asset else ''
|
||||||
|
@ -177,9 +188,9 @@ class ExtraActionApiMixin(RDPFileClientProtocolURLMixin):
|
||||||
get_serializer: callable
|
get_serializer: callable
|
||||||
perform_create: callable
|
perform_create: callable
|
||||||
|
|
||||||
@action(methods=['POST', 'GET'], detail=False, url_path='rdp/file')
|
@action(methods=['POST', 'GET'], detail=True, url_path='rdp-file')
|
||||||
def get_rdp_file(self, request, *args, **kwargs):
|
def get_rdp_file(self, *args, **kwargs):
|
||||||
token = self.create_connection_token()
|
token = self.get_object()
|
||||||
token.is_valid()
|
token.is_valid()
|
||||||
filename, content = self.get_rdp_file_info(token)
|
filename, content = self.get_rdp_file_info(token)
|
||||||
filename = '{}.rdp'.format(filename)
|
filename = '{}.rdp'.format(filename)
|
||||||
|
@ -187,9 +198,9 @@ class ExtraActionApiMixin(RDPFileClientProtocolURLMixin):
|
||||||
response['Content-Disposition'] = 'attachment; filename*=UTF-8\'\'%s' % filename
|
response['Content-Disposition'] = 'attachment; filename*=UTF-8\'\'%s' % filename
|
||||||
return response
|
return response
|
||||||
|
|
||||||
@action(methods=['POST', 'GET'], detail=False, url_path='client-url')
|
@action(methods=['POST', 'GET'], detail=True, url_path='client-url')
|
||||||
def get_client_protocol_url(self, request, *args, **kwargs):
|
def get_client_protocol_url(self, *args, **kwargs):
|
||||||
token = self.create_connection_token()
|
token = self.get_object()
|
||||||
token.is_valid()
|
token.is_valid()
|
||||||
try:
|
try:
|
||||||
protocol_data = self.get_client_protocol_data(token)
|
protocol_data = self.get_client_protocol_data(token)
|
||||||
|
@ -208,14 +219,6 @@ class ExtraActionApiMixin(RDPFileClientProtocolURLMixin):
|
||||||
instance.expire()
|
instance.expire()
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
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):
|
class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelViewSet):
|
||||||
filterset_fields = (
|
filterset_fields = (
|
||||||
|
@ -224,11 +227,10 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView
|
||||||
search_fields = filterset_fields
|
search_fields = filterset_fields
|
||||||
serializer_classes = {
|
serializer_classes = {
|
||||||
'default': ConnectionTokenSerializer,
|
'default': ConnectionTokenSerializer,
|
||||||
'list': ConnectionTokenDisplaySerializer,
|
|
||||||
'retrieve': ConnectionTokenDisplaySerializer,
|
|
||||||
'get_secret_detail': ConnectionTokenSecretSerializer,
|
'get_secret_detail': ConnectionTokenSecretSerializer,
|
||||||
}
|
}
|
||||||
rbac_perms = {
|
rbac_perms = {
|
||||||
|
'list': 'authentication.view_connectiontoken',
|
||||||
'retrieve': 'authentication.view_connectiontoken',
|
'retrieve': 'authentication.view_connectiontoken',
|
||||||
'create': 'authentication.add_connectiontoken',
|
'create': 'authentication.add_connectiontoken',
|
||||||
'expire': 'authentication.add_connectiontoken',
|
'expire': 'authentication.add_connectiontoken',
|
||||||
|
@ -243,18 +245,25 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView
|
||||||
rbac_perm = 'authentication.view_connectiontokensecret'
|
rbac_perm = 'authentication.view_connectiontokensecret'
|
||||||
if not request.user.has_perm(rbac_perm):
|
if not request.user.has_perm(rbac_perm):
|
||||||
raise PermissionDenied('Not allow to view secret')
|
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)
|
token = get_object_or_404(ConnectionToken, pk=token_id)
|
||||||
|
if token.is_expired:
|
||||||
|
raise ValidationError({'id': 'Token is expired'})
|
||||||
|
|
||||||
token.is_valid()
|
token.is_valid()
|
||||||
serializer = self.get_serializer(instance=token)
|
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)
|
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):
|
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):
|
def get_user(self, serializer):
|
||||||
return self.request.user
|
return self.request.user
|
||||||
|
@ -269,16 +278,17 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView
|
||||||
data = serializer.validated_data
|
data = serializer.validated_data
|
||||||
user = self.get_user(serializer)
|
user = self.get_user(serializer)
|
||||||
asset = data.get('asset')
|
asset = data.get('asset')
|
||||||
login = data.get('login')
|
account_name = data.get('account_name')
|
||||||
data['org_id'] = asset.org_id
|
data['org_id'] = asset.org_id
|
||||||
data['user'] = user
|
data['user'] = user
|
||||||
|
data['value'] = random_string(16)
|
||||||
|
|
||||||
util = PermAccountUtil()
|
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:
|
if not permed_account or not permed_account.actions:
|
||||||
msg = 'user `{}` not has asset `{}` permission for login `{}`'.format(
|
msg = 'user `{}` not has asset `{}` permission for login `{}`'.format(
|
||||||
user, asset, login
|
user, asset, account_name
|
||||||
)
|
)
|
||||||
raise PermissionDenied(msg)
|
raise PermissionDenied(msg)
|
||||||
|
|
||||||
|
@ -286,9 +296,9 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView
|
||||||
raise PermissionDenied('Expired')
|
raise PermissionDenied('Expired')
|
||||||
|
|
||||||
if permed_account.has_secret:
|
if permed_account.has_secret:
|
||||||
data['secret'] = ''
|
data['input_secret'] = ''
|
||||||
if permed_account.username != '@INPUT':
|
if permed_account.username != '@INPUT':
|
||||||
data['username'] = ''
|
data['input_username'] = ''
|
||||||
return permed_account
|
return permed_account
|
||||||
|
|
||||||
|
|
||||||
|
@ -311,7 +321,7 @@ class SuperConnectionTokenViewSet(ConnectionTokenViewSet):
|
||||||
def renewal(self, request, *args, **kwargs):
|
def renewal(self, request, *args, **kwargs):
|
||||||
from common.utils.timezone import as_current_tz
|
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)
|
token = get_object_or_404(ConnectionToken, pk=token_id)
|
||||||
date_expired = as_current_tz(token.date_expired)
|
date_expired = as_current_tz(token.date_expired)
|
||||||
if token.is_expired:
|
if token.is_expired:
|
||||||
|
|
|
@ -2,10 +2,10 @@ from django.utils import timezone
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
|
|
||||||
|
from rbac.permissions import RBACPermission
|
||||||
from common.drf.api import JMSModelViewSet
|
from common.drf.api import JMSModelViewSet
|
||||||
from ..models import TempToken
|
from ..models import TempToken
|
||||||
from ..serializers import TempTokenSerializer
|
from ..serializers import TempTokenSerializer
|
||||||
from rbac.permissions import RBACPermission
|
|
||||||
|
|
||||||
|
|
||||||
class TempTokenViewSet(JMSModelViewSet):
|
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 datetime import timedelta
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import models
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.translation import ugettext_lazy as _
|
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 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 import lazyproperty, pretty_string
|
||||||
from common.utils.timezone import as_current_tz
|
from common.utils.timezone import as_current_tz
|
||||||
from common.db.models import JMSBaseModel
|
from orgs.mixins.models import OrgModelMixin
|
||||||
from common.db.fields import EncryptCharField
|
|
||||||
from assets.const import Protocol
|
|
||||||
|
|
||||||
|
|
||||||
def date_expired_default():
|
def date_expired_default():
|
||||||
|
@ -19,6 +19,7 @@ def date_expired_default():
|
||||||
|
|
||||||
|
|
||||||
class ConnectionToken(OrgModelMixin, JMSBaseModel):
|
class ConnectionToken(OrgModelMixin, JMSBaseModel):
|
||||||
|
value = models.CharField(max_length=64, default='', verbose_name=_("Value"))
|
||||||
user = models.ForeignKey(
|
user = models.ForeignKey(
|
||||||
'users.User', on_delete=models.SET_NULL, null=True, blank=True,
|
'users.User', on_delete=models.SET_NULL, null=True, blank=True,
|
||||||
related_name='connection_tokens', verbose_name=_('User')
|
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,
|
'assets.Asset', on_delete=models.SET_NULL, null=True, blank=True,
|
||||||
related_name='connection_tokens', verbose_name=_('Asset'),
|
related_name='connection_tokens', verbose_name=_('Asset'),
|
||||||
)
|
)
|
||||||
login = models.CharField(max_length=128, verbose_name=_("Login account"))
|
account_name = models.CharField(max_length=128, verbose_name=_("Account name")) # 登录账号Name
|
||||||
username = models.CharField(max_length=128, default='', verbose_name=_("Username"))
|
input_username = models.CharField(max_length=128, default='', blank=True, verbose_name=_("Input Username"))
|
||||||
secret = EncryptCharField(max_length=64, default='', verbose_name=_("Secret"))
|
input_secret = EncryptCharField(max_length=64, default='', blank=True, verbose_name=_("Input Secret"))
|
||||||
protocol = models.CharField(
|
protocol = models.CharField(
|
||||||
choices=Protocol.choices, max_length=16, default=Protocol.ssh, verbose_name=_("Protocol")
|
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"))
|
user_display = models.CharField(max_length=128, default='', verbose_name=_("User display"))
|
||||||
asset_display = models.CharField(max_length=128, default='', verbose_name=_("Asset display"))
|
asset_display = models.CharField(max_length=128, default='', verbose_name=_("Asset display"))
|
||||||
date_expired = models.DateTimeField(
|
date_expired = models.DateTimeField(
|
||||||
|
@ -76,7 +78,7 @@ class ConnectionToken(OrgModelMixin, JMSBaseModel):
|
||||||
def permed_account(self):
|
def permed_account(self):
|
||||||
from perms.utils import PermAccountUtil
|
from perms.utils import PermAccountUtil
|
||||||
permed_account = PermAccountUtil().validate_permission(
|
permed_account = PermAccountUtil().validate_permission(
|
||||||
self.user, self.asset, self.login
|
self.user, self.asset, self.account_name
|
||||||
)
|
)
|
||||||
return permed_account
|
return permed_account
|
||||||
|
|
||||||
|
@ -99,13 +101,13 @@ class ConnectionToken(OrgModelMixin, JMSBaseModel):
|
||||||
is_valid = False
|
is_valid = False
|
||||||
error = _('No asset or inactive asset')
|
error = _('No asset or inactive asset')
|
||||||
return is_valid, error
|
return is_valid, error
|
||||||
if not self.login:
|
if not self.account_name:
|
||||||
error = _('No account')
|
error = _('No account')
|
||||||
raise PermissionDenied(error)
|
raise PermissionDenied(error)
|
||||||
|
|
||||||
if not self.permed_account or not self.permed_account.actions:
|
if not self.permed_account or not self.permed_account.actions:
|
||||||
msg = 'user `{}` not has asset `{}` permission for login `{}`'.format(
|
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)
|
raise PermissionDenied(msg)
|
||||||
|
|
||||||
|
@ -122,20 +124,22 @@ class ConnectionToken(OrgModelMixin, JMSBaseModel):
|
||||||
if not self.asset:
|
if not self.asset:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
account = self.asset.accounts.filter(name=self.login).first()
|
account = self.asset.accounts.filter(name=self.account_name).first()
|
||||||
if self.login == '@INPUT' or not account:
|
if self.account_name == '@INPUT' or not account:
|
||||||
return {
|
return {
|
||||||
'name': self.login,
|
'name': self.account_name,
|
||||||
'username': self.username,
|
'username': self.input_username,
|
||||||
'secret_type': 'password',
|
'secret_type': 'password',
|
||||||
'secret': self.secret
|
'secret': self.input_secret,
|
||||||
|
'su_from': None
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
return {
|
return {
|
||||||
'name': account.name,
|
'name': account.name,
|
||||||
'username': account.username,
|
'username': account.username,
|
||||||
'secret_type': account.secret_type,
|
'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
|
@lazyproperty
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from assets.serializers import PlatformSerializer
|
from assets.models import Asset, CommandFilterRule, Account, Platform
|
||||||
from assets.models import Asset, Domain, CommandFilterRule, Account, Platform
|
from assets.serializers import PlatformSerializer, AssetProtocolsSerializer
|
||||||
from authentication.models import ConnectionToken
|
from authentication.models import ConnectionToken
|
||||||
from orgs.mixins.serializers import OrgResourceModelSerializerMixin
|
from orgs.mixins.serializers import OrgResourceModelSerializerMixin
|
||||||
from perms.serializers.permission import ActionChoicesField
|
from perms.serializers.permission import ActionChoicesField
|
||||||
|
@ -15,28 +15,28 @@ __all__ = [
|
||||||
|
|
||||||
|
|
||||||
class ConnectionTokenSerializer(OrgResourceModelSerializerMixin):
|
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'))
|
expire_time = serializers.IntegerField(read_only=True, label=_('Expired time'))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ConnectionToken
|
model = ConnectionToken
|
||||||
fields_mini = ['id']
|
fields_mini = ['id', 'value']
|
||||||
fields_small = fields_mini + [
|
fields_small = fields_mini + [
|
||||||
'protocol', 'login', 'secret', 'username',
|
'user', 'asset', 'account_name',
|
||||||
|
'input_username', 'input_secret',
|
||||||
|
'connect_method', 'protocol',
|
||||||
'actions', 'date_expired', 'date_created',
|
'actions', 'date_expired', 'date_created',
|
||||||
'date_updated', 'created_by',
|
'date_updated', 'created_by',
|
||||||
'updated_by', 'org_id', 'org_name',
|
'updated_by', 'org_id', 'org_name',
|
||||||
]
|
]
|
||||||
fields_fk = [
|
|
||||||
'user', 'asset',
|
|
||||||
]
|
|
||||||
read_only_fields = [
|
read_only_fields = [
|
||||||
# 普通 Token 不支持指定 user
|
# 普通 Token 不支持指定 user
|
||||||
'user', 'expire_time',
|
'user', 'expire_time',
|
||||||
'user_display', 'asset_display',
|
'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):
|
def get_request_user(self):
|
||||||
request = self.context.get('request')
|
request = self.context.get('request')
|
||||||
|
@ -85,19 +85,30 @@ class ConnectionTokenUserSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
class ConnectionTokenAssetSerializer(serializers.ModelSerializer):
|
class ConnectionTokenAssetSerializer(serializers.ModelSerializer):
|
||||||
""" Asset """
|
""" Asset """
|
||||||
|
protocols = AssetProtocolsSerializer(many=True, required=False, label=_('Protocols'))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Asset
|
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 """
|
""" 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:
|
class Meta:
|
||||||
model = Account
|
model = Account
|
||||||
fields = [
|
fields = [
|
||||||
'name', 'username', 'secret_type', 'secret',
|
'name', 'username', 'secret_type', 'secret', 'su_from',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -106,16 +117,10 @@ class ConnectionTokenGatewaySerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Asset
|
model = Asset
|
||||||
fields = ['id', 'address', 'port', 'username', 'password', 'private_key']
|
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']
|
|
||||||
|
|
||||||
|
|
||||||
class ConnectionTokenCmdFilterRuleSerializer(serializers.ModelSerializer):
|
class ConnectionTokenCmdFilterRuleSerializer(serializers.ModelSerializer):
|
||||||
|
@ -140,11 +145,12 @@ class ConnectionTokenPlatform(PlatformSerializer):
|
||||||
|
|
||||||
|
|
||||||
class ConnectionTokenSecretSerializer(OrgResourceModelSerializerMixin):
|
class ConnectionTokenSecretSerializer(OrgResourceModelSerializerMixin):
|
||||||
|
expire_now = serializers.BooleanField(label=_('Expired now'), default=True)
|
||||||
user = ConnectionTokenUserSerializer(read_only=True)
|
user = ConnectionTokenUserSerializer(read_only=True)
|
||||||
asset = ConnectionTokenAssetSerializer(read_only=True)
|
asset = ConnectionTokenAssetSerializer(read_only=True)
|
||||||
platform = ConnectionTokenPlatform(read_only=True)
|
|
||||||
account = ConnectionTokenAccountSerializer(read_only=True)
|
account = ConnectionTokenAccountSerializer(read_only=True)
|
||||||
gateway = ConnectionTokenGatewaySerializer(read_only=True)
|
gateway = ConnectionTokenGatewaySerializer(read_only=True)
|
||||||
|
platform = ConnectionTokenPlatform(read_only=True)
|
||||||
# cmd_filter_rules = ConnectionTokenCmdFilterRuleSerializer(many=True)
|
# cmd_filter_rules = ConnectionTokenCmdFilterRuleSerializer(many=True)
|
||||||
actions = ActionChoicesField()
|
actions = ActionChoicesField()
|
||||||
expire_at = serializers.IntegerField()
|
expire_at = serializers.IntegerField()
|
||||||
|
@ -152,7 +158,10 @@ class ConnectionTokenSecretSerializer(OrgResourceModelSerializerMixin):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ConnectionToken
|
model = ConnectionToken
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'secret', 'user', 'asset', 'account',
|
'id', 'value', 'user', 'asset', 'account', 'platform',
|
||||||
'protocol', 'domain', 'gateway',
|
'protocol', 'gateway', 'actions', 'expire_at', 'expire_now',
|
||||||
'actions', 'expire_at', 'platform',
|
|
||||||
]
|
]
|
||||||
|
extra_kwargs = {
|
||||||
|
'value': {'read_only': True},
|
||||||
|
'expire_now': {'write_only': True},
|
||||||
|
}
|
||||||
|
|
|
@ -6,7 +6,6 @@ from .hands import *
|
||||||
|
|
||||||
class Services(TextChoices):
|
class Services(TextChoices):
|
||||||
gunicorn = 'gunicorn', 'gunicorn'
|
gunicorn = 'gunicorn', 'gunicorn'
|
||||||
daphne = 'daphne', 'daphne'
|
|
||||||
celery_ansible = 'celery_ansible', 'celery_ansible'
|
celery_ansible = 'celery_ansible', 'celery_ansible'
|
||||||
celery_default = 'celery_default', 'celery_default'
|
celery_default = 'celery_default', 'celery_default'
|
||||||
beat = 'beat', 'beat'
|
beat = 'beat', 'beat'
|
||||||
|
@ -22,7 +21,6 @@ class Services(TextChoices):
|
||||||
from . import services
|
from . import services
|
||||||
services_map = {
|
services_map = {
|
||||||
cls.gunicorn.value: services.GunicornService,
|
cls.gunicorn.value: services.GunicornService,
|
||||||
cls.daphne: services.DaphneService,
|
|
||||||
cls.flower: services.FlowerService,
|
cls.flower: services.FlowerService,
|
||||||
cls.celery_default: services.CeleryDefaultService,
|
cls.celery_default: services.CeleryDefaultService,
|
||||||
cls.celery_ansible: services.CeleryAnsibleService,
|
cls.celery_ansible: services.CeleryAnsibleService,
|
||||||
|
@ -30,13 +28,9 @@ class Services(TextChoices):
|
||||||
}
|
}
|
||||||
return services_map.get(name)
|
return services_map.get(name)
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def ws_services(cls):
|
|
||||||
return [cls.daphne]
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def web_services(cls):
|
def web_services(cls):
|
||||||
return [cls.gunicorn, cls.daphne, cls.flower]
|
return [cls.gunicorn, cls.flower]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def celery_services(cls):
|
def celery_services(cls):
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
from .beat import *
|
from .beat import *
|
||||||
from .celery_ansible import *
|
from .celery_ansible import *
|
||||||
from .celery_default import *
|
from .celery_default import *
|
||||||
from .daphne import *
|
|
||||||
from .flower import *
|
from .flower import *
|
||||||
from .gunicorn 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 '
|
log_format = '%(h)s %(t)s %(L)ss "%(r)s" %(s)s %(b)s '
|
||||||
bind = f'{HTTP_HOST}:{HTTP_PORT}'
|
bind = f'{HTTP_HOST}:{HTTP_PORT}'
|
||||||
cmd = [
|
cmd = [
|
||||||
'gunicorn', 'jumpserver.wsgi',
|
'gunicorn', 'jumpserver.asgi:application',
|
||||||
'-b', bind,
|
'-b', bind,
|
||||||
'-k', 'gthread',
|
'-k', 'uvicorn.workers.UvicornWorker',
|
||||||
'--threads', '10',
|
'--threads', '10',
|
||||||
'-w', str(self.worker),
|
'-w', str(self.worker),
|
||||||
'--max-requests', '4096',
|
'--max-requests', '4096',
|
||||||
|
|
|
@ -2,11 +2,11 @@
|
||||||
#
|
#
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from django.shortcuts import reverse as dj_reverse
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils import timezone
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models.signals import post_save, pre_save
|
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}')
|
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:
|
for i in items:
|
||||||
post_save.send(sender=cls, instance=i, created=True)
|
post_save.send(sender=cls, instance=i, created=True)
|
||||||
return result
|
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 -*-
|
# -*- coding: utf-8 -*-
|
||||||
#
|
#
|
||||||
import re
|
|
||||||
import json
|
|
||||||
from six import string_types
|
|
||||||
import base64
|
import base64
|
||||||
import os
|
|
||||||
import time
|
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import time
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
from itertools import chain
|
|
||||||
|
|
||||||
import paramiko
|
import paramiko
|
||||||
import sshpubkeys
|
import sshpubkeys
|
||||||
|
from cryptography.hazmat.primitives import serialization
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.serializers.json import DjangoJSONEncoder
|
||||||
from itsdangerous import (
|
from itsdangerous import (
|
||||||
TimedJSONWebSignatureSerializer, JSONWebSignatureSerializer,
|
TimedJSONWebSignatureSerializer, JSONWebSignatureSerializer,
|
||||||
BadSignature, SignatureExpired
|
BadSignature, SignatureExpired
|
||||||
)
|
)
|
||||||
from django.conf import settings
|
from six import string_types
|
||||||
from django.core.serializers.json import DjangoJSONEncoder
|
|
||||||
from django.db.models.fields.files import FileField
|
|
||||||
|
|
||||||
from .http import http_date
|
from .http import http_date
|
||||||
|
|
||||||
|
@ -69,22 +68,19 @@ class Signer(metaclass=Singleton):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
_supported_paramiko_ssh_key_types = (paramiko.RSAKey, paramiko.DSSKey, paramiko.Ed25519Key)
|
||||||
|
|
||||||
|
|
||||||
def ssh_key_string_to_obj(text, password=None):
|
def ssh_key_string_to_obj(text, password=None):
|
||||||
key = None
|
key = None
|
||||||
|
for ssh_key_type in _supported_paramiko_ssh_key_types:
|
||||||
|
if not isinstance(ssh_key_type, paramiko.PKey):
|
||||||
|
continue
|
||||||
try:
|
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:
|
except paramiko.SSHException:
|
||||||
pass
|
pass
|
||||||
else:
|
|
||||||
return key
|
|
||||||
|
|
||||||
try:
|
|
||||||
key = paramiko.DSSKey.from_private_key(StringIO(text), password=password)
|
|
||||||
except paramiko.SSHException:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
return key
|
|
||||||
|
|
||||||
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):
|
def validate_ssh_private_key(text, password=None):
|
||||||
if isinstance(text, bytes):
|
if isinstance(text, str):
|
||||||
try:
|
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:
|
except UnicodeDecodeError:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
key = ssh_key_string_to_obj(text, password=password)
|
key = parse_ssh_private_key_str(text, password=password)
|
||||||
if key is None:
|
return bool(key)
|
||||||
return False
|
|
||||||
else:
|
|
||||||
return True
|
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):
|
def validate_ssh_public_key(text):
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
#
|
#
|
||||||
import time
|
|
||||||
from email.utils import formatdate
|
|
||||||
import calendar
|
import calendar
|
||||||
import threading
|
import threading
|
||||||
|
import time
|
||||||
|
from email.utils import formatdate
|
||||||
|
|
||||||
_STRPTIME_LOCK = threading.Lock()
|
_STRPTIME_LOCK = threading.Lock()
|
||||||
|
|
||||||
|
@ -35,3 +35,6 @@ def http_to_unixtime(time_string):
|
||||||
def iso8601_to_unixtime(time_string):
|
def iso8601_to_unixtime(time_string):
|
||||||
"""把ISO8601时间字符串(形如,2012-02-24T06:07:48.000Z)转换为UNIX时间,精确到秒。"""
|
"""把ISO8601时间字符串(形如,2012-02-24T06:07:48.000Z)转换为UNIX时间,精确到秒。"""
|
||||||
return to_unixtime(time_string, _ISO8601_FORMAT)
|
return to_unixtime(time_string, _ISO8601_FORMAT)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -66,7 +66,7 @@ def contains_ip(ip, ip_group):
|
||||||
if in_ip_segment(ip, _ip):
|
if in_ip_segment(ip, _ip):
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
# is domain name
|
# address / host
|
||||||
if ip == _ip:
|
if ip == _ip:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
|
@ -1,22 +1,21 @@
|
||||||
import datetime
|
|
||||||
|
|
||||||
import pytz
|
import pytz
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
from django.utils import timezone as dj_timezone
|
from django.utils import timezone as dj_timezone
|
||||||
from rest_framework.fields import DateTimeField
|
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)
|
assert dj_timezone.is_aware(dt)
|
||||||
return tzinfo.normalize(dt.astimezone(tzinfo))
|
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'))
|
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())
|
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)
|
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()
|
_rest_dt_field = DateTimeField()
|
||||||
dt_parser = _rest_dt_field.to_internal_value
|
dt_parser = _rest_dt_field.to_internal_value
|
||||||
dt_formatter = _rest_dt_field.to_representation
|
dt_formatter = _rest_dt_field.to_representation
|
||||||
|
|
|
@ -3,51 +3,131 @@ import time
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.timesince import timesince
|
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 django.http.response import JsonResponse, HttpResponse
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
from rest_framework.permissions import AllowAny
|
from rest_framework.permissions import AllowAny
|
||||||
from collections import Counter
|
from rest_framework.request import Request
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
from users.models import User
|
from users.models import User
|
||||||
from assets.models import Asset
|
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 terminal.utils import ComponentsPrometheusMetricsUtil
|
||||||
from orgs.utils import current_org
|
from orgs.utils import current_org
|
||||||
from common.utils import lazyproperty
|
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
|
from orgs.caches import OrgResourceStatisticsCache
|
||||||
|
|
||||||
|
|
||||||
__all__ = ['IndexApi']
|
__all__ = ['IndexApi']
|
||||||
|
|
||||||
|
|
||||||
class DatesLoginMetricMixin:
|
class DateTimeMixin:
|
||||||
|
request: Request
|
||||||
|
|
||||||
|
@property
|
||||||
|
def org(self):
|
||||||
|
return current_org
|
||||||
|
|
||||||
@lazyproperty
|
@lazyproperty
|
||||||
def days(self):
|
def days(self):
|
||||||
query_params = self.request.query_params
|
query_params = self.request.query_params
|
||||||
if query_params.get('monthly'):
|
count = query_params.get('days')
|
||||||
return 30
|
count = int(count) if count else 0
|
||||||
return 7
|
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
|
@lazyproperty
|
||||||
def sessions_queryset(self):
|
def dates_list(self):
|
||||||
days = timezone.now() - timezone.timedelta(days=self.days)
|
now = local_now()
|
||||||
sessions_queryset = Session.objects.filter(date_start__gt=days)
|
|
||||||
return sessions_queryset
|
|
||||||
|
|
||||||
@lazyproperty
|
|
||||||
def session_dates_list(self):
|
|
||||||
now = timezone.now()
|
|
||||||
dates = [(now - timezone.timedelta(days=i)).date() for i in range(self.days)]
|
dates = [(now - timezone.timedelta(days=i)).date() for i in range(self.days)]
|
||||||
dates.reverse()
|
dates.reverse()
|
||||||
# dates = self.sessions_queryset.dates('date_start', 'day')
|
|
||||||
return dates
|
return dates
|
||||||
|
|
||||||
def get_dates_metrics_date(self):
|
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
|
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
|
@staticmethod
|
||||||
def get_cache_key(date, tp):
|
def get_cache_key(date, tp):
|
||||||
date_str = date.strftime("%Y%m%d")
|
date_str = date.strftime("%Y%m%d")
|
||||||
|
@ -86,7 +166,7 @@ class DatesLoginMetricMixin:
|
||||||
|
|
||||||
def get_dates_metrics_total_count_login(self):
|
def get_dates_metrics_total_count_login(self):
|
||||||
data = []
|
data = []
|
||||||
for d in self.session_dates_list:
|
for d in self.dates_list:
|
||||||
count = self.get_date_login_count(d)
|
count = self.get_date_login_count(d)
|
||||||
data.append(count)
|
data.append(count)
|
||||||
if len(data) == 0:
|
if len(data) == 0:
|
||||||
|
@ -105,7 +185,7 @@ class DatesLoginMetricMixin:
|
||||||
|
|
||||||
def get_dates_metrics_total_count_active_users(self):
|
def get_dates_metrics_total_count_active_users(self):
|
||||||
data = []
|
data = []
|
||||||
for d in self.session_dates_list:
|
for d in self.dates_list:
|
||||||
count = self.get_date_user_count(d)
|
count = self.get_date_user_count(d)
|
||||||
data.append(count)
|
data.append(count)
|
||||||
return data
|
return data
|
||||||
|
@ -122,80 +202,61 @@ class DatesLoginMetricMixin:
|
||||||
|
|
||||||
def get_dates_metrics_total_count_active_assets(self):
|
def get_dates_metrics_total_count_active_assets(self):
|
||||||
data = []
|
data = []
|
||||||
for d in self.session_dates_list:
|
for d in self.dates_list:
|
||||||
count = self.get_date_asset_count(d)
|
count = self.get_date_asset_count(d)
|
||||||
data.append(count)
|
data.append(count)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
@lazyproperty
|
def get_date_session_count(self, date):
|
||||||
def dates_total_count_active_users(self):
|
tp = "SESSION"
|
||||||
count = len(set(self.sessions_queryset.values_list('user_id', flat=True)))
|
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
|
return count
|
||||||
|
|
||||||
@lazyproperty
|
def get_dates_metrics_total_count_sessions(self):
|
||||||
def dates_total_count_inactive_users(self):
|
data = []
|
||||||
total = current_org.get_members().count()
|
for d in self.dates_list:
|
||||||
active = self.dates_total_count_active_users
|
count = self.get_date_session_count(d)
|
||||||
count = total - active
|
data.append(count)
|
||||||
if count < 0:
|
return data
|
||||||
count = 0
|
|
||||||
return count
|
|
||||||
|
|
||||||
@lazyproperty
|
@lazyproperty
|
||||||
def dates_total_count_disabled_users(self):
|
def get_type_to_assets(self):
|
||||||
return current_org.get_members().filter(is_active=False).count()
|
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 get_dates_login_times_assets(self):
|
||||||
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):
|
|
||||||
assets = self.sessions_queryset.values("asset") \
|
assets = self.sessions_queryset.values("asset") \
|
||||||
.annotate(total=Count("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:
|
for asset in assets:
|
||||||
asset['last'] = str(asset['last'])
|
asset['last'] = str(asset['last'])
|
||||||
return list(assets)
|
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") \
|
users = self.sessions_queryset.values("user_id") \
|
||||||
.annotate(total=Count("user_id")) \
|
.annotate(total=Count("user_id")) \
|
||||||
.annotate(user=Max('user')) \
|
.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:
|
for user in users:
|
||||||
user['last'] = str(user['last'])
|
user['last'] = str(user['last'])
|
||||||
return list(users)
|
return list(users)
|
||||||
|
|
||||||
def get_dates_login_record_top10_sessions(self):
|
def get_dates_login_record_sessions(self):
|
||||||
sessions = self.sessions_queryset.order_by('-date_start')[:10]
|
sessions = self.sessions_queryset.order_by('-date_start')
|
||||||
|
sessions = sessions[:10]
|
||||||
for session in sessions:
|
for session in sessions:
|
||||||
session.avatar_url = User.get_avatar_url("")
|
session.avatar_url = User.get_avatar_url("")
|
||||||
sessions = [
|
sessions = [
|
||||||
|
@ -210,8 +271,44 @@ class DatesLoginMetricMixin:
|
||||||
]
|
]
|
||||||
return sessions
|
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']
|
http_method_names = ['get']
|
||||||
|
|
||||||
def check_permissions(self, request):
|
def check_permissions(self, request):
|
||||||
|
@ -222,7 +319,7 @@ class IndexApi(DatesLoginMetricMixin, APIView):
|
||||||
|
|
||||||
query_params = self.request.query_params
|
query_params = self.request.query_params
|
||||||
|
|
||||||
caches = OrgResourceStatisticsCache(current_org)
|
caches = OrgResourceStatisticsCache(self.org)
|
||||||
|
|
||||||
_all = query_params.get('all')
|
_all = query_params.get('all')
|
||||||
|
|
||||||
|
@ -236,6 +333,26 @@ class IndexApi(DatesLoginMetricMixin, APIView):
|
||||||
'total_count_assets': caches.assets_amount,
|
'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'):
|
if _all or query_params.get('total_count') or query_params.get('total_count_online_users'):
|
||||||
data.update({
|
data.update({
|
||||||
'total_count_online_users': caches.total_count_online_users,
|
'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,
|
'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'):
|
if _all or query_params.get('dates_metrics'):
|
||||||
data.update({
|
data.update({
|
||||||
'dates_metrics_date': self.get_dates_metrics_date(),
|
'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(),
|
'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'):
|
if _all or query_params.get('dates_login_times_top10_assets'):
|
||||||
data.update({
|
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'):
|
if _all or query_params.get('dates_login_times_top10_users'):
|
||||||
data.update({
|
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'):
|
if _all or query_params.get('dates_login_record_top10_sessions'):
|
||||||
data.update({
|
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)
|
return JsonResponse(data, status=200)
|
||||||
|
@ -353,4 +501,3 @@ class PrometheusMetricsApi(HealthApiMixin):
|
||||||
util = ComponentsPrometheusMetricsUtil()
|
util = ComponentsPrometheusMetricsUtil()
|
||||||
metrics_text = util.get_prometheus_metrics_text()
|
metrics_text = util.get_prometheus_metrics_text()
|
||||||
return HttpResponse(metrics_text, content_type='text/plain; version=0.0.4; charset=utf-8')
|
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
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:adfa9c01178d5f6490e616f62d41c71974d42f9e3bd078fcf1b3c7124384df0b
|
oid sha256:4a5338177d87680e0030c77f187a06664136d5dea63c8dffc43fa686091f2da4
|
||||||
size 117024
|
size 117102
|
||||||
|
|
|
@ -603,14 +603,10 @@ msgid "All"
|
||||||
msgstr "すべて"
|
msgstr "すべて"
|
||||||
|
|
||||||
#: assets/models/account.py:46
|
#: assets/models/account.py:46
|
||||||
#, fuzzy
|
|
||||||
#| msgid "Manually input"
|
|
||||||
msgid "Manual input"
|
msgid "Manual input"
|
||||||
msgstr "手動入力"
|
msgstr "手動入力"
|
||||||
|
|
||||||
#: assets/models/account.py:47
|
#: assets/models/account.py:47
|
||||||
#, fuzzy
|
|
||||||
#| msgid "Dynamic code"
|
|
||||||
msgid "Dynamic user"
|
msgid "Dynamic user"
|
||||||
msgstr "動的コード"
|
msgstr "動的コード"
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:eeaa813f4ea052a1cd85b8ae5addfde6b088fd21a0261f8724d62823835512a2
|
oid sha256:30ae571e06eb7d2f0fee70013a812ea3bdb8e14715e1a1f4eb5e2c92311034f8
|
||||||
size 104043
|
size 104086
|
||||||
|
|
|
@ -578,16 +578,12 @@ msgid "All"
|
||||||
msgstr "全部"
|
msgstr "全部"
|
||||||
|
|
||||||
#: assets/models/account.py:46
|
#: assets/models/account.py:46
|
||||||
#, fuzzy
|
|
||||||
#| msgid "Manually input"
|
|
||||||
msgid "Manual input"
|
msgid "Manual input"
|
||||||
msgstr "手动输入"
|
msgstr "手动输入"
|
||||||
|
|
||||||
#: assets/models/account.py:47
|
#: assets/models/account.py:47
|
||||||
#, fuzzy
|
|
||||||
#| msgid "Dynamic code"
|
|
||||||
msgid "Dynamic user"
|
msgid "Dynamic user"
|
||||||
msgstr "动态码"
|
msgstr "动态用户"
|
||||||
|
|
||||||
#: assets/models/account.py:55
|
#: assets/models/account.py:55
|
||||||
msgid "Su from"
|
msgid "Su from"
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
#
|
#
|
||||||
|
|
||||||
from rest_framework import viewsets
|
from rest_framework import viewsets
|
||||||
|
|
||||||
|
from orgs.mixins.api import OrgBulkModelViewSet
|
||||||
from ..models import AdHoc
|
from ..models import AdHoc
|
||||||
from ..serializers import (
|
from ..serializers import (
|
||||||
AdHocSerializer
|
AdHocSerializer
|
||||||
|
@ -12,6 +14,7 @@ __all__ = [
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class AdHocViewSet(viewsets.ModelViewSet):
|
class AdHocViewSet(OrgBulkModelViewSet):
|
||||||
queryset = AdHoc.objects.all()
|
|
||||||
serializer_class = AdHocSerializer
|
serializer_class = AdHocSerializer
|
||||||
|
permission_classes = ()
|
||||||
|
model = AdHoc
|
||||||
|
|
|
@ -5,10 +5,16 @@ from ops.serializers.job import JobSerializer, JobExecutionSerializer
|
||||||
|
|
||||||
__all__ = ['JobViewSet', 'JobExecutionViewSet']
|
__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
|
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):
|
class JobViewSet(OrgBulkModelViewSet):
|
||||||
serializer_class = JobSerializer
|
serializer_class = JobSerializer
|
||||||
model = Job
|
model = Job
|
||||||
|
@ -16,28 +22,32 @@ class JobViewSet(OrgBulkModelViewSet):
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
query_set = super().get_queryset()
|
query_set = super().get_queryset()
|
||||||
|
if self.action != 'retrieve':
|
||||||
return query_set.filter(instant=False)
|
return query_set.filter(instant=False)
|
||||||
|
return query_set
|
||||||
|
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
instance = serializer.save()
|
instance = serializer.save()
|
||||||
if instance.instant:
|
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):
|
class JobExecutionViewSet(OrgBulkModelViewSet):
|
||||||
serializer_class = JobExecutionSerializer
|
serializer_class = JobExecutionSerializer
|
||||||
http_method_names = ('get', 'post', 'head', 'options',)
|
http_method_names = ('get', 'post', 'head', 'options',)
|
||||||
# filter_fields = ('type',)
|
|
||||||
permission_classes = ()
|
permission_classes = ()
|
||||||
model = JobExecution
|
model = JobExecution
|
||||||
|
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
instance = serializer.save()
|
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):
|
def get_queryset(self):
|
||||||
query_set = super().get_queryset()
|
query_set = super().get_queryset()
|
||||||
job_id = self.request.query_params.get('job_id')
|
job_id = self.request.query_params.get('job_id')
|
||||||
if 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
|
return query_set
|
||||||
|
|
|
@ -2,7 +2,8 @@ import os
|
||||||
import zipfile
|
import zipfile
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from rest_framework import viewsets
|
|
||||||
|
from orgs.mixins.api import OrgBulkModelViewSet
|
||||||
from ..models import Playbook
|
from ..models import Playbook
|
||||||
from ..serializers.playbook import PlaybookSerializer
|
from ..serializers.playbook import PlaybookSerializer
|
||||||
|
|
||||||
|
@ -15,9 +16,10 @@ def unzip_playbook(src, dist):
|
||||||
fz.extract(file, dist)
|
fz.extract(file, dist)
|
||||||
|
|
||||||
|
|
||||||
class PlaybookViewSet(viewsets.ModelViewSet):
|
class PlaybookViewSet(OrgBulkModelViewSet):
|
||||||
queryset = Playbook.objects.all()
|
|
||||||
serializer_class = PlaybookSerializer
|
serializer_class = PlaybookSerializer
|
||||||
|
permission_classes = ()
|
||||||
|
model = Playbook
|
||||||
|
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
instance = serializer.save()
|
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 ~*~
|
# ~*~ coding: utf-8 ~*~
|
||||||
import os.path
|
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from common.db.models import BaseCreateUpdateModel
|
|
||||||
from common.utils import get_logger
|
from common.utils import get_logger
|
||||||
from .base import BaseAnsibleJob, BaseAnsibleExecution
|
from orgs.mixins.models import JMSOrgBaseModel
|
||||||
from ..ansible import AdHocRunner
|
|
||||||
|
|
||||||
__all__ = ["AdHoc", "AdHocExecution"]
|
__all__ = ["AdHoc"]
|
||||||
|
|
||||||
logger = get_logger(__file__)
|
logger = get_logger(__file__)
|
||||||
|
|
||||||
|
|
||||||
class AdHoc(BaseCreateUpdateModel):
|
class AdHoc(JMSOrgBaseModel):
|
||||||
class Modules(models.TextChoices):
|
class Modules(models.TextChoices):
|
||||||
shell = 'shell', _('Shell')
|
shell = 'shell', _('Shell')
|
||||||
winshell = 'win_shell', _('Powershell')
|
winshell = 'win_shell', _('Powershell')
|
||||||
|
@ -26,7 +23,9 @@ class AdHoc(BaseCreateUpdateModel):
|
||||||
module = models.CharField(max_length=128, choices=Modules.choices, default=Modules.shell,
|
module = models.CharField(max_length=128, choices=Modules.choices, default=Modules.shell,
|
||||||
verbose_name=_('Module'))
|
verbose_name=_('Module'))
|
||||||
args = models.CharField(max_length=1024, default='', verbose_name=_('Args'))
|
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
|
@property
|
||||||
def row_count(self):
|
def row_count(self):
|
||||||
|
@ -41,28 +40,3 @@ class AdHoc(BaseCreateUpdateModel):
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "{}: {}".format(self.module, self.args)
|
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 = models.CharField(max_length=128, default='root', verbose_name=_('Runas'))
|
||||||
runas_policy = models.CharField(max_length=128, choices=RunasPolicies.choices, default=RunasPolicies.skip,
|
runas_policy = models.CharField(max_length=128, choices=RunasPolicies.choices, default=RunasPolicies.skip,
|
||||||
verbose_name=_('Runas policy'))
|
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'))
|
parameters_define = models.JSONField(default=dict, verbose_name=_('Parameters define'))
|
||||||
comment = models.CharField(max_length=1024, default='', verbose_name=_('Comment'), null=True, blank=True)
|
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
|
return total_cost / finished_count if finished_count else 0
|
||||||
|
|
||||||
def get_register_task(self):
|
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])
|
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),)
|
args = (str(self.id),)
|
||||||
kwargs = {}
|
kwargs = {}
|
||||||
return name, task, args, kwargs
|
return name, task, args, kwargs
|
||||||
|
@ -91,6 +92,9 @@ class Job(JMSOrgBaseModel, PeriodTaskModelMixin):
|
||||||
def create_execution(self):
|
def create_execution(self):
|
||||||
return self.executions.create()
|
return self.executions.create()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['date_created']
|
||||||
|
|
||||||
|
|
||||||
class JobExecution(JMSOrgBaseModel):
|
class JobExecution(JMSOrgBaseModel):
|
||||||
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
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_start = models.DateTimeField(null=True, verbose_name=_('Date start'), db_index=True)
|
||||||
date_finished = models.DateTimeField(null=True, verbose_name=_("Date finished"))
|
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):
|
def get_runner(self):
|
||||||
inv = self.job.inventory
|
inv = self.job.inventory
|
||||||
inv.write_to_file(self.inventory_path)
|
inv.write_to_file(self.inventory_path)
|
||||||
|
@ -114,8 +129,9 @@ class JobExecution(JMSOrgBaseModel):
|
||||||
extra_vars = {}
|
extra_vars = {}
|
||||||
|
|
||||||
if self.job.type == 'adhoc':
|
if self.job.type == 'adhoc':
|
||||||
|
args = self.compile_shell()
|
||||||
runner = AdHocRunner(
|
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,
|
pattern="all", project_dir=self.private_dir, extra_vars=extra_vars,
|
||||||
)
|
)
|
||||||
elif self.job.type == 'playbook':
|
elif self.job.type == 'playbook':
|
||||||
|
@ -198,3 +214,6 @@ class JobExecution(JMSOrgBaseModel):
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(e, exc_info=True)
|
logging.error(e, exc_info=True)
|
||||||
self.set_error(e)
|
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.db import models
|
||||||
from django.utils.translation import gettext_lazy as _
|
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)
|
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||||
name = models.CharField(max_length=128, verbose_name=_('Name'), null=True)
|
name = models.CharField(max_length=128, verbose_name=_('Name'), null=True)
|
||||||
path = models.FileField(upload_to='playbooks/')
|
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
|
@property
|
||||||
def work_path(self):
|
def work_path(self):
|
||||||
|
|
|
@ -1,75 +1,19 @@
|
||||||
# ~*~ coding: utf-8 ~*~
|
# ~*~ coding: utf-8 ~*~
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import datetime
|
|
||||||
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from common.drf.fields import ReadableHiddenField
|
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):
|
class AdHocSerializer(BulkOrgResourceModelSerializer, serializers.ModelSerializer):
|
||||||
owner = ReadableHiddenField(default=serializers.CurrentUserDefault())
|
creator = ReadableHiddenField(default=serializers.CurrentUserDefault())
|
||||||
row_count = serializers.IntegerField(read_only=True)
|
row_count = serializers.IntegerField(read_only=True)
|
||||||
size = serializers.IntegerField(read_only=True)
|
size = serializers.IntegerField(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = AdHoc
|
model = AdHoc
|
||||||
fields = ["id", "name", "module", "row_count", "size", "args", "owner", "date_created", "date_updated"]
|
read_only_field = ["id", "row_count", "size", "creator", "date_created", "date_updated"]
|
||||||
|
fields = read_only_field + ["id", "name", "module", "args", "comment"]
|
||||||
|
|
||||||
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'
|
|
||||||
)
|
|
||||||
|
|
|
@ -15,6 +15,7 @@ class JobSerializer(BulkOrgResourceModelSerializer, PeriodTaskSerializerMixin):
|
||||||
read_only_fields = ["id", "date_last_run", "date_created", "date_updated", "average_time_cost"]
|
read_only_fields = ["id", "date_last_run", "date_created", "date_updated", "average_time_cost"]
|
||||||
fields = read_only_fields + [
|
fields = read_only_fields + [
|
||||||
"name", "instant", "type", "module", "args", "playbook", "assets", "runas_policy", "runas", "owner",
|
"name", "instant", "type", "module", "args", "playbook", "assets", "runas_policy", "runas", "owner",
|
||||||
|
"use_parameter_define",
|
||||||
"parameters_define",
|
"parameters_define",
|
||||||
"timeout",
|
"timeout",
|
||||||
"chdir",
|
"chdir",
|
||||||
|
@ -28,7 +29,7 @@ class JobExecutionSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = JobExecution
|
model = JobExecution
|
||||||
read_only_fields = ["id", "task_id", "timedelta", "time_cost", 'is_finished', 'date_start', 'date_created',
|
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 + [
|
fields = read_only_fields + [
|
||||||
"job", "parameters"
|
"job", "parameters"
|
||||||
]
|
]
|
||||||
|
|
|
@ -4,6 +4,7 @@ from rest_framework import serializers
|
||||||
|
|
||||||
from common.drf.fields import ReadableHiddenField
|
from common.drf.fields import ReadableHiddenField
|
||||||
from ops.models import Playbook
|
from ops.models import Playbook
|
||||||
|
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||||
|
|
||||||
|
|
||||||
def parse_playbook_name(path):
|
def parse_playbook_name(path):
|
||||||
|
@ -11,8 +12,8 @@ def parse_playbook_name(path):
|
||||||
return file_name.split(".")[-2]
|
return file_name.split(".")[-2]
|
||||||
|
|
||||||
|
|
||||||
class PlaybookSerializer(serializers.ModelSerializer):
|
class PlaybookSerializer(BulkOrgResourceModelSerializer, serializers.ModelSerializer):
|
||||||
owner = ReadableHiddenField(default=serializers.CurrentUserDefault())
|
creator = ReadableHiddenField(default=serializers.CurrentUserDefault())
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
name = validated_data.get('name')
|
name = validated_data.get('name')
|
||||||
|
@ -23,6 +24,7 @@ class PlaybookSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Playbook
|
model = Playbook
|
||||||
fields = [
|
read_only_fields = ["id", "date_created", "date_updated"]
|
||||||
"id", "name", "path", "date_created", "owner", "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)
|
job = get_object_or_none(Job, id=job_id)
|
||||||
with tmp_to_org(job.org):
|
with tmp_to_org(job.org):
|
||||||
execution = job.create_execution()
|
execution = job.create_execution()
|
||||||
try:
|
run_ops_job_execution(execution)
|
||||||
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))
|
|
||||||
|
|
||||||
|
|
||||||
@shared_task(soft_time_limit=60, queue="ansible", verbose_name=_("Run ansible task 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)
|
execution = get_object_or_none(JobExecution, id=execution_id)
|
||||||
with tmp_to_org(execution.org):
|
with tmp_to_org(execution.org):
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
from django.db.transaction import on_commit
|
from django.db.transaction import on_commit
|
||||||
|
|
||||||
from orgs.models import Organization
|
from orgs.models import Organization
|
||||||
from orgs.tasks import refresh_org_cache_task
|
from orgs.tasks import refresh_org_cache_task
|
||||||
from orgs.utils import current_org, tmp_to_org
|
from orgs.utils import current_org, tmp_to_org
|
||||||
|
|
||||||
from common.cache import Cache, IntegerField
|
from common.cache import Cache, IntegerField
|
||||||
from common.utils import get_logger
|
from common.utils import get_logger
|
||||||
|
from common.utils.timezone import local_zero_hour, local_monday
|
||||||
from users.models import UserGroup, User
|
from users.models import UserGroup, User
|
||||||
from assets.models import Node, Domain, Asset, Account
|
from assets.models import Node, Domain, Asset, Account
|
||||||
from terminal.models import Session
|
from terminal.models import Session
|
||||||
|
@ -35,30 +36,34 @@ class OrgRelatedCache(Cache):
|
||||||
"""
|
"""
|
||||||
在事务提交之后再发送信号,防止因事务的隔离性导致未获得最新的数据
|
在事务提交之后再发送信号,防止因事务的隔离性导致未获得最新的数据
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def func():
|
def func():
|
||||||
logger.debug(f'CACHE: Send refresh task {self}.{fields}')
|
logger.debug(f'CACHE: Send refresh task {self}.{fields}')
|
||||||
refresh_org_cache_task.delay(self, *fields)
|
refresh_org_cache_task.delay(self, *fields)
|
||||||
|
|
||||||
on_commit(func)
|
on_commit(func)
|
||||||
|
|
||||||
def expire(self, *fields):
|
def expire(self, *fields):
|
||||||
def func():
|
def func():
|
||||||
super(OrgRelatedCache, self).expire(*fields)
|
super(OrgRelatedCache, self).expire(*fields)
|
||||||
|
|
||||||
on_commit(func)
|
on_commit(func)
|
||||||
|
|
||||||
|
|
||||||
class OrgResourceStatisticsCache(OrgRelatedCache):
|
class OrgResourceStatisticsCache(OrgRelatedCache):
|
||||||
users_amount = IntegerField()
|
users_amount = IntegerField()
|
||||||
groups_amount = IntegerField(queryset=UserGroup.objects)
|
|
||||||
|
|
||||||
assets_amount = IntegerField()
|
assets_amount = IntegerField()
|
||||||
|
new_users_amount_this_week = IntegerField()
|
||||||
|
new_assets_amount_this_week = IntegerField()
|
||||||
nodes_amount = IntegerField(queryset=Node.objects)
|
nodes_amount = IntegerField(queryset=Node.objects)
|
||||||
accounts_amount = IntegerField(queryset=Account.objects)
|
|
||||||
domains_amount = IntegerField(queryset=Domain.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)
|
asset_perms_amount = IntegerField(queryset=AssetPermission.objects)
|
||||||
|
|
||||||
total_count_online_users = IntegerField()
|
total_count_online_users = IntegerField()
|
||||||
total_count_online_sessions = IntegerField()
|
total_count_online_sessions = IntegerField()
|
||||||
|
total_count_today_active_assets = IntegerField()
|
||||||
|
total_count_today_failed_sessions = IntegerField()
|
||||||
|
|
||||||
def __init__(self, org):
|
def __init__(self, org):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
@ -70,18 +75,49 @@ class OrgResourceStatisticsCache(OrgRelatedCache):
|
||||||
def get_current_org(self):
|
def get_current_org(self):
|
||||||
return self.org
|
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):
|
def compute_users_amount(self):
|
||||||
amount = User.get_org_users(self.org).count()
|
users = self.get_users()
|
||||||
return amount
|
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):
|
def compute_assets_amount(self):
|
||||||
if self.org.is_root():
|
assets = self.get_assets()
|
||||||
return Asset.objects.all().count()
|
return assets.count()
|
||||||
node = Node.org_root()
|
|
||||||
return node.assets_amount
|
|
||||||
|
|
||||||
def compute_total_count_online_users(self):
|
def compute_new_assets_amount_this_week(self):
|
||||||
return Session.objects.filter(is_finished=False).values_list('user_id').distinct().count()
|
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()
|
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 django.dispatch import receiver
|
||||||
|
|
||||||
from orgs.models import Organization
|
from orgs.models import Organization
|
||||||
from assets.models import Node
|
from assets.models import Node, Account
|
||||||
from perms.models import AssetPermission
|
from perms.models import AssetPermission
|
||||||
|
from audits.models import UserLoginLog
|
||||||
from users.models import UserGroup, User
|
from users.models import UserGroup, User
|
||||||
from users.signals import pre_user_leave_org
|
from users.signals import pre_user_leave_org
|
||||||
from terminal.models import Session
|
from terminal.models import Session
|
||||||
|
@ -74,12 +75,14 @@ def on_user_delete_refresh_cache(sender, instance, **kwargs):
|
||||||
|
|
||||||
class OrgResourceStatisticsRefreshUtil:
|
class OrgResourceStatisticsRefreshUtil:
|
||||||
model_cache_field_mapper = {
|
model_cache_field_mapper = {
|
||||||
AssetPermission: ['asset_perms_amount'],
|
|
||||||
Domain: ['domains_amount'],
|
|
||||||
Node: ['nodes_amount'],
|
Node: ['nodes_amount'],
|
||||||
Asset: ['assets_amount'],
|
Domain: ['domains_amount'],
|
||||||
UserGroup: ['groups_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
|
@classmethod
|
||||||
|
@ -88,7 +91,7 @@ class OrgResourceStatisticsRefreshUtil:
|
||||||
if not cache_field_name:
|
if not cache_field_name:
|
||||||
return
|
return
|
||||||
OrgResourceStatisticsCache(Organization.root()).expire(*cache_field_name)
|
OrgResourceStatisticsCache(Organization.root()).expire(*cache_field_name)
|
||||||
if instance.org:
|
if getattr(instance, 'org', None):
|
||||||
OrgResourceStatisticsCache(instance.org).expire(*cache_field_name)
|
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 common.utils import get_logger
|
||||||
from .mixin import (
|
from .mixin import (
|
||||||
UserAllGrantedAssetsQuerysetMixin, UserDirectGrantedAssetsQuerysetMixin, UserFavoriteGrantedAssetsMixin,
|
AssetsTreeFormatMixin,
|
||||||
UserGrantedNodeAssetsMixin, AssetsSerializerFormatMixin, AssetsTreeFormatMixin,
|
UserGrantedNodeAssetsMixin,
|
||||||
|
AssetSerializerFormatMixin,
|
||||||
|
UserFavoriteGrantedAssetsMixin,
|
||||||
|
UserAllGrantedAssetsQuerysetMixin,
|
||||||
|
UserDirectGrantedAssetsQuerysetMixin,
|
||||||
)
|
)
|
||||||
from ..mixin import AssetRoleAdminMixin, AssetRoleUserMixin
|
from ..mixin import SelfOrPKUserMixin, RebuildTreeMixin
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'UserDirectGrantedAssetsApi', 'MyDirectGrantedAssetsApi',
|
'UserDirectGrantedAssetsApi',
|
||||||
'UserFavoriteGrantedAssetsApi',
|
'UserFavoriteGrantedAssetsApi',
|
||||||
'MyFavoriteGrantedAssetsApi', 'UserDirectGrantedAssetsAsTreeApi',
|
'UserDirectGrantedAssetsAsTreeApi',
|
||||||
'MyUngroupAssetsAsTreeApi',
|
'UserUngroupAssetsAsTreeApi',
|
||||||
'UserAllGrantedAssetsApi', 'MyAllGrantedAssetsApi', 'MyAllAssetsAsTreeApi',
|
'UserAllGrantedAssetsApi',
|
||||||
'UserGrantedNodeAssetsApi',
|
'UserGrantedNodeAssetsApi',
|
||||||
'MyGrantedNodeAssetsApi',
|
|
||||||
]
|
]
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class UserDirectGrantedAssetsApi(
|
class UserDirectGrantedAssetsApi(
|
||||||
AssetRoleAdminMixin, UserDirectGrantedAssetsQuerysetMixin,
|
SelfOrPKUserMixin,
|
||||||
AssetsSerializerFormatMixin, ListAPIView
|
UserDirectGrantedAssetsQuerysetMixin,
|
||||||
|
AssetSerializerFormatMixin,
|
||||||
|
ListAPIView
|
||||||
):
|
):
|
||||||
""" 直接授权给用户的资产 """
|
""" 直接授权给用户的资产 """
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class MyDirectGrantedAssetsApi(AssetRoleUserMixin, UserDirectGrantedAssetsApi):
|
|
||||||
""" 直接授权给我的资产 """
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class UserFavoriteGrantedAssetsApi(
|
class UserFavoriteGrantedAssetsApi(
|
||||||
AssetRoleAdminMixin, UserFavoriteGrantedAssetsMixin,
|
SelfOrPKUserMixin,
|
||||||
AssetsSerializerFormatMixin, ListAPIView
|
UserFavoriteGrantedAssetsMixin,
|
||||||
|
AssetSerializerFormatMixin,
|
||||||
|
ListAPIView
|
||||||
):
|
):
|
||||||
""" 用户收藏的授权资产 """
|
""" 用户收藏的授权资产 """
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class MyFavoriteGrantedAssetsApi(AssetRoleUserMixin, UserFavoriteGrantedAssetsApi):
|
class UserDirectGrantedAssetsAsTreeApi(
|
||||||
""" 我收藏的授权资产 """
|
RebuildTreeMixin,
|
||||||
pass
|
AssetsTreeFormatMixin,
|
||||||
|
UserDirectGrantedAssetsApi
|
||||||
|
):
|
||||||
class UserDirectGrantedAssetsAsTreeApi(AssetsTreeFormatMixin, UserDirectGrantedAssetsApi):
|
|
||||||
""" 用户直接授权的资产作为树 """
|
""" 用户直接授权的资产作为树 """
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class MyUngroupAssetsAsTreeApi(AssetRoleUserMixin, UserDirectGrantedAssetsAsTreeApi):
|
class UserUngroupAssetsAsTreeApi(UserDirectGrantedAssetsAsTreeApi):
|
||||||
""" 我的未分组节点下的资产作为树 """
|
""" 用户未分组节点下的资产作为树 """
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
queryset = super().get_queryset()
|
queryset = super().get_queryset()
|
||||||
if not settings.PERM_SINGLE_ASSET_TO_UNGROUP_NODE:
|
if not settings.PERM_SINGLE_ASSET_TO_UNGROUP_NODE:
|
||||||
|
@ -63,31 +63,20 @@ class MyUngroupAssetsAsTreeApi(AssetRoleUserMixin, UserDirectGrantedAssetsAsTree
|
||||||
|
|
||||||
|
|
||||||
class UserAllGrantedAssetsApi(
|
class UserAllGrantedAssetsApi(
|
||||||
AssetRoleAdminMixin, UserAllGrantedAssetsQuerysetMixin,
|
SelfOrPKUserMixin,
|
||||||
AssetsSerializerFormatMixin, ListAPIView
|
UserAllGrantedAssetsQuerysetMixin,
|
||||||
|
AssetSerializerFormatMixin,
|
||||||
|
ListAPIView
|
||||||
):
|
):
|
||||||
""" 授权给用户的所有资产 """
|
""" 授权给用户的所有资产 """
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class MyAllGrantedAssetsApi(AssetRoleUserMixin, UserAllGrantedAssetsApi):
|
|
||||||
""" 授权给我的所有资产 """
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class MyAllAssetsAsTreeApi(AssetsTreeFormatMixin, MyAllGrantedAssetsApi):
|
|
||||||
""" 授权给我的所有资产作为树 """
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class UserGrantedNodeAssetsApi(
|
class UserGrantedNodeAssetsApi(
|
||||||
AssetRoleAdminMixin, UserGrantedNodeAssetsMixin,
|
SelfOrPKUserMixin,
|
||||||
AssetsSerializerFormatMixin, ListAPIView
|
UserGrantedNodeAssetsMixin,
|
||||||
|
AssetSerializerFormatMixin,
|
||||||
|
ListAPIView
|
||||||
):
|
):
|
||||||
""" 授权给用户的节点资产 """
|
""" 授权给用户的节点资产 """
|
||||||
pass
|
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.request import Request
|
||||||
|
from rest_framework.response import Response
|
||||||
|
|
||||||
from common.utils import get_logger
|
from common.utils import get_logger
|
||||||
from users.models import User
|
from users.models import User
|
||||||
|
from assets.api.asset.asset import AssetFilterSet
|
||||||
from assets.api.mixin import SerializeToTreeNodeMixin
|
from assets.api.mixin import SerializeToTreeNodeMixin
|
||||||
from assets.models import Asset, Node
|
from assets.models import Asset, Node
|
||||||
from perms.pagination import NodeGrantedAssetPagination, AllGrantedAssetPagination
|
|
||||||
from perms import serializers
|
from perms import serializers
|
||||||
|
from perms.pagination import NodeGrantedAssetPagination, AllGrantedAssetPagination
|
||||||
from perms.utils.user_permission import UserGrantedAssetsQueryUtils
|
from perms.utils.user_permission import UserGrantedAssetsQueryUtils
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
# 获取数据的 ------------------------------------------------------------
|
|
||||||
|
|
||||||
class UserDirectGrantedAssetsQuerysetMixin:
|
class UserDirectGrantedAssetsQuerysetMixin:
|
||||||
only_fields = serializers.AssetGrantedSerializer.Meta.only_fields
|
only_fields = serializers.AssetGrantedSerializer.Meta.only_fields
|
||||||
user: User
|
user: User
|
||||||
|
@ -32,6 +31,7 @@ class UserAllGrantedAssetsQuerysetMixin:
|
||||||
only_fields = serializers.AssetGrantedSerializer.Meta.only_fields
|
only_fields = serializers.AssetGrantedSerializer.Meta.only_fields
|
||||||
pagination_class = AllGrantedAssetPagination
|
pagination_class = AllGrantedAssetPagination
|
||||||
ordering_fields = ("name", "address")
|
ordering_fields = ("name", "address")
|
||||||
|
filterset_class = AssetFilterSet
|
||||||
ordering = ('name',)
|
ordering = ('name',)
|
||||||
|
|
||||||
user: User
|
user: User
|
||||||
|
@ -71,21 +71,19 @@ class UserGrantedNodeAssetsMixin:
|
||||||
return Asset.objects.none()
|
return Asset.objects.none()
|
||||||
node_id = self.kwargs.get("node_id")
|
node_id = self.kwargs.get("node_id")
|
||||||
|
|
||||||
node, assets = UserGrantedAssetsQueryUtils(self.user).get_node_all_assets(
|
node, assets = UserGrantedAssetsQueryUtils(self.user).get_node_all_assets(node_id)
|
||||||
node_id
|
|
||||||
)
|
|
||||||
assets = assets.prefetch_related('platform').only(*self.only_fields)
|
assets = assets.prefetch_related('platform').only(*self.only_fields)
|
||||||
self.pagination_node = node
|
self.pagination_node = node
|
||||||
return assets
|
return assets
|
||||||
|
|
||||||
|
|
||||||
# 控制格式的 ----------------------------------------------------
|
class AssetSerializerFormatMixin:
|
||||||
|
|
||||||
|
|
||||||
class AssetsSerializerFormatMixin:
|
|
||||||
serializer_class = serializers.AssetGrantedSerializer
|
serializer_class = serializers.AssetGrantedSerializer
|
||||||
filterset_fields = ['name', 'address', 'id', 'comment']
|
filterset_fields = ['name', 'address', 'id', 'comment']
|
||||||
search_fields = ['name', 'address', 'comment']
|
search_fields = ['name', 'address', 'comment']
|
||||||
|
filterset_class = AssetFilterSet
|
||||||
|
ordering_fields = ("name", "address")
|
||||||
|
ordering = ('name',)
|
||||||
|
|
||||||
|
|
||||||
class AssetsTreeFormatMixin(SerializeToTreeNodeMixin):
|
class AssetsTreeFormatMixin(SerializeToTreeNodeMixin):
|
||||||
|
|
|
@ -7,7 +7,6 @@ from django.utils.translation import ugettext_lazy as _
|
||||||
from common.http import is_true
|
from common.http import is_true
|
||||||
from common.utils import is_uuid
|
from common.utils import is_uuid
|
||||||
from common.exceptions import JMSObjectDoesNotExist
|
from common.exceptions import JMSObjectDoesNotExist
|
||||||
from common.mixins.api import RoleAdminMixin, RoleUserMixin
|
|
||||||
from perms.utils.user_permission import UserGrantedTreeRefreshController
|
from perms.utils.user_permission import UserGrantedTreeRefreshController
|
||||||
from rbac.permissions import RBACPermission
|
from rbac.permissions import RBACPermission
|
||||||
from users.models import User
|
from users.models import User
|
||||||
|
@ -23,24 +22,6 @@ class RebuildTreeMixin:
|
||||||
return super().get(request, *args, **kwargs)
|
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:
|
class SelfOrPKUserMixin:
|
||||||
kwargs: dict
|
kwargs: dict
|
||||||
request: Request
|
request: Request
|
||||||
|
@ -59,6 +40,7 @@ class SelfOrPKUserMixin:
|
||||||
('retrieve', 'perms.view_myassets'),
|
('retrieve', 'perms.view_myassets'),
|
||||||
('get_tree', 'perms.view_myassets'),
|
('get_tree', 'perms.view_myassets'),
|
||||||
('GET', 'perms.view_myassets'),
|
('GET', 'perms.view_myassets'),
|
||||||
|
('OPTIONS', 'perms.view_myassets'),
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -68,6 +50,7 @@ class SelfOrPKUserMixin:
|
||||||
('retrieve', 'perms.view_userassets'),
|
('retrieve', 'perms.view_userassets'),
|
||||||
('get_tree', 'perms.view_userassets'),
|
('get_tree', 'perms.view_userassets'),
|
||||||
('GET', 'perms.view_userassets'),
|
('GET', 'perms.view_userassets'),
|
||||||
|
('OPTIONS', 'perms.view_userassets'),
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -76,6 +59,8 @@ class SelfOrPKUserMixin:
|
||||||
user = self.request.user
|
user = self.request.user
|
||||||
elif is_uuid(self.kwargs.get('user')):
|
elif is_uuid(self.kwargs.get('user')):
|
||||||
user = get_object_or_404(User, pk=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:
|
else:
|
||||||
raise JMSObjectDoesNotExist(object_name=_('User'))
|
raise JMSObjectDoesNotExist(object_name=_('User'))
|
||||||
return user
|
return user
|
||||||
|
|
|
@ -1,31 +1,25 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
#
|
#
|
||||||
import abc
|
import abc
|
||||||
from rest_framework.generics import (
|
|
||||||
ListAPIView
|
|
||||||
)
|
|
||||||
from rest_framework.response import Response
|
|
||||||
from rest_framework.request import Request
|
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 common.utils import get_logger
|
||||||
from .mixin import AssetRoleAdminMixin, AssetRoleUserMixin
|
from assets.api.mixin import SerializeToTreeNodeMixin
|
||||||
from perms.hands import User
|
|
||||||
from perms import serializers
|
from perms import serializers
|
||||||
|
from perms.hands import User
|
||||||
from perms.utils.user_permission import UserGrantedNodesQueryUtils
|
from perms.utils.user_permission import UserGrantedNodesQueryUtils
|
||||||
|
|
||||||
|
from .mixin import SelfOrPKUserMixin, RebuildTreeMixin
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'UserGrantedNodesApi',
|
'UserGrantedNodesApi',
|
||||||
'MyGrantedNodesApi',
|
'UserGrantedNodesAsTreeApi',
|
||||||
'MyGrantedNodesAsTreeApi',
|
'UserGrantedNodeChildrenApi',
|
||||||
'UserGrantedNodeChildrenForAdminApi',
|
'UserGrantedNodeChildrenAsTreeApi',
|
||||||
'MyGrantedNodeChildrenApi',
|
|
||||||
'UserGrantedNodeChildrenAsTreeForAdminApi',
|
|
||||||
'MyGrantedNodeChildrenAsTreeApi',
|
|
||||||
'BaseGrantedNodeAsTreeApi',
|
'BaseGrantedNodeAsTreeApi',
|
||||||
'UserGrantedNodesMixin',
|
'UserGrantedNodesMixin',
|
||||||
]
|
]
|
||||||
|
@ -98,35 +92,42 @@ class UserGrantedNodesMixin:
|
||||||
return nodes
|
return nodes
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------
|
# API
|
||||||
# 最终的 api
|
|
||||||
class UserGrantedNodeChildrenForAdminApi(AssetRoleAdminMixin, UserGrantedNodeChildrenMixin, BaseNodeChildrenApi):
|
|
||||||
|
class UserGrantedNodeChildrenApi(
|
||||||
|
SelfOrPKUserMixin,
|
||||||
|
UserGrantedNodeChildrenMixin,
|
||||||
|
BaseNodeChildrenApi
|
||||||
|
):
|
||||||
|
""" 用户授权的节点下的子节点"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class MyGrantedNodeChildrenApi(AssetRoleUserMixin, UserGrantedNodeChildrenMixin, BaseNodeChildrenApi):
|
class UserGrantedNodeChildrenAsTreeApi(
|
||||||
|
SelfOrPKUserMixin,
|
||||||
|
RebuildTreeMixin,
|
||||||
|
UserGrantedNodeChildrenMixin,
|
||||||
|
BaseNodeChildrenAsTreeApi
|
||||||
|
):
|
||||||
|
""" 用户授权的节点下的子节点树"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class UserGrantedNodeChildrenAsTreeForAdminApi(AssetRoleAdminMixin, UserGrantedNodeChildrenMixin, BaseNodeChildrenAsTreeApi):
|
class UserGrantedNodesApi(
|
||||||
|
SelfOrPKUserMixin,
|
||||||
|
UserGrantedNodesMixin,
|
||||||
|
BaseGrantedNodeApi
|
||||||
|
):
|
||||||
|
""" 用户授权的节点 """
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class MyGrantedNodeChildrenAsTreeApi(AssetRoleUserMixin, UserGrantedNodeChildrenMixin, BaseNodeChildrenAsTreeApi):
|
class UserGrantedNodesAsTreeApi(
|
||||||
def get_permissions(self):
|
SelfOrPKUserMixin,
|
||||||
permissions = super().get_permissions()
|
RebuildTreeMixin,
|
||||||
return permissions
|
UserGrantedNodesMixin,
|
||||||
|
BaseGrantedNodeAsTreeApi
|
||||||
|
):
|
||||||
class UserGrantedNodesApi(AssetRoleAdminMixin, UserGrantedNodesMixin, BaseGrantedNodeApi):
|
""" 用户授权的节点树 """
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class MyGrantedNodesApi(AssetRoleUserMixin, UserGrantedNodesApi):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class MyGrantedNodesAsTreeApi(AssetRoleUserMixin, UserGrantedNodesMixin, BaseGrantedNodeAsTreeApi):
|
|
||||||
pass
|
|
||||||
|
|
||||||
# ------------------------------------------
|
|
||||||
|
|
|
@ -1,24 +1,25 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- 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.generics import ListAPIView
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
from rest_framework.response import Response
|
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 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 (
|
from perms.utils.user_permission import (
|
||||||
UserGrantedTreeBuildUtils, get_user_all_asset_perm_ids,
|
UserGrantedTreeBuildUtils, get_user_all_asset_perm_ids,
|
||||||
UserGrantedNodesQueryUtils, UserGrantedAssetsQueryUtils,
|
UserGrantedNodesQueryUtils, UserGrantedAssetsQueryUtils,
|
||||||
)
|
)
|
||||||
from perms.models import AssetPermission, PermNode
|
|
||||||
from assets.models import Asset
|
from .mixin import SelfOrPKUserMixin, RebuildTreeMixin
|
||||||
from assets.api import SerializeToTreeNodeMixin
|
|
||||||
from perms.hands import Node
|
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
@ -148,9 +149,10 @@ class GrantedNodeChildrenWithAssetsAsTreeApiMixin(SerializeToTreeNodeMixin,
|
||||||
return Response(data=all_tree_nodes)
|
return Response(data=all_tree_nodes)
|
||||||
|
|
||||||
|
|
||||||
class UserGrantedNodeChildrenWithAssetsAsTreeApi(AssetRoleAdminMixin, GrantedNodeChildrenWithAssetsAsTreeApiMixin):
|
class UserGrantedNodeChildrenWithAssetsAsTreeApi(
|
||||||
pass
|
SelfOrPKUserMixin,
|
||||||
|
RebuildTreeMixin,
|
||||||
|
GrantedNodeChildrenWithAssetsAsTreeApiMixin
|
||||||
class MyGrantedNodeChildrenWithAssetsAsTreeApi(AssetRoleUserMixin, GrantedNodeChildrenWithAssetsAsTreeApiMixin):
|
):
|
||||||
|
""" 用户授权的节点的子节点与资产树 """
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -125,7 +125,7 @@ class AssetPermission(OrgModelMixin):
|
||||||
"""
|
"""
|
||||||
asset_ids = self.get_all_assets(flat=True)
|
asset_ids = self.get_all_assets(flat=True)
|
||||||
q = Q(asset_id__in=asset_ids)
|
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)
|
q &= Q(username__in=self.accounts)
|
||||||
accounts = Account.objects.filter(q).order_by('asset__name', 'name', 'username')
|
accounts = Account.objects.filter(q).order_by('asset__name', 'name', 'username')
|
||||||
if not flat:
|
if not flat:
|
||||||
|
|
|
@ -5,14 +5,14 @@ from django.utils.translation import ugettext_lazy as _
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from assets.const import Category, AllTypes
|
from assets.const import Category, AllTypes
|
||||||
from assets.serializers.asset.common import AssetProtocolsSerializer
|
|
||||||
from assets.models import Node, Asset, Platform, Account
|
from assets.models import Node, Asset, Platform, Account
|
||||||
|
from assets.serializers.asset.common import AssetProtocolsSerializer
|
||||||
from common.drf.fields import ObjectRelatedField, LabeledChoiceField
|
from common.drf.fields import ObjectRelatedField, LabeledChoiceField
|
||||||
from perms.serializers.permission import ActionChoicesField
|
from perms.serializers.permission import ActionChoicesField
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'NodeGrantedSerializer', 'AssetGrantedSerializer',
|
'NodeGrantedSerializer', 'AssetGrantedSerializer',
|
||||||
'ActionsSerializer', 'AccountsPermedSerializer'
|
'AccountsPermedSerializer'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -30,7 +30,7 @@ class AssetGrantedSerializer(serializers.ModelSerializer):
|
||||||
'domain', 'platform',
|
'domain', 'platform',
|
||||||
"comment", "org_id", "is_active",
|
"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
|
read_only_fields = fields
|
||||||
|
|
||||||
|
|
||||||
|
@ -43,14 +43,11 @@ class NodeGrantedSerializer(serializers.ModelSerializer):
|
||||||
read_only_fields = fields
|
read_only_fields = fields
|
||||||
|
|
||||||
|
|
||||||
class ActionsSerializer(serializers.Serializer):
|
|
||||||
actions = ActionChoicesField(read_only=True)
|
|
||||||
|
|
||||||
|
|
||||||
class AccountsPermedSerializer(serializers.ModelSerializer):
|
class AccountsPermedSerializer(serializers.ModelSerializer):
|
||||||
actions = ActionChoicesField(read_only=True)
|
actions = ActionChoicesField(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Account
|
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
|
read_only_fields = fields
|
||||||
|
|
|
@ -5,5 +5,4 @@ from .user_permission import user_permission_urlpatterns
|
||||||
|
|
||||||
app_name = 'perms'
|
app_name = 'perms'
|
||||||
|
|
||||||
urlpatterns = asset_permission_urlpatterns \
|
urlpatterns = asset_permission_urlpatterns + user_permission_urlpatterns
|
||||||
+ user_permission_urlpatterns
|
|
||||||
|
|
|
@ -3,68 +3,54 @@ from django.urls import path, include
|
||||||
from .. import api
|
from .. import api
|
||||||
|
|
||||||
user_permission_urlpatterns = [
|
user_permission_urlpatterns = [
|
||||||
# 以 serializer 格式返回
|
# <str:user> such as: my | self | user.id
|
||||||
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'),
|
|
||||||
|
|
||||||
# 获取用户所有`直接授权的节点`与`直接授权资产`关联的节点
|
# assets
|
||||||
# 以 serializer 格式返回
|
path('<str:user>/assets/', api.UserAllGrantedAssetsApi.as_view(),
|
||||||
path('<uuid:pk>/nodes/', api.UserGrantedNodesApi.as_view(), name='user-nodes'),
|
name='user-assets'),
|
||||||
path('nodes/', api.MyGrantedNodesApi.as_view(), name='my-nodes'),
|
path('<str:user>/assets/tree/', api.UserDirectGrantedAssetsAsTreeApi.as_view(),
|
||||||
# 以 Tree Node 的数据格式返回
|
name='user-assets-as-tree'),
|
||||||
path('<uuid:pk>/nodes/tree/', api.MyGrantedNodesAsTreeApi.as_view(), name='user-nodes-as-tree'),
|
path('<str:user>/ungroup/assets/tree/', api.UserUngroupAssetsAsTreeApi.as_view(),
|
||||||
path('nodes/tree/', api.MyGrantedNodesAsTreeApi.as_view(), name='my-nodes-as-tree'),
|
name='user-ungroup-assets-as-tree'),
|
||||||
|
|
||||||
# 一层一层的获取用户授权的节点,
|
# nodes
|
||||||
# 以 Serializer 的数据格式返回
|
path('<str:user>/nodes/', api.UserGrantedNodesApi.as_view(),
|
||||||
path('<uuid:pk>/nodes/children/', api.UserGrantedNodeChildrenForAdminApi.as_view(), name='user-nodes-children'),
|
name='user-nodes'),
|
||||||
path('nodes/children/', api.MyGrantedNodeChildrenApi.as_view(), name='my-nodes-children'),
|
path('<str:user>/nodes/tree/', api.UserGrantedNodesAsTreeApi.as_view(),
|
||||||
# 以 Tree Node 的数据格式返回
|
name='user-nodes-as-tree'),
|
||||||
path('<uuid:pk>/nodes/children/tree/', api.UserGrantedNodeChildrenAsTreeForAdminApi.as_view(),
|
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'),
|
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(),
|
path('nodes-with-assets/tree/', api.MyGrantedNodesWithAssetsAsTreeApi.as_view(),
|
||||||
name='my-nodes-with-assets-as-tree'),
|
name='my-nodes-with-assets-as-tree'),
|
||||||
|
|
||||||
# 主要用于 luna 页面,带资产的节点树
|
# accounts
|
||||||
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
|
|
||||||
path('<str:user>/assets/<uuid:asset_id>/accounts/', api.UserPermedAssetAccountsApi.as_view(),
|
path('<str:user>/assets/<uuid:asset_id>/accounts/', api.UserPermedAssetAccountsApi.as_view(),
|
||||||
name='user-permed-asset-accounts'),
|
name='user-permed-asset-accounts'),
|
||||||
]
|
]
|
||||||
|
|
||||||
user_group_permission_urlpatterns = [
|
user_group_permission_urlpatterns = [
|
||||||
# 查询某个用户组授权的资产和资产组
|
# 查询某个用户组授权的资产和资产组
|
||||||
path('<uuid:pk>/assets/', api.UserGroupGrantedAssetsApi.as_view(), name='user-group-assets'),
|
path('<uuid:pk>/assets/', api.UserGroupGrantedAssetsApi.as_view(),
|
||||||
path('<uuid:pk>/nodes/', api.UserGroupGrantedNodesApi.as_view(), name='user-group-nodes'),
|
name='user-group-assets'),
|
||||||
path('<uuid:pk>/nodes/children/', api.UserGroupGrantedNodesApi.as_view(), name='user-group-nodes-children'),
|
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(),
|
path('<uuid:pk>/nodes/children/tree/', api.UserGroupGrantedNodeChildrenAsTreeApi.as_view(),
|
||||||
name='user-group-nodes-children-as-tree'),
|
name='user-group-nodes-children-as-tree'),
|
||||||
path('<uuid:pk>/nodes/<uuid:node_id>/assets/', api.UserGroupGrantedNodeAssetsApi.as_view(),
|
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.exceptions import JMSException
|
||||||
from common.permissions import IsValidUser
|
from common.permissions import IsValidUser
|
||||||
from common.permissions import WithBootstrapToken
|
from common.permissions import WithBootstrapToken
|
||||||
|
from common.utils import get_request_os
|
||||||
from terminal import serializers
|
from terminal import serializers
|
||||||
from terminal.const import TerminalType
|
from terminal.const import TerminalType
|
||||||
from terminal.models import Terminal
|
from terminal.models import Terminal
|
||||||
|
@ -77,13 +78,7 @@ class ConnectMethodListApi(generics.ListAPIView):
|
||||||
permission_classes = [IsValidUser]
|
permission_classes = [IsValidUser]
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
user_agent = self.request.META['HTTP_USER_AGENT'].lower()
|
os = get_request_os(self.request)
|
||||||
if 'macintosh' in user_agent:
|
|
||||||
os = 'macos'
|
|
||||||
elif 'windows' in user_agent:
|
|
||||||
os = 'windows'
|
|
||||||
else:
|
|
||||||
os = 'linux'
|
|
||||||
return TerminalType.get_protocols_connect_methods(os)
|
return TerminalType.get_protocols_connect_methods(os)
|
||||||
|
|
||||||
def list(self, request, *args, **kwargs):
|
def list(self, request, *args, **kwargs):
|
||||||
|
|
|
@ -44,9 +44,30 @@ class ComponentLoad(TextChoices):
|
||||||
return set(dict(cls.choices).keys())
|
return set(dict(cls.choices).keys())
|
||||||
|
|
||||||
|
|
||||||
class HttpMethod(TextChoices):
|
class WebMethod(TextChoices):
|
||||||
web_gui = 'web_gui', 'Web GUI'
|
web_gui = 'web_gui', 'Web GUI'
|
||||||
web_cli = 'web_cli', 'Web CLI'
|
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):
|
class NativeClient(TextChoices):
|
||||||
|
@ -56,17 +77,19 @@ class NativeClient(TextChoices):
|
||||||
xshell = 'xshell', 'Xshell'
|
xshell = 'xshell', 'Xshell'
|
||||||
|
|
||||||
# Magnus
|
# Magnus
|
||||||
mysql = 'mysql', 'mysql'
|
mysql = 'db_client_mysql', _('DB Client')
|
||||||
psql = 'psql', 'psql'
|
psql = 'db_client_psql', _('DB Client')
|
||||||
sqlplus = 'sqlplus', 'sqlplus'
|
sqlplus = 'db_client_sqlplus', _('DB Client')
|
||||||
redis = 'redis-cli', 'redis-cli'
|
redis = 'db_client_redis', _('DB Client')
|
||||||
mongodb = 'mongo', 'mongo'
|
mongodb = 'db_client_mongodb', _('DB Client')
|
||||||
|
|
||||||
# Razor
|
# Razor
|
||||||
mstsc = 'mstsc', 'Remote Desktop'
|
mstsc = 'mstsc', 'Remote Desktop'
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_native_clients(cls):
|
def get_native_clients(cls):
|
||||||
|
# native client 关注的是 endpoint 的 protocol,
|
||||||
|
# 比如 telnet mysql, koko 都支持,到那时暴露的是 ssh 协议
|
||||||
clients = {
|
clients = {
|
||||||
Protocol.ssh: {
|
Protocol.ssh: {
|
||||||
'default': [cls.ssh],
|
'default': [cls.ssh],
|
||||||
|
@ -81,6 +104,15 @@ class NativeClient(TextChoices):
|
||||||
}
|
}
|
||||||
return clients
|
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
|
@classmethod
|
||||||
def get_methods(cls, os='windows'):
|
def get_methods(cls, os='windows'):
|
||||||
clients_map = cls.get_native_clients()
|
clients_map = cls.get_native_clients()
|
||||||
|
@ -98,19 +130,19 @@ class NativeClient(TextChoices):
|
||||||
return methods
|
return methods
|
||||||
|
|
||||||
@classmethod
|
@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 = {
|
commands = {
|
||||||
'ssh': 'ssh {username}@{hostname} -p {port}',
|
cls.ssh: f'ssh {username}@{endpoint.host} -p {endpoint.ssh_port}',
|
||||||
'putty': 'putty -ssh {username}@{hostname} -P {port}',
|
cls.putty: f'putty.exe -ssh {username}@{endpoint.host} -P {endpoint.ssh_port}',
|
||||||
'xshell': '-url ssh://root:passwd@192.168.10.100',
|
cls.xshell: f'xshell.exe -url ssh://{username}:{token.value}@{endpoint.host}:{endpoint.ssh_port}',
|
||||||
'mysql': 'mysql -h {hostname} -P {port} -u {username} -p',
|
# cls.mysql: 'mysql -h {hostname} -P {port} -u {username} -p',
|
||||||
'psql': {
|
# cls.psql: {
|
||||||
'default': 'psql -h {hostname} -p {port} -U {username} -W',
|
# 'default': 'psql -h {hostname} -p {port} -U {username} -W',
|
||||||
'windows': 'psql /h {hostname} /p {port} /U {username} -W',
|
# 'windows': 'psql /h {hostname} /p {port} /U {username} -W',
|
||||||
},
|
# },
|
||||||
'sqlplus': 'sqlplus {username}/{password}@{hostname}:{port}',
|
# cls.sqlplus: 'sqlplus {username}/{password}@{hostname}:{port}',
|
||||||
'redis': 'redis-cli -h {hostname} -p {port} -a {password}',
|
# cls.redis: 'redis-cli -h {hostname} -p {port} -a {password}',
|
||||||
'mstsc': 'mstsc /v:{hostname}:{port}',
|
|
||||||
}
|
}
|
||||||
command = commands.get(name)
|
command = commands.get(name)
|
||||||
if isinstance(command, dict):
|
if isinstance(command, dict):
|
||||||
|
@ -154,7 +186,7 @@ class TerminalType(TextChoices):
|
||||||
def protocols(cls):
|
def protocols(cls):
|
||||||
protocols = {
|
protocols = {
|
||||||
cls.koko: {
|
cls.koko: {
|
||||||
'web_method': HttpMethod.web_cli,
|
'web_methods': [WebMethod.web_cli, WebMethod.web_sftp],
|
||||||
'listen': [Protocol.ssh, Protocol.http],
|
'listen': [Protocol.ssh, Protocol.http],
|
||||||
'support': [
|
'support': [
|
||||||
Protocol.ssh, Protocol.telnet,
|
Protocol.ssh, Protocol.telnet,
|
||||||
|
@ -166,7 +198,7 @@ class TerminalType(TextChoices):
|
||||||
'match': 'm2m'
|
'match': 'm2m'
|
||||||
},
|
},
|
||||||
cls.omnidb: {
|
cls.omnidb: {
|
||||||
'web_method': HttpMethod.web_gui,
|
'web_methods': [WebMethod.web_gui],
|
||||||
'listen': [Protocol.http],
|
'listen': [Protocol.http],
|
||||||
'support': [
|
'support': [
|
||||||
Protocol.mysql, Protocol.postgresql, Protocol.oracle,
|
Protocol.mysql, Protocol.postgresql, Protocol.oracle,
|
||||||
|
@ -175,7 +207,7 @@ class TerminalType(TextChoices):
|
||||||
'match': 'm2m'
|
'match': 'm2m'
|
||||||
},
|
},
|
||||||
cls.lion: {
|
cls.lion: {
|
||||||
'web_method': HttpMethod.web_gui,
|
'web_methods': [WebMethod.web_gui],
|
||||||
'listen': [Protocol.http],
|
'listen': [Protocol.http],
|
||||||
'support': [Protocol.rdp, Protocol.vnc],
|
'support': [Protocol.rdp, Protocol.vnc],
|
||||||
'match': 'm2m'
|
'match': 'm2m'
|
||||||
|
@ -183,8 +215,8 @@ class TerminalType(TextChoices):
|
||||||
cls.magnus: {
|
cls.magnus: {
|
||||||
'listen': [],
|
'listen': [],
|
||||||
'support': [
|
'support': [
|
||||||
Protocol.mysql, Protocol.postgresql, Protocol.oracle,
|
Protocol.mysql, Protocol.postgresql,
|
||||||
Protocol.mariadb
|
Protocol.oracle, Protocol.mariadb
|
||||||
],
|
],
|
||||||
'match': 'map'
|
'match': 'map'
|
||||||
},
|
},
|
||||||
|
@ -196,9 +228,19 @@ class TerminalType(TextChoices):
|
||||||
}
|
}
|
||||||
return protocols
|
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
|
@classmethod
|
||||||
def get_protocols_connect_methods(cls, os):
|
def get_protocols_connect_methods(cls, os):
|
||||||
methods = defaultdict(list)
|
methods = defaultdict(list)
|
||||||
|
web_methods = WebMethod.get_methods()
|
||||||
native_methods = NativeClient.get_methods(os)
|
native_methods = NativeClient.get_methods(os)
|
||||||
applet_methods = AppletMethod.get_methods()
|
applet_methods = AppletMethod.get_methods()
|
||||||
|
|
||||||
|
@ -212,24 +254,35 @@ class TerminalType(TextChoices):
|
||||||
listen = component_protocol['listen']
|
listen = component_protocol['listen']
|
||||||
|
|
||||||
for listen_protocol in 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
|
# Native method
|
||||||
methods[protocol.value].extend([
|
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]
|
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 protocol, applet_methods in applet_methods.items():
|
||||||
for method in applet_methods:
|
for method in applet_methods:
|
||||||
method['type'] = 'applet'
|
method['type'] = 'applet'
|
||||||
|
method['listen'] = 'rdp'
|
||||||
method['component'] = cls.tinker.value
|
method['component'] = cls.tinker.value
|
||||||
methods[protocol].extend(applet_methods)
|
methods[protocol].extend(applet_methods)
|
||||||
return methods
|
return methods
|
||||||
|
|
|
@ -138,4 +138,6 @@ class TerminalRegistrationSerializer(serializers.ModelSerializer):
|
||||||
class ConnectMethodSerializer(serializers.Serializer):
|
class ConnectMethodSerializer(serializers.Serializer):
|
||||||
value = serializers.CharField(max_length=128)
|
value = serializers.CharField(max_length=128)
|
||||||
label = 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
|
# Runtime
|
||||||
django-proxy==1.2.1
|
django-proxy==1.2.1
|
||||||
channels-redis==3.4.0
|
channels-redis==3.4.0
|
||||||
channels==3.0.4
|
|
||||||
daphne==3.0.2
|
|
||||||
python-daemon==2.3.0
|
python-daemon==2.3.0
|
||||||
eventlet==0.33.1
|
eventlet==0.33.1
|
||||||
greenlet==1.1.2
|
greenlet==1.1.2
|
||||||
|
@ -96,6 +94,8 @@ celery==5.2.7
|
||||||
flower==1.0.0
|
flower==1.0.0
|
||||||
django-celery-beat==2.3.0
|
django-celery-beat==2.3.0
|
||||||
kombu==5.2.4
|
kombu==5.2.4
|
||||||
|
uvicorn==0.20.0
|
||||||
|
websockets==10.4
|
||||||
# Auth
|
# Auth
|
||||||
python-ldap==3.4.0
|
python-ldap==3.4.0
|
||||||
ldap3==2.9.1
|
ldap3==2.9.1
|
||||||
|
|
Loading…
Reference in New Issue