* 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
Jiangjie.Bai 2021-03-11 20:17:44 +08:00 committed by GitHub
parent 09303ecc56
commit 64641a18e6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 1166 additions and 262 deletions

0
apps/acls/__init__.py Normal file
View File

3
apps/acls/admin.py Normal file
View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

View File

@ -0,0 +1,3 @@
from .login_acl import *
from .login_asset_acl import *
from .login_asset_check import *

View File

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

View File

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

View File

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

5
apps/acls/apps.py Normal file
View File

@ -0,0 +1,5 @@
from django.apps import AppConfig
class AclsConfig(AppConfig):
name = 'acls'

9
apps/acls/const.py Normal file
View File

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

View File

@ -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')},
},
),
]

View File

View File

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

35
apps/acls/models/base.py Normal file
View File

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

View File

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

View File

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

View File

@ -0,0 +1,3 @@
from .login_acl import *
from .login_asset_acl import *
from .login_asset_check import *

View File

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

View File

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

View File

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

3
apps/acls/tests.py Normal file
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

View File

@ -0,0 +1 @@
from .api_urls import *

View File

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

68
apps/acls/utils.py Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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-1001最低优先级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 "数据正在初始化,请稍等"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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