mirror of https://github.com/jumpserver/jumpserver
feat: ACL (#5696)
* feature: acl (v0.1) * feature: acl (v0.2) * feature: acl (v0.3) * feature: acl (v0.4) * feature: acl (v0.5) * feature: acl (v0.6) * feature: acl (v0.7) * feature: acl (v0.8) * feature: acl (v0.9) * feature: acl (v1.0) * feature: acl (v1.1) * feature: acl (v1.2) * feature: acl (v1.3) * feature: acl (v1.4) * feature: acl (v1.5) * feature: acl (v1.6) * feature: acl (v1.7) * feature: acl (v1.8) * feature: acl (v1.9) * feature: acl (v2.0) * feature: acl (v2.1) * feature: acl (v2.2) * feature: acl (v2.3) * feature: acl (v2.4) * feature: acl (v2.5) * feature: acl (v2.6) * feature: acl (v2.7) * feature: acl (v2.8) * feature: acl (v2.9) * feature: acl (v3.0) * feature: acl (v3.1) * feature: acl (v3.2) * feature: acl (v3.3) * feature: acl (v3.4) * feature: acl (v3.5) * feature: acl (v3.6) * feature: acl (v3.7) * feature: acl (v3.8) * feature: acl (v3.9) * feature: acl (v4.0) * feature: acl (v4.1) * feature: acl (v4.2) * feature: acl (v4.3) * feature: acl (v4.4)pull/5727/head
parent
09303ecc56
commit
64641a18e6
|
@ -0,0 +1,3 @@
|
|||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
|
@ -0,0 +1,3 @@
|
|||
from .login_acl import *
|
||||
from .login_asset_acl import *
|
||||
from .login_asset_check import *
|
|
@ -0,0 +1,19 @@
|
|||
from common.permissions import IsOrgAdmin, HasQueryParamsUserAndIsCurrentOrgMember
|
||||
from common.drf.api import JMSBulkModelViewSet
|
||||
from ..models import LoginACL
|
||||
from .. import serializers
|
||||
|
||||
__all__ = ['LoginACLViewSet', ]
|
||||
|
||||
|
||||
class LoginACLViewSet(JMSBulkModelViewSet):
|
||||
queryset = LoginACL.objects.all()
|
||||
filterset_fields = ('name', 'user', )
|
||||
search_fields = filterset_fields
|
||||
permission_classes = (IsOrgAdmin, )
|
||||
serializer_class = serializers.LoginACLSerializer
|
||||
|
||||
def get_permissions(self):
|
||||
if self.action in ["retrieve", "list"]:
|
||||
self.permission_classes = (IsOrgAdmin, HasQueryParamsUserAndIsCurrentOrgMember)
|
||||
return super().get_permissions()
|
|
@ -0,0 +1,15 @@
|
|||
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
from common.permissions import IsOrgAdmin
|
||||
from .. import models, serializers
|
||||
|
||||
|
||||
__all__ = ['LoginAssetACLViewSet']
|
||||
|
||||
|
||||
class LoginAssetACLViewSet(OrgBulkModelViewSet):
|
||||
model = models.LoginAssetACL
|
||||
filterset_fields = ('name', )
|
||||
search_fields = filterset_fields
|
||||
permission_classes = (IsOrgAdmin, )
|
||||
serializer_class = serializers.LoginAssetACLSerializer
|
|
@ -0,0 +1,105 @@
|
|||
from django.shortcuts import get_object_or_404
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.generics import CreateAPIView, RetrieveDestroyAPIView
|
||||
|
||||
from common.permissions import IsAppUser
|
||||
from common.utils import reverse, lazyproperty
|
||||
from orgs.utils import tmp_to_org, tmp_to_root_org
|
||||
from tickets.models import Ticket
|
||||
from ..models import LoginAssetACL
|
||||
from .. import serializers
|
||||
|
||||
|
||||
__all__ = ['LoginAssetCheckAPI', 'LoginAssetConfirmStatusAPI']
|
||||
|
||||
|
||||
class LoginAssetCheckAPI(CreateAPIView):
|
||||
permission_classes = (IsAppUser, )
|
||||
serializer_class = serializers.LoginAssetCheckSerializer
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
is_need_confirm, response_data = self.check_if_need_confirm()
|
||||
return Response(data=response_data, status=200)
|
||||
|
||||
def check_if_need_confirm(self):
|
||||
queries = {
|
||||
'user': self.serializer.user, 'asset': self.serializer.asset,
|
||||
'system_user': self.serializer.system_user,
|
||||
'action': LoginAssetACL.ActionChoices.login_confirm
|
||||
}
|
||||
with tmp_to_org(self.serializer.org):
|
||||
acl = LoginAssetACL.filter(**queries).valid().first()
|
||||
|
||||
if not acl:
|
||||
is_need_confirm = False
|
||||
response_data = {}
|
||||
else:
|
||||
is_need_confirm = True
|
||||
response_data = self._get_response_data_of_need_confirm(acl)
|
||||
response_data['need_confirm'] = is_need_confirm
|
||||
return is_need_confirm, response_data
|
||||
|
||||
def _get_response_data_of_need_confirm(self, acl):
|
||||
ticket = LoginAssetACL.create_login_asset_confirm_ticket(
|
||||
user=self.serializer.user,
|
||||
asset=self.serializer.asset,
|
||||
system_user=self.serializer.system_user,
|
||||
assignees=acl.reviewers.all(),
|
||||
org_id=self.serializer.org.id
|
||||
)
|
||||
confirm_status_url = reverse(
|
||||
view_name='acls:login-asset-confirm-status',
|
||||
kwargs={'pk': str(ticket.id)}
|
||||
)
|
||||
ticket_detail_url = reverse(
|
||||
view_name='api-tickets:ticket-detail',
|
||||
kwargs={'pk': str(ticket.id)},
|
||||
external=True, api_to_ui=True
|
||||
)
|
||||
ticket_detail_url = '{url}?type={type}'.format(url=ticket_detail_url, type=ticket.type)
|
||||
data = {
|
||||
'check_confirm_status': {'method': 'GET', 'url': confirm_status_url},
|
||||
'close_confirm': {'method': 'DELETE', 'url': confirm_status_url},
|
||||
'ticket_detail_url': ticket_detail_url,
|
||||
'reviewers': [str(user) for user in ticket.assignees.all()],
|
||||
}
|
||||
return data
|
||||
|
||||
@lazyproperty
|
||||
def serializer(self):
|
||||
serializer = self.get_serializer(data=self.request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
return serializer
|
||||
|
||||
|
||||
class LoginAssetConfirmStatusAPI(RetrieveDestroyAPIView):
|
||||
permission_classes = (IsAppUser, )
|
||||
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
if self.ticket.action_open:
|
||||
status = 'await'
|
||||
elif self.ticket.action_approve:
|
||||
status = 'approve'
|
||||
else:
|
||||
status = 'reject'
|
||||
data = {
|
||||
'status': status,
|
||||
'action': self.ticket.action,
|
||||
'processor': self.ticket.processor_display
|
||||
}
|
||||
return Response(data=data, status=200)
|
||||
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
if self.ticket.status_open:
|
||||
self.ticket.close(processor=self.ticket.applicant)
|
||||
data = {
|
||||
'action': self.ticket.action,
|
||||
'status': self.ticket.status,
|
||||
'processor': self.ticket.processor_display
|
||||
}
|
||||
return Response(data=data, status=200)
|
||||
|
||||
@lazyproperty
|
||||
def ticket(self):
|
||||
with tmp_to_root_org():
|
||||
return get_object_or_404(Ticket, pk=self.kwargs['pk'])
|
|
@ -0,0 +1,5 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AclsConfig(AppConfig):
|
||||
name = 'acls'
|
|
@ -0,0 +1,9 @@
|
|||
from django.utils.translation import ugettext as _
|
||||
|
||||
|
||||
common_help_text = _('Format for comma-delimited string, with * indicating a match all. ')
|
||||
|
||||
ip_group_help_text = common_help_text + _(
|
||||
'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 '
|
||||
)
|
|
@ -0,0 +1,61 @@
|
|||
# Generated by Django 3.1 on 2021-03-11 09:53
|
||||
|
||||
from django.conf import settings
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='LoginACL',
|
||||
fields=[
|
||||
('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')),
|
||||
('is_active', models.BooleanField(default=True, verbose_name='Active')),
|
||||
('comment', models.TextField(blank=True, default='', verbose_name='Comment')),
|
||||
('ip_group', models.JSONField(default=list, verbose_name='Login IP')),
|
||||
('action', models.CharField(choices=[('reject', 'Reject'), ('allow', 'Allow')], default='reject', max_length=64, verbose_name='Action')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='login_acls', to=settings.AUTH_USER_MODEL, verbose_name='User')),
|
||||
],
|
||||
options={
|
||||
'ordering': ('priority', '-date_updated', 'name'),
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='LoginAssetACL',
|
||||
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')),
|
||||
('is_active', models.BooleanField(default=True, verbose_name='Active')),
|
||||
('comment', models.TextField(blank=True, default='', verbose_name='Comment')),
|
||||
('users', models.JSONField(verbose_name='User')),
|
||||
('system_users', models.JSONField(verbose_name='System User')),
|
||||
('assets', models.JSONField(verbose_name='Asset')),
|
||||
('action', models.CharField(choices=[('login_confirm', 'Login confirm')], default='login_confirm', max_length=64, verbose_name='Action')),
|
||||
('reviewers', models.ManyToManyField(blank=True, related_name='review_login_asset_acls', to=settings.AUTH_USER_MODEL, verbose_name='Reviewers')),
|
||||
],
|
||||
options={
|
||||
'ordering': ('priority', '-date_updated', 'name'),
|
||||
'unique_together': {('name', 'org_id')},
|
||||
},
|
||||
),
|
||||
]
|
|
@ -0,0 +1,2 @@
|
|||
from .login_acl import *
|
||||
from .login_asset_acl import *
|
|
@ -0,0 +1,35 @@
|
|||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.core.validators import MinValueValidator, MaxValueValidator
|
||||
from common.mixins import CommonModelMixin
|
||||
|
||||
|
||||
__all__ = ['BaseACL', 'BaseACLQuerySet']
|
||||
|
||||
|
||||
class BaseACLQuerySet(models.QuerySet):
|
||||
def active(self):
|
||||
return self.filter(is_active=True)
|
||||
|
||||
def inactive(self):
|
||||
return self.filter(is_active=False)
|
||||
|
||||
def valid(self):
|
||||
return self.active()
|
||||
|
||||
def invalid(self):
|
||||
return self.inactive()
|
||||
|
||||
|
||||
class BaseACL(CommonModelMixin):
|
||||
name = models.CharField(max_length=128, verbose_name=_('Name'))
|
||||
priority = models.IntegerField(
|
||||
default=50, verbose_name=_("Priority"),
|
||||
help_text=_("1-100, the lower the value will be match first"),
|
||||
validators=[MinValueValidator(1), MaxValueValidator(100)]
|
||||
)
|
||||
is_active = models.BooleanField(default=True, verbose_name=_("Active"))
|
||||
comment = models.TextField(default='', blank=True, verbose_name=_('Comment'))
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
|
@ -0,0 +1,54 @@
|
|||
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from .base import BaseACL, BaseACLQuerySet
|
||||
from ..utils import contains_ip
|
||||
|
||||
|
||||
class ACLManager(models.Manager):
|
||||
|
||||
def valid(self):
|
||||
return self.get_queryset().valid()
|
||||
|
||||
|
||||
class LoginACL(BaseACL):
|
||||
class ActionChoices(models.TextChoices):
|
||||
reject = 'reject', _('Reject')
|
||||
allow = 'allow', _('Allow')
|
||||
|
||||
# 条件
|
||||
ip_group = models.JSONField(default=list, verbose_name=_('Login IP'))
|
||||
# 动作
|
||||
action = models.CharField(
|
||||
max_length=64, choices=ActionChoices.choices, default=ActionChoices.reject,
|
||||
verbose_name=_('Action')
|
||||
)
|
||||
# 关联
|
||||
user = models.ForeignKey(
|
||||
'users.User', on_delete=models.CASCADE, related_name='login_acls', verbose_name=_('User')
|
||||
)
|
||||
|
||||
objects = ACLManager.from_queryset(BaseACLQuerySet)()
|
||||
|
||||
class Meta:
|
||||
ordering = ('priority', '-date_updated', 'name')
|
||||
|
||||
@property
|
||||
def action_reject(self):
|
||||
return self.action == self.ActionChoices.reject
|
||||
|
||||
@property
|
||||
def action_allow(self):
|
||||
return self.action == self.ActionChoices.allow
|
||||
|
||||
@staticmethod
|
||||
def allow_user_to_login(user, ip):
|
||||
acl = user.login_acls.valid().first()
|
||||
if not acl:
|
||||
return True
|
||||
is_contained = contains_ip(ip, acl.ip_group)
|
||||
if acl.action_allow and is_contained:
|
||||
return True
|
||||
if acl.action_reject and not is_contained:
|
||||
return True
|
||||
return False
|
|
@ -0,0 +1,99 @@
|
|||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from orgs.mixins.models import OrgModelMixin, OrgManager
|
||||
from .base import BaseACL, BaseACLQuerySet
|
||||
from ..utils import contains_ip
|
||||
|
||||
|
||||
class ACLManager(OrgManager):
|
||||
|
||||
def valid(self):
|
||||
return self.get_queryset().valid()
|
||||
|
||||
|
||||
class LoginAssetACL(BaseACL, OrgModelMixin):
|
||||
class ActionChoices(models.TextChoices):
|
||||
login_confirm = 'login_confirm', _('Login confirm')
|
||||
|
||||
# 条件
|
||||
users = models.JSONField(verbose_name=_('User'))
|
||||
system_users = models.JSONField(verbose_name=_('System User'))
|
||||
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)()
|
||||
|
||||
class Meta:
|
||||
unique_together = ('name', 'org_id')
|
||||
ordering = ('priority', '-date_updated', 'name')
|
||||
|
||||
@classmethod
|
||||
def filter(cls, user, asset, system_user, action):
|
||||
queryset = cls.objects.filter(action=action)
|
||||
queryset = cls.filter_user(user, queryset)
|
||||
queryset = cls.filter_asset(asset, queryset)
|
||||
queryset = cls.filter_system_user(system_user, 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.hostname) |
|
||||
Q(assets__hostname_group__contains='*')
|
||||
)
|
||||
ids = [q.id for q in queryset if contains_ip(asset.ip, q.assets.get('ip_group', []))]
|
||||
queryset = cls.objects.filter(id__in=ids)
|
||||
return queryset
|
||||
|
||||
@classmethod
|
||||
def filter_system_user(cls, system_user, queryset):
|
||||
queryset = queryset.filter(
|
||||
Q(system_users__name_group__contains=system_user.name) |
|
||||
Q(system_users__name_group__contains='*')
|
||||
).filter(
|
||||
Q(system_users__username_group__contains=system_user.username) |
|
||||
Q(system_users__username_group__contains='*')
|
||||
).filter(
|
||||
Q(system_users__protocol_group__contains=system_user.protocol) |
|
||||
Q(system_users__protocol_group__contains='*')
|
||||
)
|
||||
return queryset
|
||||
|
||||
@classmethod
|
||||
def create_login_asset_confirm_ticket(cls, user, asset, system_user, assignees, org_id):
|
||||
from tickets.const import TicketTypeChoices
|
||||
from tickets.models import Ticket
|
||||
data = {
|
||||
'title': _('Login asset confirm') + ' ({})'.format(user),
|
||||
'type': TicketTypeChoices.login_asset_confirm,
|
||||
'meta': {
|
||||
'apply_login_user': str(user),
|
||||
'apply_login_asset': str(asset),
|
||||
'apply_login_system_user': str(system_user),
|
||||
},
|
||||
'org_id': org_id,
|
||||
}
|
||||
ticket = Ticket.objects.create(**data)
|
||||
ticket.assignees.set(assignees)
|
||||
ticket.open(applicant=user)
|
||||
return ticket
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
from .login_acl import *
|
||||
from .login_asset_acl import *
|
||||
from .login_asset_check import *
|
|
@ -0,0 +1,49 @@
|
|||
from django.utils.translation import ugettext as _
|
||||
from rest_framework import serializers
|
||||
from common.drf.serializers import BulkModelSerializer
|
||||
from orgs.utils import current_org
|
||||
from ..models import LoginACL
|
||||
from ..utils import is_ip_address, is_ip_network, is_ip_segment
|
||||
from .. import const
|
||||
|
||||
|
||||
__all__ = ['LoginACLSerializer', ]
|
||||
|
||||
|
||||
def ip_group_child_validator(ip_group_child):
|
||||
is_valid = ip_group_child == '*' \
|
||||
or is_ip_address(ip_group_child) \
|
||||
or is_ip_network(ip_group_child) \
|
||||
or is_ip_segment(ip_group_child)
|
||||
if not is_valid:
|
||||
error = _('IP address invalid: `{}`').format(ip_group_child)
|
||||
raise serializers.ValidationError(error)
|
||||
|
||||
|
||||
class LoginACLSerializer(BulkModelSerializer):
|
||||
ip_group = serializers.ListField(
|
||||
default=['*'], label=_('IP'), help_text=const.ip_group_help_text,
|
||||
child=serializers.CharField(max_length=1024, validators=[ip_group_child_validator])
|
||||
)
|
||||
user_display = serializers.ReadOnlyField(source='user.name', label=_('User'))
|
||||
action_display = serializers.ReadOnlyField(source='get_action_display', label=_('Action'))
|
||||
|
||||
class Meta:
|
||||
model = LoginACL
|
||||
fields = [
|
||||
'id', 'name', 'priority', 'ip_group', 'user', 'user_display', 'action',
|
||||
'action_display', 'is_active', 'comment', 'created_by', 'date_created', 'date_updated'
|
||||
]
|
||||
extra_kwargs = {
|
||||
'priority': {'default': 50},
|
||||
'is_active': {'default': True},
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def validate_user(user):
|
||||
if user not in current_org.get_members():
|
||||
error = _('The user `{}` is not in the current organization: `{}`').format(
|
||||
user, current_org
|
||||
)
|
||||
raise serializers.ValidationError(error)
|
||||
return user
|
|
@ -0,0 +1,87 @@
|
|||
from rest_framework import serializers
|
||||
from django.utils.translation import ugettext as _
|
||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||
from assets.models import SystemUser
|
||||
from acls import models
|
||||
from orgs.models import Organization
|
||||
from .. import const
|
||||
|
||||
|
||||
__all__ = ['LoginAssetACLSerializer']
|
||||
|
||||
|
||||
class LoginAssetACLUsersSerializer(serializers.Serializer):
|
||||
username_group = serializers.ListField(
|
||||
default=['*'], child=serializers.CharField(max_length=128), label=_('Username'),
|
||||
help_text=const.common_help_text
|
||||
)
|
||||
|
||||
|
||||
class LoginAssetACLAssestsSerializer(serializers.Serializer):
|
||||
ip_group = serializers.ListField(
|
||||
default=['*'], child=serializers.CharField(max_length=1024), label=_('IP'),
|
||||
help_text=const.ip_group_help_text + _('(Domain name support)')
|
||||
)
|
||||
hostname_group = serializers.ListField(
|
||||
default=['*'], child=serializers.CharField(max_length=128), label=_('Hostname'),
|
||||
help_text=const.common_help_text
|
||||
)
|
||||
|
||||
|
||||
class LoginAssetACLSystemUsersSerializer(serializers.Serializer):
|
||||
name_group = serializers.ListField(
|
||||
default=['*'], child=serializers.CharField(max_length=128), label=_('Name'),
|
||||
help_text=const.common_help_text
|
||||
)
|
||||
username_group = serializers.ListField(
|
||||
default=['*'], child=serializers.CharField(max_length=128), label=_('Username'),
|
||||
help_text=const.common_help_text
|
||||
)
|
||||
protocol_group = serializers.ListField(
|
||||
default=['*'], child=serializers.CharField(max_length=16), label=_('Protocol'),
|
||||
help_text=const.common_help_text + _('Protocol options: {}').format(
|
||||
', '.join(SystemUser.ASSET_CATEGORY_PROTOCOLS)
|
||||
)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def validate_protocol_group(protocol_group):
|
||||
unsupported_protocols = set(protocol_group) - set(SystemUser.ASSET_CATEGORY_PROTOCOLS + ['*'])
|
||||
if unsupported_protocols:
|
||||
error = _('Unsupported protocols: {}').format(unsupported_protocols)
|
||||
raise serializers.ValidationError(error)
|
||||
return protocol_group
|
||||
|
||||
|
||||
class LoginAssetACLSerializer(BulkOrgResourceModelSerializer):
|
||||
users = LoginAssetACLUsersSerializer()
|
||||
assets = LoginAssetACLAssestsSerializer()
|
||||
system_users = LoginAssetACLSystemUsersSerializer()
|
||||
reviewers_amount = serializers.IntegerField(read_only=True, source='reviewers.count')
|
||||
action_display = serializers.ReadOnlyField(source='get_action_display', label=_('Action'))
|
||||
|
||||
class Meta:
|
||||
model = models.LoginAssetACL
|
||||
fields = [
|
||||
'id', 'name', 'priority', 'users', 'system_users', 'assets', 'action', 'action_display',
|
||||
'is_active', 'comment', 'reviewers', 'reviewers_amount', 'created_by', 'date_created',
|
||||
'date_updated', 'org_id'
|
||||
]
|
||||
extra_kwargs = {
|
||||
"reviewers": {'allow_null': False, 'required': True},
|
||||
'priority': {'default': 50},
|
||||
'is_active': {'default': True},
|
||||
}
|
||||
|
||||
def validate_reviewers(self, reviewers):
|
||||
org_id = self.fields['org_id'].default()
|
||||
org = Organization.get_instance(org_id)
|
||||
if not org:
|
||||
error = _('The organization `{}` does not exist'.format(org_id))
|
||||
raise serializers.ValidationError(error)
|
||||
users = org.get_members()
|
||||
valid_reviewers = list(set(reviewers) & set(users))
|
||||
if not valid_reviewers:
|
||||
error = _('None of the reviewers belong to Organization `{}`'.format(org.name))
|
||||
raise serializers.ValidationError(error)
|
||||
return valid_reviewers
|
|
@ -0,0 +1,71 @@
|
|||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
from orgs.utils import tmp_to_root_org
|
||||
from common.utils import get_object_or_none, lazyproperty
|
||||
from users.models import User
|
||||
from assets.models import Asset, SystemUser
|
||||
|
||||
|
||||
__all__ = ['LoginAssetCheckSerializer']
|
||||
|
||||
|
||||
class LoginAssetCheckSerializer(serializers.Serializer):
|
||||
user_id = serializers.UUIDField(required=True, allow_null=False)
|
||||
asset_id = serializers.UUIDField(required=True, allow_null=False)
|
||||
system_user_id = serializers.UUIDField(required=True, allow_null=False)
|
||||
system_user_username = serializers.CharField(max_length=128, default='')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.user = None
|
||||
self.asset = None
|
||||
self._system_user = None
|
||||
self._system_user_username = None
|
||||
|
||||
def validate_user_id(self, user_id):
|
||||
self.user = self.validate_object_exist(User, user_id)
|
||||
return user_id
|
||||
|
||||
def validate_asset_id(self, asset_id):
|
||||
self.asset = self.validate_object_exist(Asset, asset_id)
|
||||
return asset_id
|
||||
|
||||
def validate_system_user_id(self, system_user_id):
|
||||
self._system_user = self.validate_object_exist(SystemUser, system_user_id)
|
||||
return system_user_id
|
||||
|
||||
def validate_system_user_username(self, system_user_username):
|
||||
system_user_id = self.initial_data.get('system_user_id')
|
||||
system_user = self.validate_object_exist(SystemUser, system_user_id)
|
||||
if self._system_user.login_mode == SystemUser.LOGIN_MANUAL \
|
||||
and not system_user.username \
|
||||
and not system_user.username_same_with_user \
|
||||
and not system_user_username:
|
||||
error = 'Missing parameter: system_user_username'
|
||||
raise serializers.ValidationError(error)
|
||||
self._system_user_username = system_user_username
|
||||
return system_user_username
|
||||
|
||||
@staticmethod
|
||||
def validate_object_exist(model, field_id):
|
||||
with tmp_to_root_org():
|
||||
obj = get_object_or_none(model, pk=field_id)
|
||||
if not obj:
|
||||
error = '{} Model object does not exist'.format(model.__name__)
|
||||
raise serializers.ValidationError(error)
|
||||
return obj
|
||||
|
||||
@lazyproperty
|
||||
def system_user(self):
|
||||
if self._system_user.username_same_with_user:
|
||||
username = self.user.username
|
||||
elif self._system_user.login_mode == SystemUser.LOGIN_MANUAL:
|
||||
username = self._system_user_username
|
||||
else:
|
||||
username = self._system_user.username
|
||||
self._system_user.username = username
|
||||
return self._system_user
|
||||
|
||||
@lazyproperty
|
||||
def org(self):
|
||||
return self.asset.org
|
|
@ -0,0 +1,3 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
|
@ -0,0 +1 @@
|
|||
from .api_urls import *
|
|
@ -0,0 +1,18 @@
|
|||
from django.urls import path
|
||||
from rest_framework_bulk.routes import BulkRouter
|
||||
from .. import api
|
||||
|
||||
|
||||
app_name = 'acls'
|
||||
|
||||
|
||||
router = BulkRouter()
|
||||
router.register(r'login-acls', api.LoginACLViewSet, 'login-acl')
|
||||
router.register(r'login-asset-acls', api.LoginAssetACLViewSet, 'login-asset-acl')
|
||||
|
||||
urlpatterns = [
|
||||
path('login-asset/check/', api.LoginAssetCheckAPI.as_view(), name='login-asset-check'),
|
||||
path('login-asset-confirm/<uuid:pk>/status/', api.LoginAssetConfirmStatusAPI.as_view(), name='login-asset-confirm-status')
|
||||
]
|
||||
|
||||
urlpatterns += router.urls
|
|
@ -0,0 +1,68 @@
|
|||
from ipaddress import ip_network, ip_address
|
||||
|
||||
|
||||
def is_ip_address(address):
|
||||
""" 192.168.10.1 """
|
||||
try:
|
||||
ip_address(address)
|
||||
except ValueError:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
def is_ip_network(ip):
|
||||
""" 192.168.1.0/24 """
|
||||
try:
|
||||
ip_network(ip)
|
||||
except ValueError:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
def is_ip_segment(ip):
|
||||
""" 10.1.1.1-10.1.1.20 """
|
||||
if '-' not in ip:
|
||||
return False
|
||||
ip_address1, ip_address2 = ip.split('-')
|
||||
return is_ip_address(ip_address1) and is_ip_address(ip_address2)
|
||||
|
||||
|
||||
def in_ip_segment(ip, ip_segment):
|
||||
ip1, ip2 = ip_segment.split('-')
|
||||
ip1 = int(ip_address(ip1))
|
||||
ip2 = int(ip_address(ip2))
|
||||
ip = int(ip_address(ip))
|
||||
return min(ip1, ip2) <= ip <= max(ip1, ip2)
|
||||
|
||||
|
||||
def contains_ip(ip, ip_group):
|
||||
"""
|
||||
ip_group:
|
||||
[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.]
|
||||
|
||||
"""
|
||||
|
||||
if '*' in ip_group:
|
||||
return True
|
||||
|
||||
for _ip in ip_group:
|
||||
if is_ip_address(_ip):
|
||||
# 192.168.10.1
|
||||
if ip == _ip:
|
||||
return True
|
||||
elif is_ip_network(_ip) and is_ip_address(ip):
|
||||
# 192.168.1.0/24
|
||||
if ip_address(ip) in ip_network(_ip):
|
||||
return True
|
||||
elif is_ip_segment(_ip) and is_ip_address(ip):
|
||||
# 10.1.1.1-10.1.1.20
|
||||
if in_ip_segment(ip, _ip):
|
||||
return True
|
||||
else:
|
||||
# is domain name
|
||||
if ip == _ip:
|
||||
return True
|
||||
|
||||
return False
|
|
@ -0,0 +1,48 @@
|
|||
# Generated by Django 3.1 on 2021-03-11 03:13
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def migrate_cmd_filter_priority(apps, schema_editor):
|
||||
cmd_filter_rule_model = apps.get_model('assets', 'CommandFilterRule')
|
||||
cmd_filter_rules = cmd_filter_rule_model.objects.all()
|
||||
for cmd_filter_rule in cmd_filter_rules:
|
||||
cmd_filter_rule.priority = 100 - cmd_filter_rule.priority + 1
|
||||
|
||||
cmd_filter_rule_model.objects.bulk_update(cmd_filter_rules, fields=['priority'])
|
||||
|
||||
|
||||
def migrate_system_user_priority(apps, schema_editor):
|
||||
system_user_model = apps.get_model('assets', 'SystemUser')
|
||||
system_users = system_user_model.objects.all()
|
||||
for system_user in system_users:
|
||||
system_user.priority = 100 - system_user.priority + 1
|
||||
|
||||
system_user_model.objects.bulk_update(system_users, fields=['priority'])
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0066_auto_20210208_1802'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(migrate_cmd_filter_priority),
|
||||
migrations.RunPython(migrate_system_user_priority),
|
||||
migrations.AlterModelOptions(
|
||||
name='commandfilterrule',
|
||||
options={'ordering': ('priority', 'action'), 'verbose_name': 'Command filter rule'},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='commandfilterrule',
|
||||
name='priority',
|
||||
field=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'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='systemuser',
|
||||
name='priority',
|
||||
field=models.IntegerField(default=20, 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'),
|
||||
),
|
||||
]
|
|
@ -50,7 +50,7 @@ class CommandFilterRule(OrgModelMixin):
|
|||
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||
filter = models.ForeignKey('CommandFilter', on_delete=models.CASCADE, verbose_name=_("Filter"), related_name='rules')
|
||||
type = models.CharField(max_length=16, default=TYPE_COMMAND, choices=TYPE_CHOICES, verbose_name=_("Type"))
|
||||
priority = models.IntegerField(default=50, verbose_name=_("Priority"), help_text=_("1-100, the higher will be match first"),
|
||||
priority = models.IntegerField(default=50, verbose_name=_("Priority"), help_text=_("1-100, the lower the value will be match first"),
|
||||
validators=[MinValueValidator(1), MaxValueValidator(100)])
|
||||
content = models.TextField(verbose_name=_("Content"), help_text=_("One line one command"))
|
||||
action = models.IntegerField(default=ACTION_DENY, choices=ACTION_CHOICES, verbose_name=_("Action"))
|
||||
|
@ -60,7 +60,7 @@ class CommandFilterRule(OrgModelMixin):
|
|||
created_by = models.CharField(max_length=128, blank=True, default='', verbose_name=_('Created by'))
|
||||
|
||||
class Meta:
|
||||
ordering = ('-priority', 'action')
|
||||
ordering = ('priority', 'action')
|
||||
verbose_name = _("Command filter rule")
|
||||
|
||||
@lazyproperty
|
||||
|
|
|
@ -116,7 +116,7 @@ class SystemUser(BaseUser):
|
|||
assets = models.ManyToManyField('assets.Asset', blank=True, verbose_name=_("Assets"))
|
||||
users = models.ManyToManyField('users.User', blank=True, verbose_name=_("Users"))
|
||||
groups = models.ManyToManyField('users.UserGroup', blank=True, verbose_name=_("User groups"))
|
||||
priority = models.IntegerField(default=20, verbose_name=_("Priority"), validators=[MinValueValidator(1), MaxValueValidator(100)])
|
||||
priority = models.IntegerField(default=20, verbose_name=_("Priority"), help_text=_("1-100, the lower the value will be match first"), validators=[MinValueValidator(1), MaxValueValidator(100)])
|
||||
protocol = models.CharField(max_length=16, choices=PROTOCOL_CHOICES, default='ssh', verbose_name=_('Protocol'))
|
||||
auto_push = models.BooleanField(default=True, verbose_name=_('Auto push'))
|
||||
sudo = models.TextField(default='/bin/whoami', verbose_name=_('Sudo'))
|
||||
|
|
|
@ -19,6 +19,7 @@ reason_password_expired = 'password_expired'
|
|||
reason_user_invalid = 'user_invalid'
|
||||
reason_user_inactive = 'user_inactive'
|
||||
reason_backend_not_match = 'backend_not_match'
|
||||
reason_acl_not_allow = 'acl_not_allow'
|
||||
|
||||
reason_choices = {
|
||||
reason_password_failed: _('Username/password check failed'),
|
||||
|
@ -29,7 +30,8 @@ reason_choices = {
|
|||
reason_password_expired: _("Password expired"),
|
||||
reason_user_invalid: _('Disabled or expired'),
|
||||
reason_user_inactive: _("This account is inactive."),
|
||||
reason_backend_not_match: _("Auth backend not match")
|
||||
reason_backend_not_match: _("Auth backend not match"),
|
||||
reason_acl_not_allow: _("ACL is not allowed")
|
||||
}
|
||||
old_reason_choices = {
|
||||
'0': '-',
|
||||
|
|
|
@ -128,6 +128,13 @@ class AuthMixin:
|
|||
if auth_backend not in auth_backends_allowed:
|
||||
self.raise_credential_error(error=errors.reason_backend_not_match)
|
||||
|
||||
def _check_login_acl(self, user, ip):
|
||||
# ACL 限制用户登录
|
||||
from acls.models import LoginACL
|
||||
is_allowed = LoginACL.allow_user_to_login(user, ip)
|
||||
if not is_allowed:
|
||||
raise self.raise_credential_error(error=errors.reason_acl_not_allow)
|
||||
|
||||
def check_user_auth(self, decrypt_passwd=False):
|
||||
self.check_is_block()
|
||||
request = self.request
|
||||
|
@ -135,8 +142,9 @@ class AuthMixin:
|
|||
|
||||
self._check_only_allow_exists_user_auth(username)
|
||||
user = self._check_auth_user_is_valid(username, password, public_key)
|
||||
# 校验login-acl规则
|
||||
self._check_login_acl(user, ip)
|
||||
# 限制只能从认证来源登录
|
||||
|
||||
auth_backend = getattr(user, 'backend', 'django.contrib.auth.backends.ModelBackend')
|
||||
self._check_auth_source_is_valid(user, auth_backend)
|
||||
self._check_password_require_reset_or_not(user)
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
import time
|
||||
|
||||
from rest_framework import permissions
|
||||
from django.contrib.auth.mixins import UserPassesTestMixin
|
||||
from django.conf import settings
|
||||
|
@ -193,3 +192,12 @@ class IsObjectOwner(IsValidUser):
|
|||
def has_object_permission(self, request, view, obj):
|
||||
return (super().has_object_permission(request, view, obj) and
|
||||
request.user == getattr(obj, 'user', None))
|
||||
|
||||
|
||||
class HasQueryParamsUserAndIsCurrentOrgMember(permissions.BasePermission):
|
||||
def has_permission(self, request, view):
|
||||
query_user_id = request.query_params.get('user')
|
||||
if not query_user_id:
|
||||
return False
|
||||
query_user = current_org.get_members().filter(id=query_user_id).first()
|
||||
return bool(query_user)
|
||||
|
|
|
@ -142,7 +142,7 @@ def is_uuid(seq):
|
|||
elif isinstance(seq, str) and UUID_PATTERN.match(seq):
|
||||
return True
|
||||
elif isinstance(seq, (list, tuple)):
|
||||
all([is_uuid(x) for x in seq])
|
||||
return all([is_uuid(x) for x in seq])
|
||||
return False
|
||||
|
||||
|
||||
|
|
|
@ -48,6 +48,7 @@ INSTALLED_APPS = [
|
|||
'authentication.apps.AuthenticationConfig', # authentication
|
||||
'applications.apps.ApplicationsConfig',
|
||||
'tickets.apps.TicketsConfig',
|
||||
'acls.apps.AclsConfig',
|
||||
'jms_oidc_rp',
|
||||
'rest_framework',
|
||||
'rest_framework_swagger',
|
||||
|
|
|
@ -22,6 +22,7 @@ api_v1 = [
|
|||
path('common/', include('common.urls.api_urls', namespace='api-common')),
|
||||
path('applications/', include('applications.urls.api_urls', namespace='api-applications')),
|
||||
path('tickets/', include('tickets.urls.api_urls', namespace='api-tickets')),
|
||||
path('acls/', include('acls.urls.api_urls', namespace='api-acls')),
|
||||
path('prometheus/metrics/', api.PrometheusMetricsApi.as_view())
|
||||
]
|
||||
|
||||
|
|
Binary file not shown.
|
@ -8,7 +8,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: JumpServer 0.3.3\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-03-10 11:16+0800\n"
|
||||
"POT-Creation-Date: 2021-03-11 17:54+0800\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: ibuler <ibuler@qq.com>\n"
|
||||
"Language-Team: JumpServer team<ibuler@qq.com>\n"
|
||||
|
@ -17,21 +17,19 @@ msgstr ""
|
|||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: applications/const.py:9
|
||||
#: applications/serializers/attrs/application_category/db.py:14
|
||||
#: applications/serializers/attrs/application_type/mysql_workbench.py:26
|
||||
#: users/templates/users/user_granted_database_app.html:37
|
||||
msgid "Database"
|
||||
msgstr "数据库"
|
||||
#: acls/const.py:4
|
||||
msgid "Format for comma-delimited string, with * indicating a match all. "
|
||||
msgstr "格式为逗号分隔的字符串, * 表示匹配所有. "
|
||||
|
||||
#: applications/const.py:10
|
||||
msgid "Remote app"
|
||||
msgstr "远程应用"
|
||||
|
||||
#: applications/const.py:29
|
||||
msgid "Custom"
|
||||
msgstr "自定义"
|
||||
#: acls/const.py:7
|
||||
msgid ""
|
||||
"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 "
|
||||
msgstr ""
|
||||
"例如: 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"
|
||||
|
||||
#: acls/models/base.py:25 acls/serializers/login_asset_acl.py:33
|
||||
#: applications/models/application.py:11 assets/models/asset.py:142
|
||||
#: assets/models/base.py:250 assets/models/cluster.py:18
|
||||
#: assets/models/cmd_filter.py:21 assets/models/domain.py:21
|
||||
|
@ -57,6 +55,216 @@ msgstr "自定义"
|
|||
msgid "Name"
|
||||
msgstr "名称"
|
||||
|
||||
#: acls/models/base.py:27 assets/models/cmd_filter.py:53
|
||||
#: assets/models/user.py:119
|
||||
msgid "Priority"
|
||||
msgstr "优先级"
|
||||
|
||||
#: acls/models/base.py:28 assets/models/cmd_filter.py:53
|
||||
#: assets/models/user.py:119
|
||||
msgid "1-100, the lower the value will be match first"
|
||||
msgstr "优先级可选范围为 1-100 (数值越小越优先)"
|
||||
|
||||
#: acls/models/base.py:31 authentication/models.py:20
|
||||
#: authentication/templates/authentication/_access_key_modal.html:32
|
||||
#: perms/models/base.py:52 users/templates/users/_select_user_modal.html:18
|
||||
#: users/templates/users/user_detail.html:132
|
||||
#: users/templates/users/user_profile.html:63
|
||||
msgid "Active"
|
||||
msgstr "激活中"
|
||||
|
||||
# msgid "Date created"
|
||||
# msgstr "创建日期"
|
||||
#: acls/models/base.py:32 applications/models/application.py:24
|
||||
#: assets/models/asset.py:147 assets/models/asset.py:223
|
||||
#: assets/models/base.py:255 assets/models/cluster.py:29
|
||||
#: assets/models/cmd_filter.py:23 assets/models/cmd_filter.py:57
|
||||
#: assets/models/domain.py:22 assets/models/domain.py:56
|
||||
#: assets/models/group.py:23 assets/models/label.py:23 ops/models/adhoc.py:37
|
||||
#: orgs/models.py:26 perms/models/base.py:57 settings/models.py:34
|
||||
#: terminal/models/storage.py:29 terminal/models/storage.py:81
|
||||
#: terminal/models/terminal.py:153 tickets/models/ticket.py:73
|
||||
#: users/models/group.py:16 users/models/user.py:563
|
||||
#: users/templates/users/user_detail.html:115
|
||||
#: users/templates/users/user_granted_database_app.html:38
|
||||
#: users/templates/users/user_granted_remote_app.html:37
|
||||
#: users/templates/users/user_group_detail.html:62
|
||||
#: users/templates/users/user_group_list.html:16
|
||||
#: users/templates/users/user_profile.html:138
|
||||
#: xpack/plugins/change_auth_plan/models.py:77 xpack/plugins/cloud/models.py:35
|
||||
#: xpack/plugins/cloud/models.py:98 xpack/plugins/gathered_user/models.py:26
|
||||
msgid "Comment"
|
||||
msgstr "备注"
|
||||
|
||||
#: acls/models/login_acl.py:16 tickets/const.py:18
|
||||
msgid "Reject"
|
||||
msgstr "拒绝"
|
||||
|
||||
#: acls/models/login_acl.py:17 assets/models/cmd_filter.py:47
|
||||
msgid "Allow"
|
||||
msgstr "允许"
|
||||
|
||||
#: acls/models/login_acl.py:20
|
||||
msgid "Login IP"
|
||||
msgstr "登录IP"
|
||||
|
||||
#: acls/models/login_acl.py:24 acls/models/login_asset_acl.py:26
|
||||
#: acls/serializers/login_acl.py:29 acls/serializers/login_asset_acl.py:61
|
||||
#: assets/models/cmd_filter.py:56 audits/models.py:57
|
||||
#: authentication/templates/authentication/_access_key_modal.html:34
|
||||
#: tickets/models/ticket.py:43 users/templates/users/_granted_assets.html:29
|
||||
#: users/templates/users/user_asset_permission.html:44
|
||||
#: users/templates/users/user_asset_permission.html:79
|
||||
#: users/templates/users/user_database_app_permission.html:42
|
||||
#: users/templates/users/user_group_list.html:17
|
||||
#: users/templates/users/user_list.html:20
|
||||
#: users/templates/users/user_remote_app_permission.html:42
|
||||
msgid "Action"
|
||||
msgstr "动作"
|
||||
|
||||
#: acls/models/login_acl.py:28 acls/models/login_asset_acl.py:20
|
||||
#: acls/serializers/login_acl.py:28 assets/models/label.py:15
|
||||
#: audits/models.py:36 audits/models.py:56 audits/models.py:69
|
||||
#: audits/serializers.py:81 authentication/models.py:44
|
||||
#: authentication/models.py:95 orgs/models.py:18 orgs/models.py:403
|
||||
#: perms/models/base.py:50 templates/index.html:78
|
||||
#: terminal/backends/command/models.py:18
|
||||
#: terminal/backends/command/serializers.py:12 terminal/models/session.py:37
|
||||
#: tickets/models/comment.py:17 users/models/user.py:159
|
||||
#: users/models/user.py:677 users/serializers/group.py:20
|
||||
#: users/templates/users/user_asset_permission.html:38
|
||||
#: users/templates/users/user_asset_permission.html:64
|
||||
#: users/templates/users/user_database_app_permission.html:37
|
||||
#: users/templates/users/user_database_app_permission.html:58
|
||||
#: users/templates/users/user_group_detail.html:73
|
||||
#: users/templates/users/user_group_list.html:15
|
||||
#: users/templates/users/user_list.html:135
|
||||
#: users/templates/users/user_remote_app_permission.html:37
|
||||
#: users/templates/users/user_remote_app_permission.html:58
|
||||
msgid "User"
|
||||
msgstr "用户"
|
||||
|
||||
#: acls/models/login_asset_acl.py:17 authentication/models.py:71
|
||||
#: tickets/const.py:9 users/templates/users/user_detail.html:250
|
||||
msgid "Login confirm"
|
||||
msgstr "登录复核"
|
||||
|
||||
#: acls/models/login_asset_acl.py:21
|
||||
msgid "System User"
|
||||
msgstr "系统用户"
|
||||
|
||||
#: acls/models/login_asset_acl.py:22
|
||||
#: applications/serializers/attrs/application_category/remote_app.py:33
|
||||
#: assets/models/asset.py:355 assets/models/authbook.py:26
|
||||
#: assets/models/gathered_user.py:14 assets/serializers/admin_user.py:29
|
||||
#: assets/serializers/asset_user.py:47 assets/serializers/asset_user.py:84
|
||||
#: assets/serializers/system_user.py:191 audits/models.py:38
|
||||
#: perms/models/asset_permission.py:99 templates/index.html:82
|
||||
#: terminal/backends/command/models.py:19
|
||||
#: terminal/backends/command/serializers.py:13 terminal/models/session.py:39
|
||||
#: users/templates/users/user_asset_permission.html:40
|
||||
#: users/templates/users/user_asset_permission.html:70
|
||||
#: users/templates/users/user_granted_remote_app.html:36
|
||||
#: xpack/plugins/change_auth_plan/models.py:282
|
||||
#: xpack/plugins/cloud/models.py:202
|
||||
msgid "Asset"
|
||||
msgstr "资产"
|
||||
|
||||
#: acls/models/login_asset_acl.py:32 authentication/models.py:45
|
||||
#: users/templates/users/user_detail.html:258
|
||||
msgid "Reviewers"
|
||||
msgstr "审批人"
|
||||
|
||||
#: acls/models/login_asset_acl.py:86 tickets/const.py:12
|
||||
msgid "Login asset confirm"
|
||||
msgstr "登录资产复核"
|
||||
|
||||
#: acls/serializers/login_acl.py:19
|
||||
msgid "IP address invalid: `{}`"
|
||||
msgstr "IP 地址无效: `{}`"
|
||||
|
||||
#: acls/serializers/login_acl.py:25 acls/serializers/login_asset_acl.py:22
|
||||
#: applications/serializers/attrs/application_type/mysql_workbench.py:18
|
||||
#: assets/models/asset.py:183 assets/models/domain.py:52
|
||||
#: assets/serializers/asset_user.py:46 settings/serializers/settings.py:108
|
||||
#: users/templates/users/_granted_assets.html:26
|
||||
#: users/templates/users/user_asset_permission.html:156
|
||||
msgid "IP"
|
||||
msgstr "IP"
|
||||
|
||||
#: acls/serializers/login_acl.py:41
|
||||
msgid "The user `{}` is not in the current organization: `{}`"
|
||||
msgstr "用户 `{}` 不在当前组织: `{}`"
|
||||
|
||||
#: acls/serializers/login_asset_acl.py:15
|
||||
#: acls/serializers/login_asset_acl.py:37
|
||||
#: applications/serializers/attrs/application_type/chrome.py:20
|
||||
#: applications/serializers/attrs/application_type/custom.py:21
|
||||
#: applications/serializers/attrs/application_type/mysql_workbench.py:30
|
||||
#: applications/serializers/attrs/application_type/vmware_client.py:26
|
||||
#: assets/models/base.py:251 assets/models/gathered_user.py:15
|
||||
#: audits/models.py:99 authentication/forms.py:15 authentication/forms.py:17
|
||||
#: ops/models/adhoc.py:148 users/forms/profile.py:31 users/models/user.py:528
|
||||
#: users/templates/users/_select_user_modal.html:14
|
||||
#: users/templates/users/user_detail.html:53
|
||||
#: users/templates/users/user_list.html:15
|
||||
#: users/templates/users/user_profile.html:47
|
||||
#: xpack/plugins/change_auth_plan/models.py:47
|
||||
#: xpack/plugins/change_auth_plan/models.py:278
|
||||
#: xpack/plugins/cloud/serializers.py:44
|
||||
msgid "Username"
|
||||
msgstr "用户名"
|
||||
|
||||
#: acls/serializers/login_asset_acl.py:23
|
||||
msgid "(Domain name support)"
|
||||
msgstr "(支持域名)"
|
||||
|
||||
#: acls/serializers/login_asset_acl.py:26 assets/models/asset.py:184
|
||||
#: assets/serializers/asset_user.py:45 assets/serializers/gathered_user.py:20
|
||||
#: settings/serializers/settings.py:107
|
||||
#: users/templates/users/_granted_assets.html:25
|
||||
#: users/templates/users/user_asset_permission.html:157
|
||||
msgid "Hostname"
|
||||
msgstr "主机名"
|
||||
|
||||
#: acls/serializers/login_asset_acl.py:41 assets/models/asset.py:187
|
||||
#: assets/models/domain.py:54 assets/models/user.py:120
|
||||
#: terminal/serializers/session.py:29 terminal/serializers/storage.py:69
|
||||
msgid "Protocol"
|
||||
msgstr "协议"
|
||||
|
||||
#: acls/serializers/login_asset_acl.py:42
|
||||
msgid "Protocol options: {}"
|
||||
msgstr "协议选项: {}"
|
||||
|
||||
#: acls/serializers/login_asset_acl.py:51
|
||||
msgid "Unsupported protocols: {}"
|
||||
msgstr "不支持的协议: {}"
|
||||
|
||||
#: acls/serializers/login_asset_acl.py:78
|
||||
#: tickets/serializers/ticket/ticket.py:109
|
||||
msgid "The organization `{}` does not exist"
|
||||
msgstr "组织 `{}` 不存在"
|
||||
|
||||
#: acls/serializers/login_asset_acl.py:83
|
||||
msgid "None of the reviewers belong to Organization `{}`"
|
||||
msgstr "所有复核人都不属于组织 `{}`"
|
||||
|
||||
#: applications/const.py:9
|
||||
#: applications/serializers/attrs/application_category/db.py:14
|
||||
#: applications/serializers/attrs/application_type/mysql_workbench.py:26
|
||||
#: users/templates/users/user_granted_database_app.html:37
|
||||
msgid "Database"
|
||||
msgstr "数据库"
|
||||
|
||||
#: applications/const.py:10
|
||||
msgid "Remote app"
|
||||
msgstr "远程应用"
|
||||
|
||||
#: applications/const.py:29
|
||||
msgid "Custom"
|
||||
msgstr "自定义"
|
||||
|
||||
#: applications/models/application.py:13
|
||||
#: applications/serializers/application.py:47 assets/models/label.py:21
|
||||
#: perms/models/application_permission.py:20
|
||||
|
@ -87,28 +295,6 @@ msgstr "网域"
|
|||
msgid "Attrs"
|
||||
msgstr ""
|
||||
|
||||
# msgid "Date created"
|
||||
# msgstr "创建日期"
|
||||
#: applications/models/application.py:24 assets/models/asset.py:147
|
||||
#: assets/models/asset.py:223 assets/models/base.py:255
|
||||
#: assets/models/cluster.py:29 assets/models/cmd_filter.py:23
|
||||
#: assets/models/cmd_filter.py:57 assets/models/domain.py:22
|
||||
#: assets/models/domain.py:56 assets/models/group.py:23
|
||||
#: assets/models/label.py:23 ops/models/adhoc.py:37 orgs/models.py:26
|
||||
#: perms/models/base.py:57 settings/models.py:34 terminal/models/storage.py:29
|
||||
#: terminal/models/storage.py:81 terminal/models/terminal.py:153
|
||||
#: tickets/models/ticket.py:73 users/models/group.py:16
|
||||
#: users/models/user.py:563 users/templates/users/user_detail.html:115
|
||||
#: users/templates/users/user_granted_database_app.html:38
|
||||
#: users/templates/users/user_granted_remote_app.html:37
|
||||
#: users/templates/users/user_group_detail.html:62
|
||||
#: users/templates/users/user_group_list.html:16
|
||||
#: users/templates/users/user_profile.html:138
|
||||
#: xpack/plugins/change_auth_plan/models.py:77 xpack/plugins/cloud/models.py:35
|
||||
#: xpack/plugins/cloud/models.py:98 xpack/plugins/gathered_user/models.py:26
|
||||
msgid "Comment"
|
||||
msgstr "备注"
|
||||
|
||||
#: applications/serializers/attrs/application_category/cloud.py:9
|
||||
#: assets/models/cluster.py:40
|
||||
msgid "Cluster"
|
||||
|
@ -131,22 +317,6 @@ msgstr "主机"
|
|||
msgid "Port"
|
||||
msgstr "端口"
|
||||
|
||||
#: applications/serializers/attrs/application_category/remote_app.py:33
|
||||
#: assets/models/asset.py:355 assets/models/authbook.py:26
|
||||
#: assets/models/gathered_user.py:14 assets/serializers/admin_user.py:29
|
||||
#: assets/serializers/asset_user.py:47 assets/serializers/asset_user.py:84
|
||||
#: assets/serializers/system_user.py:191 audits/models.py:38
|
||||
#: perms/models/asset_permission.py:99 templates/index.html:82
|
||||
#: terminal/backends/command/models.py:19
|
||||
#: terminal/backends/command/serializers.py:13 terminal/models/session.py:39
|
||||
#: users/templates/users/user_asset_permission.html:40
|
||||
#: users/templates/users/user_asset_permission.html:70
|
||||
#: users/templates/users/user_granted_remote_app.html:36
|
||||
#: xpack/plugins/change_auth_plan/models.py:282
|
||||
#: xpack/plugins/cloud/models.py:202
|
||||
msgid "Asset"
|
||||
msgstr "资产"
|
||||
|
||||
#: applications/serializers/attrs/application_category/remote_app.py:36
|
||||
#: applications/serializers/attrs/application_type/chrome.py:14
|
||||
#: applications/serializers/attrs/application_type/mysql_workbench.py:14
|
||||
|
@ -159,23 +329,6 @@ msgstr "应用路径"
|
|||
msgid "Target URL"
|
||||
msgstr "目标URL"
|
||||
|
||||
#: applications/serializers/attrs/application_type/chrome.py:20
|
||||
#: applications/serializers/attrs/application_type/custom.py:21
|
||||
#: applications/serializers/attrs/application_type/mysql_workbench.py:30
|
||||
#: applications/serializers/attrs/application_type/vmware_client.py:26
|
||||
#: assets/models/base.py:251 assets/models/gathered_user.py:15
|
||||
#: audits/models.py:99 authentication/forms.py:15 authentication/forms.py:17
|
||||
#: ops/models/adhoc.py:148 users/forms/profile.py:31 users/models/user.py:528
|
||||
#: users/templates/users/_select_user_modal.html:14
|
||||
#: users/templates/users/user_detail.html:53
|
||||
#: users/templates/users/user_list.html:15
|
||||
#: users/templates/users/user_profile.html:47
|
||||
#: xpack/plugins/change_auth_plan/models.py:47
|
||||
#: xpack/plugins/change_auth_plan/models.py:278
|
||||
#: xpack/plugins/cloud/serializers.py:44
|
||||
msgid "Username"
|
||||
msgstr "用户名"
|
||||
|
||||
#: applications/serializers/attrs/application_type/chrome.py:23
|
||||
#: applications/serializers/attrs/application_type/custom.py:25
|
||||
#: applications/serializers/attrs/application_type/mysql_workbench.py:34
|
||||
|
@ -205,14 +358,6 @@ msgstr "运行参数"
|
|||
msgid "Target url"
|
||||
msgstr "目标URL"
|
||||
|
||||
#: applications/serializers/attrs/application_type/mysql_workbench.py:18
|
||||
#: assets/models/asset.py:183 assets/models/domain.py:52
|
||||
#: assets/serializers/asset_user.py:46 settings/serializers/settings.py:108
|
||||
#: users/templates/users/_granted_assets.html:26
|
||||
#: users/templates/users/user_asset_permission.html:156
|
||||
msgid "IP"
|
||||
msgstr "IP"
|
||||
|
||||
#: assets/api/admin_user.py:50
|
||||
msgid "Deleted failed, There are related assets"
|
||||
msgstr "删除失败,存在关联资产"
|
||||
|
@ -262,19 +407,6 @@ msgstr "内部的"
|
|||
msgid "Platform"
|
||||
msgstr "系统平台"
|
||||
|
||||
#: assets/models/asset.py:184 assets/serializers/asset_user.py:45
|
||||
#: assets/serializers/gathered_user.py:20 settings/serializers/settings.py:107
|
||||
#: users/templates/users/_granted_assets.html:25
|
||||
#: users/templates/users/user_asset_permission.html:157
|
||||
msgid "Hostname"
|
||||
msgstr "主机名"
|
||||
|
||||
#: assets/models/asset.py:187 assets/models/domain.py:54
|
||||
#: assets/models/user.py:120 terminal/serializers/session.py:29
|
||||
#: terminal/serializers/storage.py:69
|
||||
msgid "Protocol"
|
||||
msgstr "协议"
|
||||
|
||||
#: assets/models/asset.py:189 assets/serializers/asset.py:68
|
||||
#: perms/serializers/asset/user_permission.py:41
|
||||
msgid "Protocols"
|
||||
|
@ -484,22 +616,10 @@ msgstr "命令"
|
|||
msgid "Deny"
|
||||
msgstr "拒绝"
|
||||
|
||||
#: assets/models/cmd_filter.py:47
|
||||
msgid "Allow"
|
||||
msgstr "允许"
|
||||
|
||||
#: assets/models/cmd_filter.py:51
|
||||
msgid "Filter"
|
||||
msgstr "过滤器"
|
||||
|
||||
#: assets/models/cmd_filter.py:53 assets/models/user.py:119
|
||||
msgid "Priority"
|
||||
msgstr "优先级"
|
||||
|
||||
#: assets/models/cmd_filter.py:53
|
||||
msgid "1-100, the higher will be match first"
|
||||
msgstr "优先级可选范围为1-100,1最低优先级,100最高优先级"
|
||||
|
||||
#: assets/models/cmd_filter.py:55 xpack/plugins/license/models.py:29
|
||||
msgid "Content"
|
||||
msgstr "内容"
|
||||
|
@ -508,18 +628,6 @@ msgstr "内容"
|
|||
msgid "One line one command"
|
||||
msgstr "每行一个命令"
|
||||
|
||||
#: assets/models/cmd_filter.py:56 audits/models.py:57
|
||||
#: authentication/templates/authentication/_access_key_modal.html:34
|
||||
#: tickets/models/ticket.py:43 users/templates/users/_granted_assets.html:29
|
||||
#: users/templates/users/user_asset_permission.html:44
|
||||
#: users/templates/users/user_asset_permission.html:79
|
||||
#: users/templates/users/user_database_app_permission.html:42
|
||||
#: users/templates/users/user_group_list.html:17
|
||||
#: users/templates/users/user_list.html:20
|
||||
#: users/templates/users/user_remote_app_permission.html:42
|
||||
msgid "Action"
|
||||
msgstr "动作"
|
||||
|
||||
#: assets/models/cmd_filter.py:64
|
||||
msgid "Command filter rule"
|
||||
msgstr "命令过滤规则"
|
||||
|
@ -556,27 +664,7 @@ msgstr "资产组"
|
|||
msgid "Default asset group"
|
||||
msgstr "默认资产组"
|
||||
|
||||
#: assets/models/label.py:15 audits/models.py:36 audits/models.py:56
|
||||
#: audits/models.py:69 audits/serializers.py:81 authentication/models.py:44
|
||||
#: authentication/models.py:95 orgs/models.py:18 orgs/models.py:403
|
||||
#: perms/models/base.py:50 templates/index.html:78
|
||||
#: terminal/backends/command/models.py:18
|
||||
#: terminal/backends/command/serializers.py:12 terminal/models/session.py:37
|
||||
#: tickets/models/comment.py:17 users/models/user.py:159
|
||||
#: users/models/user.py:677 users/serializers/group.py:20
|
||||
#: users/templates/users/user_asset_permission.html:38
|
||||
#: users/templates/users/user_asset_permission.html:64
|
||||
#: users/templates/users/user_database_app_permission.html:37
|
||||
#: users/templates/users/user_database_app_permission.html:58
|
||||
#: users/templates/users/user_group_detail.html:73
|
||||
#: users/templates/users/user_group_list.html:15
|
||||
#: users/templates/users/user_list.html:135
|
||||
#: users/templates/users/user_remote_app_permission.html:37
|
||||
#: users/templates/users/user_remote_app_permission.html:58
|
||||
msgid "User"
|
||||
msgstr "用户"
|
||||
|
||||
#: assets/models/label.py:19 assets/models/node.py:553 settings/models.py:30
|
||||
#: assets/models/label.py:19 assets/models/node.py:575 settings/models.py:30
|
||||
msgid "Value"
|
||||
msgstr "值"
|
||||
|
||||
|
@ -584,23 +672,23 @@ msgstr "值"
|
|||
msgid "New node"
|
||||
msgstr "新节点"
|
||||
|
||||
#: assets/models/node.py:445 users/templates/users/_granted_assets.html:130
|
||||
#: assets/models/node.py:467 users/templates/users/_granted_assets.html:130
|
||||
msgid "empty"
|
||||
msgstr "空"
|
||||
|
||||
#: assets/models/node.py:552 perms/models/asset_permission.py:156
|
||||
#: assets/models/node.py:574 perms/models/asset_permission.py:156
|
||||
msgid "Key"
|
||||
msgstr "键"
|
||||
|
||||
#: assets/models/node.py:554
|
||||
#: assets/models/node.py:576
|
||||
msgid "Full value"
|
||||
msgstr "全称"
|
||||
|
||||
#: assets/models/node.py:557 perms/models/asset_permission.py:157
|
||||
#: assets/models/node.py:579 perms/models/asset_permission.py:157
|
||||
msgid "Parent key"
|
||||
msgstr "ssh私钥"
|
||||
|
||||
#: assets/models/node.py:566 assets/serializers/system_user.py:190
|
||||
#: assets/models/node.py:588 assets/serializers/system_user.py:190
|
||||
#: users/templates/users/user_asset_permission.html:41
|
||||
#: users/templates/users/user_asset_permission.html:73
|
||||
#: users/templates/users/user_asset_permission.html:158
|
||||
|
@ -1201,47 +1289,51 @@ msgstr ""
|
|||
msgid "Invalid token or cache refreshed."
|
||||
msgstr ""
|
||||
|
||||
#: authentication/errors.py:24
|
||||
#: authentication/errors.py:25
|
||||
msgid "Username/password check failed"
|
||||
msgstr "用户名/密码 校验失败"
|
||||
|
||||
#: authentication/errors.py:25
|
||||
#: authentication/errors.py:26
|
||||
msgid "Password decrypt failed"
|
||||
msgstr "密码解密失败"
|
||||
|
||||
#: authentication/errors.py:26
|
||||
#: authentication/errors.py:27
|
||||
msgid "MFA failed"
|
||||
msgstr "多因子认证失败"
|
||||
|
||||
#: authentication/errors.py:27
|
||||
#: authentication/errors.py:28
|
||||
msgid "MFA unset"
|
||||
msgstr "多因子认证没有设定"
|
||||
|
||||
#: authentication/errors.py:28
|
||||
#: authentication/errors.py:29
|
||||
msgid "Username does not exist"
|
||||
msgstr "用户名不存在"
|
||||
|
||||
#: authentication/errors.py:29
|
||||
#: authentication/errors.py:30
|
||||
msgid "Password expired"
|
||||
msgstr "密码已过期"
|
||||
|
||||
#: authentication/errors.py:30
|
||||
#: authentication/errors.py:31
|
||||
msgid "Disabled or expired"
|
||||
msgstr "禁用或失效"
|
||||
|
||||
#: authentication/errors.py:31
|
||||
#: authentication/errors.py:32
|
||||
msgid "This account is inactive."
|
||||
msgstr "此账户已禁用"
|
||||
|
||||
#: authentication/errors.py:32
|
||||
#: authentication/errors.py:33
|
||||
msgid "Auth backend not match"
|
||||
msgstr ""
|
||||
msgstr "没有匹配到认证后端"
|
||||
|
||||
#: authentication/errors.py:42
|
||||
#: authentication/errors.py:34
|
||||
msgid "ACL is not allowed"
|
||||
msgstr "ACL 不被允许"
|
||||
|
||||
#: authentication/errors.py:44
|
||||
msgid "No session found, check your cookie"
|
||||
msgstr "会话已变更,刷新页面"
|
||||
|
||||
#: authentication/errors.py:44
|
||||
#: authentication/errors.py:46
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
"The username or password you entered is incorrect, please enter it again. "
|
||||
|
@ -1251,46 +1343,46 @@ msgstr ""
|
|||
"您输入的用户名或密码不正确,请重新输入。 您还可以尝试 {times_try} 次(账号将"
|
||||
"被临时 锁定 {block_time} 分钟)"
|
||||
|
||||
#: authentication/errors.py:50
|
||||
#: authentication/errors.py:52
|
||||
msgid ""
|
||||
"The account has been locked (please contact admin to unlock it or try again "
|
||||
"after {} minutes)"
|
||||
msgstr "账号已被锁定(请联系管理员解锁 或 {}分钟后重试)"
|
||||
|
||||
#: authentication/errors.py:53 users/views/profile/otp.py:110
|
||||
#: authentication/errors.py:55 users/views/profile/otp.py:110
|
||||
#: users/views/profile/otp.py:149 users/views/profile/otp.py:169
|
||||
msgid "MFA code invalid, or ntp sync server time"
|
||||
msgstr "MFA验证码不正确,或者服务器端时间不对"
|
||||
|
||||
#: authentication/errors.py:55
|
||||
#: authentication/errors.py:57
|
||||
msgid "MFA required"
|
||||
msgstr "需要多因子认证"
|
||||
|
||||
#: authentication/errors.py:56
|
||||
#: authentication/errors.py:58
|
||||
msgid "MFA not set, please set it first"
|
||||
msgstr "多因子认证没有设置,请先完成设置"
|
||||
|
||||
#: authentication/errors.py:57
|
||||
#: authentication/errors.py:59
|
||||
msgid "Login confirm required"
|
||||
msgstr "需要登录复核"
|
||||
|
||||
#: authentication/errors.py:58
|
||||
#: authentication/errors.py:60
|
||||
msgid "Wait login confirm ticket for accept"
|
||||
msgstr "等待登录复核处理"
|
||||
|
||||
#: authentication/errors.py:59
|
||||
#: authentication/errors.py:61
|
||||
msgid "Login confirm ticket was {}"
|
||||
msgstr "登录复核 {}"
|
||||
|
||||
#: authentication/errors.py:215
|
||||
#: authentication/errors.py:217
|
||||
msgid "SSO auth closed"
|
||||
msgstr "SSO 认证关闭了"
|
||||
|
||||
#: authentication/errors.py:220 authentication/views/login.py:232
|
||||
#: authentication/errors.py:222 authentication/views/login.py:232
|
||||
msgid "Your password is too simple, please change it for security"
|
||||
msgstr "你的密码过于简单,为了安全,请修改"
|
||||
|
||||
#: authentication/errors.py:229 authentication/views/login.py:247
|
||||
#: authentication/errors.py:231 authentication/views/login.py:247
|
||||
msgid "Your password has expired, please reset before logging in"
|
||||
msgstr "您的密码已过期,先修改再登录"
|
||||
|
||||
|
@ -1303,27 +1395,10 @@ msgstr "{} 天内自动登录"
|
|||
msgid "MFA code"
|
||||
msgstr "多因子认证验证码"
|
||||
|
||||
#: authentication/models.py:20
|
||||
#: authentication/templates/authentication/_access_key_modal.html:32
|
||||
#: perms/models/base.py:52 users/templates/users/_select_user_modal.html:18
|
||||
#: users/templates/users/user_detail.html:132
|
||||
#: users/templates/users/user_profile.html:63
|
||||
msgid "Active"
|
||||
msgstr "激活中"
|
||||
|
||||
#: authentication/models.py:40
|
||||
msgid "Private Token"
|
||||
msgstr "SSH密钥"
|
||||
|
||||
#: authentication/models.py:45 users/templates/users/user_detail.html:258
|
||||
msgid "Reviewers"
|
||||
msgstr "审批人"
|
||||
|
||||
#: authentication/models.py:71 tickets/const.py:9
|
||||
#: users/templates/users/user_detail.html:250
|
||||
msgid "Login confirm"
|
||||
msgstr "登录复核"
|
||||
|
||||
#: authentication/models.py:94
|
||||
msgid "Expired"
|
||||
msgstr "过期时间"
|
||||
|
@ -1375,7 +1450,7 @@ msgstr "删除成功"
|
|||
|
||||
#: authentication/templates/authentication/_access_key_modal.html:155
|
||||
#: authentication/templates/authentication/_mfa_confirm_modal.html:53
|
||||
#: templates/_modal.html:22 tickets/const.py:18
|
||||
#: templates/_modal.html:22 tickets/const.py:19
|
||||
msgid "Close"
|
||||
msgstr "关闭"
|
||||
|
||||
|
@ -3161,19 +3236,15 @@ msgstr "申请资产"
|
|||
msgid "Apply for application"
|
||||
msgstr "申请应用"
|
||||
|
||||
#: tickets/const.py:15 tickets/const.py:22
|
||||
#: tickets/const.py:16 tickets/const.py:23
|
||||
msgid "Open"
|
||||
msgstr "打开"
|
||||
|
||||
#: tickets/const.py:16
|
||||
#: tickets/const.py:17
|
||||
msgid "Approve"
|
||||
msgstr "同意"
|
||||
|
||||
#: tickets/const.py:17
|
||||
msgid "Reject"
|
||||
msgstr "拒绝"
|
||||
|
||||
#: tickets/const.py:23
|
||||
#: tickets/const.py:24
|
||||
msgid "Closed"
|
||||
msgstr "关闭"
|
||||
|
||||
|
@ -3288,17 +3359,29 @@ msgstr "工单申请信息"
|
|||
msgid "Ticket approved info"
|
||||
msgstr "工单批准信息"
|
||||
|
||||
#: tickets/handler/login_asset_confirm.py:16
|
||||
msgid "Applied login user"
|
||||
msgstr "申请登录的用户"
|
||||
|
||||
#: tickets/handler/login_asset_confirm.py:17
|
||||
msgid "Applied login asset"
|
||||
msgstr "申请登录的资产"
|
||||
|
||||
#: tickets/handler/login_asset_confirm.py:18
|
||||
msgid "Applied login system user"
|
||||
msgstr "申请登录的系统用户"
|
||||
|
||||
#: tickets/handler/login_confirm.py:16
|
||||
msgid "Applied login IP"
|
||||
msgstr "申请的登录IP"
|
||||
msgstr "申请登录的IP"
|
||||
|
||||
#: tickets/handler/login_confirm.py:17
|
||||
msgid "Applied login city"
|
||||
msgstr "申请的登录城市"
|
||||
msgstr "申请登录的城市"
|
||||
|
||||
#: tickets/handler/login_confirm.py:18
|
||||
msgid "Applied login datetime"
|
||||
msgstr "申请的登录日期"
|
||||
msgstr "申请登录的日期"
|
||||
|
||||
#: tickets/models/comment.py:19
|
||||
msgid "User display name"
|
||||
|
@ -3421,6 +3504,18 @@ msgstr "在组织 `{}` 下没有发现 `资产`"
|
|||
msgid "Created by ticket ({}-{})"
|
||||
msgstr "通过工单创建 ({}-{})"
|
||||
|
||||
#: tickets/serializers/ticket/meta/ticket_type/login_asset_confirm.py:13
|
||||
msgid "Login user"
|
||||
msgstr "登录用户"
|
||||
|
||||
#: tickets/serializers/ticket/meta/ticket_type/login_asset_confirm.py:14
|
||||
msgid "Login asset"
|
||||
msgstr "登录资产"
|
||||
|
||||
#: tickets/serializers/ticket/meta/ticket_type/login_asset_confirm.py:16
|
||||
msgid "Login system user"
|
||||
msgstr "登录系统用户"
|
||||
|
||||
#: tickets/serializers/ticket/meta/ticket_type/login_confirm.py:20
|
||||
msgid "Login datetime"
|
||||
msgstr "登录日期"
|
||||
|
@ -3439,10 +3534,6 @@ msgid ""
|
|||
"request url (`{}`)"
|
||||
msgstr "提交数据中的类型 (`{}`) 与请求URL地址中的类型 (`{}`) 不一致"
|
||||
|
||||
#: tickets/serializers/ticket/ticket.py:109
|
||||
msgid "The organization `{}` does not exist"
|
||||
msgstr "组织 `{}` 不存在"
|
||||
|
||||
#: tickets/serializers/ticket/ticket.py:120
|
||||
msgid "None of the assignees belong to Organization `{}` admins"
|
||||
msgstr "所有受理人都不属于组织 `{}` 下的管理员"
|
||||
|
@ -4764,95 +4855,95 @@ msgstr "实例"
|
|||
|
||||
#: xpack/plugins/cloud/providers/aws_international.py:17
|
||||
msgid "China (Beijing)"
|
||||
msgstr ""
|
||||
msgstr "中国(北京)"
|
||||
|
||||
#: xpack/plugins/cloud/providers/aws_international.py:18
|
||||
msgid "China (Ningxia)"
|
||||
msgstr ""
|
||||
msgstr "中国(宁夏)"
|
||||
|
||||
#: xpack/plugins/cloud/providers/aws_international.py:21
|
||||
msgid "US East (Ohio)"
|
||||
msgstr ""
|
||||
msgstr "美国东部(俄亥俄州)"
|
||||
|
||||
#: xpack/plugins/cloud/providers/aws_international.py:22
|
||||
msgid "US East (N. Virginia)"
|
||||
msgstr ""
|
||||
msgstr "美国东部(弗吉尼亚北部)"
|
||||
|
||||
#: xpack/plugins/cloud/providers/aws_international.py:23
|
||||
msgid "US West (N. California)"
|
||||
msgstr ""
|
||||
msgstr "美国西部(加利福尼亚北部)"
|
||||
|
||||
#: xpack/plugins/cloud/providers/aws_international.py:24
|
||||
msgid "US West (Oregon)"
|
||||
msgstr ""
|
||||
msgstr "美国西部(俄勒冈)"
|
||||
|
||||
#: xpack/plugins/cloud/providers/aws_international.py:25
|
||||
msgid "Africa (Cape Town)"
|
||||
msgstr ""
|
||||
msgstr "非洲(开普敦)"
|
||||
|
||||
#: xpack/plugins/cloud/providers/aws_international.py:26
|
||||
msgid "Asia Pacific (Hong Kong)"
|
||||
msgstr "亚太-香港"
|
||||
msgstr "亚太地区(香港)"
|
||||
|
||||
#: xpack/plugins/cloud/providers/aws_international.py:27
|
||||
msgid "Asia Pacific (Mumbai)"
|
||||
msgstr ""
|
||||
msgstr "亚太地区(孟买)"
|
||||
|
||||
#: xpack/plugins/cloud/providers/aws_international.py:28
|
||||
msgid "Asia Pacific (Osaka-Local)"
|
||||
msgstr ""
|
||||
msgstr "亚太区域(大阪当地)"
|
||||
|
||||
#: xpack/plugins/cloud/providers/aws_international.py:29
|
||||
msgid "Asia Pacific (Seoul)"
|
||||
msgstr ""
|
||||
msgstr "亚太区域(首尔)"
|
||||
|
||||
#: xpack/plugins/cloud/providers/aws_international.py:30
|
||||
msgid "Asia Pacific (Singapore)"
|
||||
msgstr "亚太-新加坡"
|
||||
msgstr "亚太区域(新加坡)"
|
||||
|
||||
#: xpack/plugins/cloud/providers/aws_international.py:31
|
||||
msgid "Asia Pacific (Sydney)"
|
||||
msgstr ""
|
||||
msgstr "亚太区域(悉尼)"
|
||||
|
||||
#: xpack/plugins/cloud/providers/aws_international.py:32
|
||||
msgid "Asia Pacific (Tokyo)"
|
||||
msgstr ""
|
||||
msgstr "亚太区域(东京)"
|
||||
|
||||
#: xpack/plugins/cloud/providers/aws_international.py:33
|
||||
msgid "Canada (Central)"
|
||||
msgstr ""
|
||||
msgstr "加拿大(中部)"
|
||||
|
||||
#: xpack/plugins/cloud/providers/aws_international.py:34
|
||||
msgid "Europe (Frankfurt)"
|
||||
msgstr ""
|
||||
msgstr "欧洲(法兰克福)"
|
||||
|
||||
#: xpack/plugins/cloud/providers/aws_international.py:35
|
||||
msgid "Europe (Ireland)"
|
||||
msgstr ""
|
||||
msgstr "欧洲(爱尔兰)"
|
||||
|
||||
#: xpack/plugins/cloud/providers/aws_international.py:36
|
||||
msgid "Europe (London)"
|
||||
msgstr ""
|
||||
msgstr "欧洲(伦敦)"
|
||||
|
||||
#: xpack/plugins/cloud/providers/aws_international.py:37
|
||||
msgid "Europe (Milan)"
|
||||
msgstr ""
|
||||
msgstr "欧洲(米兰)"
|
||||
|
||||
#: xpack/plugins/cloud/providers/aws_international.py:38
|
||||
msgid "Europe (Paris)"
|
||||
msgstr ""
|
||||
msgstr "欧洲(巴黎)"
|
||||
|
||||
#: xpack/plugins/cloud/providers/aws_international.py:39
|
||||
msgid "Europe (Stockholm)"
|
||||
msgstr ""
|
||||
msgstr "欧洲(斯德哥尔摩)"
|
||||
|
||||
#: xpack/plugins/cloud/providers/aws_international.py:40
|
||||
msgid "Middle East (Bahrain)"
|
||||
msgstr ""
|
||||
msgstr "中东(巴林)"
|
||||
|
||||
#: xpack/plugins/cloud/providers/aws_international.py:41
|
||||
msgid "South America (São Paulo)"
|
||||
msgstr ""
|
||||
msgstr "南美洲(圣保罗)"
|
||||
|
||||
#: xpack/plugins/cloud/providers/huaweicloud.py:35
|
||||
msgid "AF-Johannesburg"
|
||||
|
@ -4912,19 +5003,19 @@ msgstr ""
|
|||
|
||||
#: xpack/plugins/cloud/serializers.py:28
|
||||
msgid "Client ID"
|
||||
msgstr "Client ID"
|
||||
msgstr ""
|
||||
|
||||
#: xpack/plugins/cloud/serializers.py:31
|
||||
msgid "Client Secret"
|
||||
msgstr "Client Secret"
|
||||
msgstr ""
|
||||
|
||||
#: xpack/plugins/cloud/serializers.py:34
|
||||
msgid "Tenant ID"
|
||||
msgstr "租户ID"
|
||||
msgstr ""
|
||||
|
||||
#: xpack/plugins/cloud/serializers.py:37
|
||||
msgid "Subscription ID"
|
||||
msgstr "订阅ID"
|
||||
msgstr ""
|
||||
|
||||
#: xpack/plugins/cloud/serializers.py:115
|
||||
msgid "History count"
|
||||
|
@ -5026,31 +5117,3 @@ msgstr "旗舰版"
|
|||
#: xpack/plugins/license/models.py:77
|
||||
msgid "Community edition"
|
||||
msgstr "社区版"
|
||||
|
||||
#~ msgid "Captcha invalid"
|
||||
#~ msgstr "验证码错误"
|
||||
|
||||
#~ msgid ""
|
||||
#~ "Not support openssh format key, using ssh-keygen -t rsa -m pem to generate"
|
||||
#~ msgstr "暂不支持OPENSSH格式的密钥,使用 ssh-keygen -t rsa -m pem生成"
|
||||
|
||||
#~ msgid "Select users"
|
||||
#~ msgstr "选择用户"
|
||||
|
||||
#~ msgid "Paste user id_rsa.pub here."
|
||||
#~ msgstr "复制用户公钥到这里"
|
||||
|
||||
#~ msgid "Available"
|
||||
#~ msgstr "有效"
|
||||
|
||||
#~ msgid "Unavailable"
|
||||
#~ msgstr "无效"
|
||||
|
||||
#~ msgid "Instances"
|
||||
#~ msgstr "实例"
|
||||
|
||||
#~ msgid "LA-Santiago"
|
||||
#~ msgstr "拉美-圣地亚哥"
|
||||
|
||||
#~ msgid "Please wait while your data is being initialized"
|
||||
#~ msgstr "数据正在初始化,请稍等"
|
||||
|
|
|
@ -67,8 +67,6 @@ def get_current_org_id():
|
|||
|
||||
def get_current_org_id_for_serializer():
|
||||
org_id = get_current_org_id()
|
||||
if org_id == Organization.DEFAULT_ID:
|
||||
org_id = ''
|
||||
return org_id
|
||||
|
||||
|
||||
|
|
|
@ -56,7 +56,7 @@ class CommandStorage(CommonModelMixin):
|
|||
return storage.ping()
|
||||
|
||||
def is_use(self):
|
||||
return Terminal.objects.filter(command_storage=self.name).exists()
|
||||
return Terminal.objects.filter(command_storage=self.name, is_deleted=False).exists()
|
||||
|
||||
def get_command_queryset(self):
|
||||
if self.type_server:
|
||||
|
|
|
@ -54,14 +54,14 @@ class TicketViewSet(CommonApiMixin, viewsets.ModelViewSet):
|
|||
def open(self, request, *args, **kwargs):
|
||||
return super().create(request, *args, **kwargs)
|
||||
|
||||
@action(detail=True, methods=[PUT], permission_classes=[IsOrgAdmin, IsAssignee, NotClosed])
|
||||
@action(detail=True, methods=[PUT], permission_classes=[IsAssignee, NotClosed])
|
||||
def approve(self, request, *args, **kwargs):
|
||||
response = super().update(request, *args, **kwargs)
|
||||
instance = self.get_object()
|
||||
instance.approve(processor=self.request.user)
|
||||
return response
|
||||
|
||||
@action(detail=True, methods=[PUT], permission_classes=[IsOrgAdmin, IsAssignee, NotClosed])
|
||||
@action(detail=True, methods=[PUT], permission_classes=[IsAssignee, NotClosed])
|
||||
def reject(self, request, *args, **kwargs):
|
||||
instance = self.get_object()
|
||||
serializer = self.get_serializer(instance)
|
||||
|
|
|
@ -9,6 +9,7 @@ class TicketTypeChoices(TextChoices):
|
|||
login_confirm = 'login_confirm', _("Login confirm")
|
||||
apply_asset = 'apply_asset', _('Apply for asset')
|
||||
apply_application = 'apply_application', _('Apply for application')
|
||||
login_asset_confirm = 'login_asset_confirm', _('Login asset confirm')
|
||||
|
||||
|
||||
class TicketActionChoices(TextChoices):
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
from django.utils.translation import ugettext as _
|
||||
from .base import BaseHandler
|
||||
|
||||
|
||||
class Handler(BaseHandler):
|
||||
|
||||
# body
|
||||
def _construct_meta_body_of_open(self):
|
||||
apply_login_user = self.ticket.meta.get('apply_login_user')
|
||||
apply_login_asset = self.ticket.meta.get('apply_login_asset')
|
||||
apply_login_system_user = self.ticket.meta.get('apply_login_system_user')
|
||||
applied_body = '''{}: {},
|
||||
{}: {},
|
||||
{}: {}
|
||||
'''.format(
|
||||
_("Applied login user"), apply_login_user,
|
||||
_("Applied login asset"), apply_login_asset,
|
||||
_("Applied login system user"), apply_login_system_user,
|
||||
)
|
||||
return applied_body
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 3.1 on 2021-03-11 03:13
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('tickets', '0007_auto_20201224_1821'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='ticket',
|
||||
name='type',
|
||||
field=models.CharField(choices=[('general', 'General'), ('login_confirm', 'Login confirm'), ('apply_asset', 'Apply for asset'), ('apply_application', 'Apply for application'), ('login_asset_confirm', 'Login asset confirm')], default='general', max_length=64, verbose_name='Type'),
|
||||
),
|
||||
]
|
|
@ -1,5 +1,5 @@
|
|||
from tickets import const
|
||||
from .ticket_type import apply_asset, apply_application, login_confirm
|
||||
from .ticket_type import apply_asset, apply_application, login_confirm, login_asset_confirm
|
||||
|
||||
__all__ = [
|
||||
'type_serializer_classes_mapping',
|
||||
|
@ -30,5 +30,10 @@ type_serializer_classes_mapping = {
|
|||
'default': login_confirm.LoginConfirmSerializer,
|
||||
action_open: login_confirm.ApplySerializer,
|
||||
action_approve: login_confirm.LoginConfirmSerializer(read_only=True),
|
||||
},
|
||||
const.TicketTypeChoices.login_asset_confirm.value: {
|
||||
'default': login_asset_confirm.LoginAssetConfirmSerializer,
|
||||
action_open: login_asset_confirm.ApplySerializer,
|
||||
action_approve: login_asset_confirm.LoginAssetConfirmSerializer(read_only=True),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
|
||||
from rest_framework import serializers
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
|
||||
__all__ = [
|
||||
'ApplySerializer', 'LoginAssetConfirmSerializer',
|
||||
]
|
||||
|
||||
|
||||
class ApplySerializer(serializers.Serializer):
|
||||
# 申请信息
|
||||
apply_login_user = serializers.CharField(required=True, label=_('Login user'))
|
||||
apply_login_asset = serializers.CharField(required=True, label=_('Login asset'))
|
||||
apply_login_system_user = serializers.CharField(
|
||||
required=True, max_length=64, label=_('Login system user')
|
||||
)
|
||||
|
||||
|
||||
class LoginAssetConfirmSerializer(ApplySerializer):
|
||||
pass
|
Loading…
Reference in New Issue