Merge branch 'v3' into pr@v3@feat_support_clear_private_key

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

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

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

View File

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

View File

@ -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"]

View File

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

View File

@ -1,10 +1,10 @@
from rest_framework.response import Response
from rest_framework.generics import CreateAPIView from rest_framework.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

View File

@ -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(

View File

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

View File

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

View File

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

View File

@ -4,7 +4,13 @@ from django.core.validators import MinValueValidator, MaxValueValidator
from common.mixins import CommonModelMixin 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

View File

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

View File

@ -1,24 +1,14 @@
from django.db import models from django.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')

View File

@ -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)

View File

View File

@ -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,31 +27,11 @@ 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(
@ -58,6 +40,15 @@ class LoginAssetACLAccountsSerializer(serializers.Serializer):
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")
) )

View File

@ -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:
error = '{} Model object does not exist'.format(model.__name__) return obj
raise serializers.ValidationError(error) error = '{} Model object does not exist'.format(model.__name__)
return obj raise serializers.ValidationError(error)
@lazyproperty
def org(self):
return self.asset.org

View File

@ -1,6 +1,10 @@
from orgs.mixins.api import OrgBulkModelViewSet
from assets.models import AccountTemplate
from assets import serializers from assets 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',
}

View File

@ -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'
] ]

View File

@ -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:

View File

@ -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

View File

@ -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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -94,6 +94,10 @@ class AccountTemplate(BaseAccount):
unique_together = ( 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

View File

@ -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")

View File

@ -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:

View File

@ -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

View File

@ -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):

View File

@ -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)

View File

@ -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)

View File

@ -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)()

View File

@ -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},
}

View File

@ -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:

View File

@ -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

View File

@ -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)
account_usernames = list(accounts.values_list('username', flat=True)) if username is None:
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)

View File

@ -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')

View File

@ -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: # 设置远程应用
name = token.asset.name self.set_applet_info(token, rdp_options)
# remote-app
# app = '||jmservisor' # 文件名
# rdp_options['remoteapplicationmode:i'] = '1' name = token.asset.name
# 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:

View File

View File

@ -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):

View File

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

View File

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

View File

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

View File

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

View File

@ -1,17 +1,17 @@
import time
from datetime import timedelta from 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,20 +19,22 @@ 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')
) )
asset = models.ForeignKey( asset = models.ForeignKey(
'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

View File

@ -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},
}

View File

@ -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):

View File

@ -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 *

View File

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

View File

@ -17,9 +17,9 @@ class GunicornService(BaseService):
log_format = '%(h)s %(t)s %(L)ss "%(r)s" %(s)s %(b)s ' 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',

View File

@ -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'

View File

@ -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
try: for ssh_key_type in _supported_paramiko_ssh_key_types:
key = paramiko.RSAKey.from_private_key(StringIO(text), password=password) if not isinstance(ssh_key_type, paramiko.PKey):
except paramiko.SSHException: continue
pass try:
else: key = ssh_key_type.from_private_key(StringIO(text), password=password)
return key return key
except paramiko.SSHException:
try: pass
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):

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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")
@ -63,7 +143,7 @@ class DatesLoginMetricMixin:
def __set_data_to_cache(self, date, tp, count): def __set_data_to_cache(self, date, tp, count):
cache_key = self.get_cache_key(date, tp) cache_key = self.get_cache_key(date, tp)
cache.set(cache_key, count, 3600*24*7) cache.set(cache_key, count, 3600 * 24 * 7)
@staticmethod @staticmethod
def get_date_start_2_end(d): def get_date_start_2_end(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')

View File

@ -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

View File

@ -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 "動的コード"

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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()
return query_set.filter(instant=False) if self.action != 'retrieve':
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

View File

@ -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()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,21 +1,18 @@
# ~*~ coding: utf-8 ~*~ # ~*~ 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")

View File

@ -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']

View File

@ -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):

View File

@ -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'
)

View File

@ -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"
] ]

View File

@ -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",
] ]

View File

@ -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:

View File

@ -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()

View File

@ -2,8 +2,9 @@ from django.db.models.signals import post_save, pre_delete, pre_save, post_delet
from django.dispatch import receiver from 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)

View File

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

View File

@ -3,58 +3,58 @@ from rest_framework.generics import ListAPIView
from common.utils import get_logger from 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

View File

@ -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,7 +31,8 @@ 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")
ordering = ('name', ) filterset_class = AssetFilterSet
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):

View File

@ -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

View File

@ -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
# ------------------------------------------

View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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):

View File

@ -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

View File

@ -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)

View File

@ -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