mirror of https://github.com/jumpserver/jumpserver
Merge branch 'v3' of github.com:jumpserver/jumpserver into v3
commit
5e503ec5b8
|
@ -1,5 +1,5 @@
|
||||||
# ~*~ coding: utf-8 ~*~
|
# ~*~ coding: utf-8 ~*~
|
||||||
|
from django.db.models import F
|
||||||
from django.views.generic.detail import SingleObjectMixin
|
from django.views.generic.detail import SingleObjectMixin
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
from rest_framework.views import APIView, Response
|
from rest_framework.views import APIView, Response
|
||||||
|
@ -29,6 +29,7 @@ class DomainViewSet(OrgBulkModelViewSet):
|
||||||
|
|
||||||
|
|
||||||
class GatewayViewSet(OrgBulkModelViewSet):
|
class GatewayViewSet(OrgBulkModelViewSet):
|
||||||
|
perm_model = Host
|
||||||
filterset_fields = ("domain__name", "name", "domain")
|
filterset_fields = ("domain__name", "name", "domain")
|
||||||
search_fields = ("domain__name",)
|
search_fields = ("domain__name",)
|
||||||
serializer_class = serializers.GatewaySerializer
|
serializer_class = serializers.GatewaySerializer
|
||||||
|
|
|
@ -207,17 +207,6 @@ class Asset(NodesRelationMixin, AbsConnectivity, JMSOrgBaseModel):
|
||||||
tree_node = TreeNode(**data)
|
tree_node = TreeNode(**data)
|
||||||
return tree_node
|
return tree_node
|
||||||
|
|
||||||
def filter_accounts(self, account_names=None):
|
|
||||||
from perms.models import AssetPermission
|
|
||||||
if account_names is None:
|
|
||||||
return self.accounts.all()
|
|
||||||
if AssetPermission.SpecialAccount.ALL in account_names:
|
|
||||||
return self.accounts.all()
|
|
||||||
# queries = Q(name__in=account_names) | Q(username__in=account_names)
|
|
||||||
queries = Q(username__in=account_names)
|
|
||||||
accounts = self.accounts.filter(queries)
|
|
||||||
return accounts
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = [('org_id', 'name')]
|
unique_together = [('org_id', 'name')]
|
||||||
verbose_name = _("Asset")
|
verbose_name = _("Asset")
|
||||||
|
|
|
@ -3,7 +3,6 @@ from .common import Asset
|
||||||
|
|
||||||
|
|
||||||
class Host(Asset):
|
class Host(Asset):
|
||||||
pass
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_gateway_queryset(cls):
|
def get_gateway_queryset(cls):
|
||||||
|
|
|
@ -1,23 +1,22 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
#
|
#
|
||||||
import io
|
|
||||||
import os
|
import os
|
||||||
import sshpubkeys
|
|
||||||
from hashlib import md5
|
from hashlib import md5
|
||||||
|
|
||||||
from django.db import models
|
import sshpubkeys
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils import timezone
|
from django.db import models
|
||||||
from django.db.models import QuerySet
|
from django.db.models import QuerySet
|
||||||
|
from django.utils import timezone
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
from assets.const import Connectivity, SecretType
|
||||||
|
from common.db import fields
|
||||||
from common.utils import (
|
from common.utils import (
|
||||||
ssh_key_string_to_obj, ssh_key_gen, get_logger,
|
ssh_key_string_to_obj, ssh_key_gen, get_logger,
|
||||||
random_string, ssh_pubkey_gen, lazyproperty
|
random_string, lazyproperty, parse_ssh_public_key_str
|
||||||
)
|
)
|
||||||
from common.db import fields
|
|
||||||
from orgs.mixins.models import JMSOrgBaseModel
|
from orgs.mixins.models import JMSOrgBaseModel
|
||||||
from assets.const import Connectivity, SecretType
|
|
||||||
|
|
||||||
logger = get_logger(__file__)
|
logger = get_logger(__file__)
|
||||||
|
|
||||||
|
@ -62,6 +61,10 @@ class BaseAccount(JMSOrgBaseModel):
|
||||||
def has_secret(self):
|
def has_secret(self):
|
||||||
return bool(self.secret)
|
return bool(self.secret)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_username(self):
|
||||||
|
return bool(self.username)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def specific(self):
|
def specific(self):
|
||||||
data = {}
|
data = {}
|
||||||
|
@ -84,7 +87,7 @@ class BaseAccount(JMSOrgBaseModel):
|
||||||
@lazyproperty
|
@lazyproperty
|
||||||
def public_key(self):
|
def public_key(self):
|
||||||
if self.secret_type == SecretType.SSH_KEY:
|
if self.secret_type == SecretType.SSH_KEY:
|
||||||
return ssh_pubkey_gen(private_key=self.private_key)
|
return parse_ssh_public_key_str(self.private_key)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -93,7 +96,7 @@ class BaseAccount(JMSOrgBaseModel):
|
||||||
public_key = self.public_key
|
public_key = self.public_key
|
||||||
elif self.private_key:
|
elif self.private_key:
|
||||||
try:
|
try:
|
||||||
public_key = ssh_pubkey_gen(private_key=self.private_key)
|
public_key = parse_ssh_public_key_str(self.private_key)
|
||||||
except IOError as e:
|
except IOError as e:
|
||||||
return str(e)
|
return str(e)
|
||||||
else:
|
else:
|
||||||
|
@ -125,12 +128,9 @@ class BaseAccount(JMSOrgBaseModel):
|
||||||
return key_path
|
return key_path
|
||||||
|
|
||||||
def get_private_key(self):
|
def get_private_key(self):
|
||||||
if not self.private_key_obj:
|
if not self.private_key:
|
||||||
return None
|
return None
|
||||||
string_io = io.StringIO()
|
return self.private_key
|
||||||
self.private_key_obj.write_private_key(string_io)
|
|
||||||
private_key = string_io.getvalue()
|
|
||||||
return private_key
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def public_key_obj(self):
|
def public_key_obj(self):
|
||||||
|
|
|
@ -183,7 +183,7 @@ class CommandFilterRule(OrgModelMixin):
|
||||||
cls, user_id=None, user_group_id=None, account=None,
|
cls, user_id=None, user_group_id=None, account=None,
|
||||||
asset_id=None, org_id=None
|
asset_id=None, org_id=None
|
||||||
):
|
):
|
||||||
from perms.models.const import SpecialAccount
|
from assets.models import Account
|
||||||
user_groups = []
|
user_groups = []
|
||||||
user = get_object_or_none(User, pk=user_id)
|
user = get_object_or_none(User, pk=user_id)
|
||||||
if user:
|
if user:
|
||||||
|
@ -202,7 +202,7 @@ class CommandFilterRule(OrgModelMixin):
|
||||||
if account:
|
if account:
|
||||||
org_id = account.org_id
|
org_id = account.org_id
|
||||||
q |= Q(accounts__contains=account.username) | \
|
q |= Q(accounts__contains=account.username) | \
|
||||||
Q(accounts__contains=SpecialAccount.ALL.value)
|
Q(accounts__contains=Account.AliasAccount.ALL)
|
||||||
if asset:
|
if asset:
|
||||||
org_id = asset.org_id
|
org_id = asset.org_id
|
||||||
q |= Q(assets=asset)
|
q |= Q(assets=asset)
|
||||||
|
|
|
@ -13,8 +13,9 @@ from django.utils.translation import ugettext_lazy as _
|
||||||
from common.db import fields
|
from common.db import fields
|
||||||
from common.utils import get_logger, lazyproperty
|
from common.utils import get_logger, lazyproperty
|
||||||
from orgs.mixins.models import OrgModelMixin
|
from orgs.mixins.models import OrgModelMixin
|
||||||
|
from assets.models import Host
|
||||||
from .base import BaseAccount
|
from .base import BaseAccount
|
||||||
from ..const import SecretType, GATEWAY_NAME
|
from ..const import SecretType
|
||||||
|
|
||||||
logger = get_logger(__file__)
|
logger = get_logger(__file__)
|
||||||
|
|
||||||
|
@ -37,7 +38,7 @@ class Domain(OrgModelMixin):
|
||||||
|
|
||||||
@lazyproperty
|
@lazyproperty
|
||||||
def gateways(self):
|
def gateways(self):
|
||||||
return self.assets.filter(platform__name=GATEWAY_NAME, is_active=True)
|
return Host.get_gateway_queryset().filter(domain=self, is_active=True)
|
||||||
|
|
||||||
def select_gateway(self):
|
def select_gateway(self):
|
||||||
return self.random_gateway()
|
return self.random_gateway()
|
||||||
|
|
|
@ -1,28 +1,34 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
#
|
#
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
from rest_framework.generics import get_object_or_404
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
from orgs.mixins.serializers import BulkOrgResourceModelSerializer, OrgResourceSerializerMixin
|
||||||
from common.drf.serializers import SecretReadableMixin
|
from common.drf.serializers import SecretReadableMixin, WritableNestedModelSerializer
|
||||||
from ..models import Domain, Asset
|
from common.drf.fields import ObjectRelatedField, EncryptedField
|
||||||
|
from assets.models import Platform, Node
|
||||||
|
from assets.const import SecretType, GATEWAY_NAME
|
||||||
|
from ..serializers import AssetProtocolsSerializer
|
||||||
|
from ..models import Domain, Asset, Account, Host
|
||||||
|
from .utils import validate_password_for_ansible, validate_ssh_key
|
||||||
|
|
||||||
|
|
||||||
class DomainSerializer(BulkOrgResourceModelSerializer):
|
class DomainSerializer(BulkOrgResourceModelSerializer):
|
||||||
asset_count = serializers.SerializerMethodField(label=_('Assets amount'))
|
asset_count = serializers.SerializerMethodField(label=_('Assets amount'))
|
||||||
gateway_count = serializers.SerializerMethodField(label=_('Gateways count'))
|
gateway_count = serializers.SerializerMethodField(label=_('Gateways count'))
|
||||||
|
assets = ObjectRelatedField(
|
||||||
|
many=True, required=False, queryset=Asset.objects, label=_('Asset')
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Domain
|
model = Domain
|
||||||
fields_mini = ['id', 'name']
|
fields_mini = ['id', 'name']
|
||||||
fields_small = fields_mini + [
|
fields_small = fields_mini + ['comment']
|
||||||
'comment', 'date_created'
|
fields_m2m = ['assets']
|
||||||
]
|
read_only_fields = ['asset_count', 'gateway_count', 'date_created']
|
||||||
fields_m2m = [
|
fields = fields_small + fields_m2m + read_only_fields
|
||||||
'asset_count', 'assets', 'gateway_count',
|
|
||||||
]
|
|
||||||
fields = fields_small + fields_m2m
|
|
||||||
read_only_fields = ('asset_count', 'gateway_count', 'date_created')
|
|
||||||
extra_kwargs = {
|
extra_kwargs = {
|
||||||
'assets': {'required': False, 'label': _('Assets')},
|
'assets': {'required': False, 'label': _('Assets')},
|
||||||
}
|
}
|
||||||
|
@ -36,20 +42,110 @@ class DomainSerializer(BulkOrgResourceModelSerializer):
|
||||||
return obj.gateways.count()
|
return obj.gateways.count()
|
||||||
|
|
||||||
|
|
||||||
class GatewaySerializer(BulkOrgResourceModelSerializer):
|
class GatewaySerializer(BulkOrgResourceModelSerializer, WritableNestedModelSerializer):
|
||||||
is_connective = serializers.BooleanField(required=False, label=_('Connectivity'))
|
password = EncryptedField(
|
||||||
|
label=_('Password'), required=False, allow_blank=True, allow_null=True, max_length=1024,
|
||||||
|
validators=[validate_password_for_ansible], write_only=True
|
||||||
|
)
|
||||||
|
private_key = EncryptedField(
|
||||||
|
label=_('SSH private key'), required=False, allow_blank=True, allow_null=True,
|
||||||
|
max_length=16384, write_only=True
|
||||||
|
)
|
||||||
|
passphrase = serializers.CharField(
|
||||||
|
label=_('Key password'), allow_blank=True, allow_null=True, required=False, write_only=True,
|
||||||
|
max_length=512,
|
||||||
|
)
|
||||||
|
username = serializers.CharField(
|
||||||
|
label=_('Username'), allow_blank=True, max_length=128, required=True, write_only=True
|
||||||
|
)
|
||||||
|
username_display = serializers.SerializerMethodField(label=_('Username'))
|
||||||
|
protocols = AssetProtocolsSerializer(many=True, required=False, label=_('Protocols'))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Asset
|
model = Host
|
||||||
fields_mini = ['id']
|
fields_mini = ['id', 'name', 'address']
|
||||||
fields_small = fields_mini + [
|
fields_small = fields_mini + ['is_active', 'comment']
|
||||||
'address', 'port', 'protocol',
|
fields = fields_small + ['domain', 'protocols'] + [
|
||||||
'is_active', 'is_connective',
|
'username', 'password', 'private_key', 'passphrase', 'username_display'
|
||||||
'date_created', 'date_updated',
|
|
||||||
'created_by', 'comment',
|
|
||||||
]
|
]
|
||||||
fields_fk = ['domain']
|
extra_kwargs = {
|
||||||
fields = fields_small + fields_fk
|
'name': {'label': _("Name")},
|
||||||
|
'address': {'label': _('Address')},
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_username_display(obj):
|
||||||
|
account = obj.accounts.order_by('-privileged').first()
|
||||||
|
return account.username if account else ''
|
||||||
|
|
||||||
|
def validate_private_key(self, secret):
|
||||||
|
if not secret:
|
||||||
|
return
|
||||||
|
passphrase = self.initial_data.get('passphrase')
|
||||||
|
passphrase = passphrase if passphrase else None
|
||||||
|
validate_ssh_key(secret, passphrase)
|
||||||
|
return secret
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def clean_auth_fields(validated_data):
|
||||||
|
username = validated_data.pop('username', None)
|
||||||
|
password = validated_data.pop('password', None)
|
||||||
|
private_key = validated_data.pop('private_key', None)
|
||||||
|
validated_data.pop('passphrase', None)
|
||||||
|
return username, password, private_key
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def generate_default_data():
|
||||||
|
platform = Platform.objects.get(name=GATEWAY_NAME, internal=True)
|
||||||
|
# node = Node.objects.all().order_by('date_created').first()
|
||||||
|
data = {
|
||||||
|
'platform': platform,
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create_accounts(instance, username, password, private_key):
|
||||||
|
account_name = f'{instance.name}-{_("Gateway")}'
|
||||||
|
account_data = {
|
||||||
|
'privileged': True,
|
||||||
|
'name': account_name,
|
||||||
|
'username': username,
|
||||||
|
'asset_id': instance.id,
|
||||||
|
'created_by': instance.created_by
|
||||||
|
}
|
||||||
|
if password:
|
||||||
|
Account.objects.create(
|
||||||
|
**account_data, secret=password, secret_type=SecretType.PASSWORD
|
||||||
|
)
|
||||||
|
if private_key:
|
||||||
|
Account.objects.create(
|
||||||
|
**account_data, secret=private_key, secret_type=SecretType.SSH_KEY
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def update_accounts(instance, username, password, private_key):
|
||||||
|
accounts = instance.accounts.filter(username=username)
|
||||||
|
if password:
|
||||||
|
account = get_object_or_404(accounts, SecretType.PASSWORD)
|
||||||
|
account.secret = password
|
||||||
|
account.save()
|
||||||
|
if private_key:
|
||||||
|
account = get_object_or_404(accounts, SecretType.SSH_KEY)
|
||||||
|
account.secret = private_key
|
||||||
|
account.save()
|
||||||
|
|
||||||
|
def create(self, validated_data):
|
||||||
|
auth_fields = self.clean_auth_fields(validated_data)
|
||||||
|
validated_data.update(self.generate_default_data())
|
||||||
|
instance = super().create(validated_data)
|
||||||
|
self.create_accounts(instance, *auth_fields)
|
||||||
|
return instance
|
||||||
|
|
||||||
|
def update(self, instance, validated_data):
|
||||||
|
auth_fields = self.clean_auth_fields(validated_data)
|
||||||
|
instance = super().update(instance, validated_data)
|
||||||
|
self.update_accounts(instance, *auth_fields)
|
||||||
|
return instance
|
||||||
|
|
||||||
|
|
||||||
class GatewayWithAuthSerializer(SecretReadableMixin, GatewaySerializer):
|
class GatewayWithAuthSerializer(SecretReadableMixin, GatewaySerializer):
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
from io import StringIO
|
|
||||||
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from common.utils import ssh_private_key_gen, validate_ssh_private_key
|
from common.utils import validate_ssh_private_key, parse_ssh_private_key_str
|
||||||
|
|
||||||
|
|
||||||
def validate_password_for_ansible(password):
|
def validate_password_for_ansible(password):
|
||||||
|
@ -24,9 +22,4 @@ def validate_ssh_key(ssh_key, passphrase=None):
|
||||||
valid = validate_ssh_private_key(ssh_key, password=passphrase)
|
valid = validate_ssh_private_key(ssh_key, password=passphrase)
|
||||||
if not valid:
|
if not valid:
|
||||||
raise serializers.ValidationError(_("private key invalid or passphrase error"))
|
raise serializers.ValidationError(_("private key invalid or passphrase error"))
|
||||||
|
return parse_ssh_private_key_str(ssh_key, passphrase)
|
||||||
ssh_key = ssh_private_key_gen(ssh_key, password=passphrase)
|
|
||||||
string_io = StringIO()
|
|
||||||
ssh_key.write_private_key(string_io)
|
|
||||||
ssh_key = string_io.getvalue()
|
|
||||||
return ssh_key
|
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 3.2.14 on 2022-11-23 02:26
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('authentication', '0014_auto_20221122_2152'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='connectiontoken',
|
||||||
|
name='login',
|
||||||
|
field=models.CharField(max_length=128, verbose_name='Login account'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -1,24 +1,23 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
#
|
#
|
||||||
import re
|
|
||||||
import json
|
|
||||||
from six import string_types
|
|
||||||
import base64
|
import base64
|
||||||
import os
|
|
||||||
import time
|
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import time
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
from itertools import chain
|
|
||||||
|
|
||||||
import paramiko
|
import paramiko
|
||||||
import sshpubkeys
|
import sshpubkeys
|
||||||
|
from cryptography.hazmat.primitives import serialization
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.serializers.json import DjangoJSONEncoder
|
||||||
from itsdangerous import (
|
from itsdangerous import (
|
||||||
TimedJSONWebSignatureSerializer, JSONWebSignatureSerializer,
|
TimedJSONWebSignatureSerializer, JSONWebSignatureSerializer,
|
||||||
BadSignature, SignatureExpired
|
BadSignature, SignatureExpired
|
||||||
)
|
)
|
||||||
from django.conf import settings
|
from six import string_types
|
||||||
from django.core.serializers.json import DjangoJSONEncoder
|
|
||||||
from django.db.models.fields.files import FileField
|
|
||||||
|
|
||||||
from .http import http_date
|
from .http import http_date
|
||||||
|
|
||||||
|
@ -69,22 +68,19 @@ class Signer(metaclass=Singleton):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
_supported_paramiko_ssh_key_types = (paramiko.RSAKey, paramiko.DSSKey, paramiko.Ed25519Key)
|
||||||
|
|
||||||
|
|
||||||
def ssh_key_string_to_obj(text, password=None):
|
def ssh_key_string_to_obj(text, password=None):
|
||||||
key = None
|
key = None
|
||||||
try:
|
for ssh_key_type in _supported_paramiko_ssh_key_types:
|
||||||
key = paramiko.RSAKey.from_private_key(StringIO(text), password=password)
|
if not isinstance(ssh_key_type, paramiko.PKey):
|
||||||
except paramiko.SSHException:
|
continue
|
||||||
pass
|
try:
|
||||||
else:
|
key = ssh_key_type.from_private_key(StringIO(text), password=password)
|
||||||
return key
|
return key
|
||||||
|
except paramiko.SSHException:
|
||||||
try:
|
pass
|
||||||
key = paramiko.DSSKey.from_private_key(StringIO(text), password=password)
|
|
||||||
except paramiko.SSHException:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
return key
|
|
||||||
|
|
||||||
return key
|
return key
|
||||||
|
|
||||||
|
|
||||||
|
@ -137,17 +133,68 @@ def ssh_key_gen(length=2048, type='rsa', password=None, username='jumpserver', h
|
||||||
|
|
||||||
|
|
||||||
def validate_ssh_private_key(text, password=None):
|
def validate_ssh_private_key(text, password=None):
|
||||||
if isinstance(text, bytes):
|
if isinstance(text, str):
|
||||||
try:
|
try:
|
||||||
text = text.decode("utf-8")
|
text = text.encode("utf-8")
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
return False
|
||||||
|
if isinstance(password, str):
|
||||||
|
try:
|
||||||
|
password = password.encode("utf-8")
|
||||||
except UnicodeDecodeError:
|
except UnicodeDecodeError:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
key = ssh_key_string_to_obj(text, password=password)
|
key = parse_ssh_private_key_str(text, password=password)
|
||||||
if key is None:
|
return bool(key)
|
||||||
return False
|
|
||||||
else:
|
|
||||||
return True
|
def parse_ssh_private_key_str(text: bytes, password=None) -> str:
|
||||||
|
private_key = _parse_ssh_private_key(text, password=password)
|
||||||
|
if private_key is None:
|
||||||
|
return ""
|
||||||
|
private_key_bytes = private_key.private_bytes(serialization.Encoding.PEM,
|
||||||
|
serialization.PrivateFormat.OpenSSH,
|
||||||
|
serialization.NoEncryption())
|
||||||
|
return private_key_bytes.decode('utf-8')
|
||||||
|
|
||||||
|
|
||||||
|
def parse_ssh_public_key_str(text: bytes = "", password=None) -> str:
|
||||||
|
private_key = _parse_ssh_private_key(text, password=password)
|
||||||
|
if private_key is None:
|
||||||
|
return ""
|
||||||
|
public_key_bytes = private_key.public_key().public_bytes(serialization.Encoding.OpenSSH,
|
||||||
|
serialization.PublicFormat.OpenSSH)
|
||||||
|
return public_key_bytes.decode('utf-8')
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_ssh_private_key(text, password=None):
|
||||||
|
"""
|
||||||
|
text: bytes
|
||||||
|
password: str
|
||||||
|
return:private key types:
|
||||||
|
ec.EllipticCurvePrivateKey,
|
||||||
|
rsa.RSAPrivateKey,
|
||||||
|
dsa.DSAPrivateKey,
|
||||||
|
ed25519.Ed25519PrivateKey,
|
||||||
|
"""
|
||||||
|
if isinstance(text, str):
|
||||||
|
try:
|
||||||
|
text = text.encode("utf-8")
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
return None
|
||||||
|
if password is not None:
|
||||||
|
if isinstance(password, str):
|
||||||
|
try:
|
||||||
|
password = password.encode("utf-8")
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
private_key = serialization.load_ssh_private_key(text, password=password)
|
||||||
|
return private_key
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def validate_ssh_public_key(text):
|
def validate_ssh_public_key(text):
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
from django.shortcuts import get_object_or_404
|
|
||||||
from rest_framework import viewsets
|
from rest_framework import viewsets
|
||||||
|
|
||||||
from ops.models import Job, JobExecution
|
from ops.models import Job, JobExecution
|
||||||
|
@ -7,14 +6,17 @@ from ops.serializers.job import JobSerializer, JobExecutionSerializer
|
||||||
__all__ = ['JobViewSet', 'JobExecutionViewSet']
|
__all__ = ['JobViewSet', 'JobExecutionViewSet']
|
||||||
|
|
||||||
from ops.tasks import run_ops_job, run_ops_job_executions
|
from ops.tasks import run_ops_job, run_ops_job_executions
|
||||||
|
from orgs.mixins.api import OrgBulkModelViewSet
|
||||||
|
|
||||||
|
|
||||||
class JobViewSet(viewsets.ModelViewSet):
|
class JobViewSet(OrgBulkModelViewSet):
|
||||||
serializer_class = JobSerializer
|
serializer_class = JobSerializer
|
||||||
queryset = Job.objects.all()
|
model = Job
|
||||||
|
permission_classes = ()
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return self.queryset.filter(instant=False)
|
query_set = super().get_queryset()
|
||||||
|
return query_set.filter(instant=False)
|
||||||
|
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
instance = serializer.save()
|
instance = serializer.save()
|
||||||
|
@ -22,20 +24,20 @@ class JobViewSet(viewsets.ModelViewSet):
|
||||||
run_ops_job.delay(instance.id)
|
run_ops_job.delay(instance.id)
|
||||||
|
|
||||||
|
|
||||||
class JobExecutionViewSet(viewsets.ModelViewSet):
|
class JobExecutionViewSet(OrgBulkModelViewSet):
|
||||||
serializer_class = JobExecutionSerializer
|
serializer_class = JobExecutionSerializer
|
||||||
queryset = JobExecution.objects.all()
|
|
||||||
http_method_names = ('get', 'post', 'head', 'options',)
|
http_method_names = ('get', 'post', 'head', 'options',)
|
||||||
|
# filter_fields = ('type',)
|
||||||
|
permission_classes = ()
|
||||||
|
model = JobExecution
|
||||||
|
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
instance = serializer.save()
|
instance = serializer.save()
|
||||||
run_ops_job_executions.delay(instance.id)
|
run_ops_job_executions.delay(instance.id)
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
|
query_set = super().get_queryset()
|
||||||
job_id = self.request.query_params.get('job_id')
|
job_id = self.request.query_params.get('job_id')
|
||||||
job_type = self.request.query_params.get('type')
|
|
||||||
if job_id:
|
if job_id:
|
||||||
self.queryset = self.queryset.filter(job_id=job_id)
|
self.queryset = query_set.filter(job_id=job_id)
|
||||||
if job_type:
|
return query_set
|
||||||
self.queryset = self.queryset.filter(job__type=job_type)
|
|
||||||
return self.queryset
|
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 3.2.14 on 2022-11-23 09:45
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('ops', '0033_auto_20221118_1431'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='job',
|
||||||
|
name='org_id',
|
||||||
|
field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 3.2.14 on 2022-11-23 10:22
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('ops', '0034_job_org_id'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='jobexecution',
|
||||||
|
name='org_id',
|
||||||
|
field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -9,16 +9,14 @@ from django.utils.translation import gettext_lazy as _
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from celery import current_task
|
from celery import current_task
|
||||||
|
|
||||||
from common.const.choices import Trigger
|
|
||||||
from common.db.models import BaseCreateUpdateModel
|
|
||||||
|
|
||||||
__all__ = ["Job", "JobExecution"]
|
__all__ = ["Job", "JobExecution"]
|
||||||
|
|
||||||
from ops.ansible import JMSInventory, AdHocRunner, PlaybookRunner
|
from ops.ansible import JMSInventory, AdHocRunner, PlaybookRunner
|
||||||
from ops.mixin import PeriodTaskModelMixin
|
from ops.mixin import PeriodTaskModelMixin
|
||||||
|
from orgs.mixins.models import JMSOrgBaseModel
|
||||||
|
|
||||||
|
|
||||||
class Job(BaseCreateUpdateModel, PeriodTaskModelMixin):
|
class Job(JMSOrgBaseModel, PeriodTaskModelMixin):
|
||||||
class Types(models.TextChoices):
|
class Types(models.TextChoices):
|
||||||
adhoc = 'adhoc', _('Adhoc')
|
adhoc = 'adhoc', _('Adhoc')
|
||||||
playbook = 'playbook', _('Playbook')
|
playbook = 'playbook', _('Playbook')
|
||||||
|
@ -94,7 +92,7 @@ class Job(BaseCreateUpdateModel, PeriodTaskModelMixin):
|
||||||
return self.executions.create()
|
return self.executions.create()
|
||||||
|
|
||||||
|
|
||||||
class JobExecution(BaseCreateUpdateModel):
|
class JobExecution(JMSOrgBaseModel):
|
||||||
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||||
task_id = models.UUIDField(null=True)
|
task_id = models.UUIDField(null=True)
|
||||||
status = models.CharField(max_length=16, verbose_name=_('Status'), default='running')
|
status = models.CharField(max_length=16, verbose_name=_('Status'), default='running')
|
||||||
|
|
|
@ -1,14 +1,13 @@
|
||||||
from django.db import transaction
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from common.drf.fields import ReadableHiddenField
|
from common.drf.fields import ReadableHiddenField
|
||||||
from ops.mixin import PeriodTaskSerializerMixin
|
from ops.mixin import PeriodTaskSerializerMixin
|
||||||
from ops.models import Job, JobExecution
|
from ops.models import Job, JobExecution
|
||||||
|
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||||
|
|
||||||
_all_ = []
|
_all_ = []
|
||||||
|
|
||||||
|
|
||||||
class JobSerializer(serializers.ModelSerializer, PeriodTaskSerializerMixin):
|
class JobSerializer(BulkOrgResourceModelSerializer, PeriodTaskSerializerMixin):
|
||||||
owner = ReadableHiddenField(default=serializers.CurrentUserDefault())
|
owner = ReadableHiddenField(default=serializers.CurrentUserDefault())
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
|
@ -10,6 +10,7 @@ from django.utils import timezone
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from common.utils import get_logger, get_object_or_none, get_log_keep_day
|
from common.utils import get_logger, get_object_or_none, get_log_keep_day
|
||||||
|
from orgs.utils import tmp_to_org
|
||||||
from .celery.decorator import (
|
from .celery.decorator import (
|
||||||
register_as_period_task, after_app_shutdown_clean_periodic,
|
register_as_period_task, after_app_shutdown_clean_periodic,
|
||||||
after_app_ready_start
|
after_app_ready_start
|
||||||
|
@ -27,28 +28,30 @@ logger = get_logger(__file__)
|
||||||
@shared_task(soft_time_limit=60, queue="ansible", verbose_name=_("Run ansible task"))
|
@shared_task(soft_time_limit=60, queue="ansible", verbose_name=_("Run ansible task"))
|
||||||
def run_ops_job(job_id):
|
def run_ops_job(job_id):
|
||||||
job = get_object_or_none(Job, id=job_id)
|
job = get_object_or_none(Job, id=job_id)
|
||||||
execution = job.create_execution()
|
with tmp_to_org(job.org):
|
||||||
try:
|
execution = job.create_execution()
|
||||||
execution.start()
|
try:
|
||||||
except SoftTimeLimitExceeded:
|
execution.start()
|
||||||
execution.set_error('Run timeout')
|
except SoftTimeLimitExceeded:
|
||||||
logger.error("Run adhoc timeout")
|
execution.set_error('Run timeout')
|
||||||
except Exception as e:
|
logger.error("Run adhoc timeout")
|
||||||
execution.set_error(e)
|
except Exception as e:
|
||||||
logger.error("Start adhoc execution error: {}".format(e))
|
execution.set_error(e)
|
||||||
|
logger.error("Start adhoc execution error: {}".format(e))
|
||||||
|
|
||||||
|
|
||||||
@shared_task(soft_time_limit=60, queue="ansible", verbose_name=_("Run ansible task execution"))
|
@shared_task(soft_time_limit=60, queue="ansible", verbose_name=_("Run ansible task execution"))
|
||||||
def run_ops_job_executions(execution_id, **kwargs):
|
def run_ops_job_executions(execution_id, **kwargs):
|
||||||
execution = get_object_or_none(JobExecution, id=execution_id)
|
execution = get_object_or_none(JobExecution, id=execution_id)
|
||||||
try:
|
with tmp_to_org(execution.org):
|
||||||
execution.start()
|
try:
|
||||||
except SoftTimeLimitExceeded:
|
execution.start()
|
||||||
execution.set_error('Run timeout')
|
except SoftTimeLimitExceeded:
|
||||||
logger.error("Run adhoc timeout")
|
execution.set_error('Run timeout')
|
||||||
except Exception as e:
|
logger.error("Run adhoc timeout")
|
||||||
execution.set_error(e)
|
except Exception as e:
|
||||||
logger.error("Start adhoc execution error: {}".format(e))
|
execution.set_error(e)
|
||||||
|
logger.error("Start adhoc execution error: {}".format(e))
|
||||||
|
|
||||||
|
|
||||||
@shared_task(verbose_name=_('Periodic clear celery tasks'))
|
@shared_task(verbose_name=_('Periodic clear celery tasks'))
|
||||||
|
|
|
@ -125,7 +125,7 @@ class AssetPermission(OrgModelMixin):
|
||||||
"""
|
"""
|
||||||
asset_ids = self.get_all_assets(flat=True)
|
asset_ids = self.get_all_assets(flat=True)
|
||||||
q = Q(asset_id__in=asset_ids)
|
q = Q(asset_id__in=asset_ids)
|
||||||
if Account.AliasAccount.ALL in self.accounts:
|
if Account.AliasAccount.ALL not in self.accounts:
|
||||||
q &= Q(username__in=self.accounts)
|
q &= Q(username__in=self.accounts)
|
||||||
accounts = Account.objects.filter(q).order_by('asset__name', 'name', 'username')
|
accounts = Account.objects.filter(q).order_by('asset__name', 'name', 'username')
|
||||||
if not flat:
|
if not flat:
|
||||||
|
|
|
@ -12,7 +12,7 @@ from perms.serializers.permission import ActionChoicesField
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'NodeGrantedSerializer', 'AssetGrantedSerializer',
|
'NodeGrantedSerializer', 'AssetGrantedSerializer',
|
||||||
'ActionsSerializer', 'AccountsPermedSerializer'
|
'AccountsPermedSerializer'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -43,14 +43,10 @@ class NodeGrantedSerializer(serializers.ModelSerializer):
|
||||||
read_only_fields = fields
|
read_only_fields = fields
|
||||||
|
|
||||||
|
|
||||||
class ActionsSerializer(serializers.Serializer):
|
|
||||||
actions = ActionChoicesField(read_only=True)
|
|
||||||
|
|
||||||
|
|
||||||
class AccountsPermedSerializer(serializers.ModelSerializer):
|
class AccountsPermedSerializer(serializers.ModelSerializer):
|
||||||
actions = ActionChoicesField(read_only=True)
|
actions = ActionChoicesField(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Account
|
model = Account
|
||||||
fields = ['id', 'name', 'username', 'secret_type', 'has_secret', 'actions']
|
fields = ['id', 'name', 'has_username', 'username', 'has_secret', 'secret_type', 'actions']
|
||||||
read_only_fields = fields
|
read_only_fields = fields
|
||||||
|
|
|
@ -5,5 +5,4 @@ from .user_permission import user_permission_urlpatterns
|
||||||
|
|
||||||
app_name = 'perms'
|
app_name = 'perms'
|
||||||
|
|
||||||
urlpatterns = asset_permission_urlpatterns \
|
urlpatterns = asset_permission_urlpatterns + user_permission_urlpatterns
|
||||||
+ user_permission_urlpatterns
|
|
||||||
|
|
Loading…
Reference in New Issue