Merge pull request #9250 from jumpserver/v3

v3 to dev
pull/9267/head
老广 2022-12-28 13:26:25 +08:00 committed by GitHub
commit ce0632f49b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
284 changed files with 20975 additions and 3445 deletions

1
.gitignore vendored
View File

@ -16,6 +16,7 @@ dump.rdb
.cache/
.idea/
.vscode/
.fleet/
db.sqlite3
config.py
config.yml

View File

@ -1,4 +1,4 @@
FROM python:3.8-slim as stage-build
FROM python:3.9-slim as stage-build
ARG TARGETARCH
ARG VERSION
@ -8,7 +8,7 @@ WORKDIR /opt/jumpserver
ADD . .
RUN cd utils && bash -ixeu build.sh
FROM python:3.8-slim
FROM python:3.9-slim
ARG TARGETARCH
MAINTAINER JumpServer Team <ibuler@qq.com>
@ -18,7 +18,6 @@ ARG BUILD_DEPENDENCIES=" \
pkg-config"
ARG DEPENDENCIES=" \
default-libmysqlclient-dev \
freetds-dev \
libpq-dev \
libffi-dev \
@ -28,21 +27,20 @@ ARG DEPENDENCIES=" \
libxml2-dev \
libxmlsec1-dev \
libxmlsec1-openssl \
libaio-dev \
openssh-client \
sshpass"
libaio-dev"
ARG TOOLS=" \
ca-certificates \
curl \
default-libmysqlclient-dev \
default-mysql-client \
iputils-ping \
locales \
openssh-client \
procps \
redis-tools \
sshpass \
telnet \
vim \
unzip \
vim \
wget"
ARG APT_MIRROR=http://mirrors.ustc.edu.cn
@ -82,6 +80,8 @@ ENV PIP_MIRROR=$PIP_MIRROR
ARG PIP_JMS_MIRROR=https://pypi.douban.com/simple
ENV PIP_JMS_MIRROR=$PIP_JMS_MIRROR
ARG DEBUG
RUN --mount=type=cache,target=/root/.cache/pip \
set -ex \
&& pip config set global.index-url ${PIP_MIRROR} \

10
Dockerfile-ee Normal file
View File

@ -0,0 +1,10 @@
ARG VERSION
FROM registry.fit2cloud.com/jumpserver/xpack:${VERSION} as build-xpack
FROM jumpserver/core:${VERSION}
COPY --from=build-xpack /opt/xpack /opt/jumpserver/apps/xpack
WORKDIR /opt/jumpserver
RUN --mount=type=cache,target=/root/.cache/pip \
set -ex \
&& pip install -r requirements/requirements_xpack.txt

View File

@ -1,4 +1,4 @@
FROM python:3.8-slim as stage-build
FROM python:3.9-slim as stage-build
ARG TARGETARCH
ARG VERSION
@ -8,7 +8,7 @@ WORKDIR /opt/jumpserver
ADD . .
RUN cd utils && bash -ixeu build.sh
FROM python:3.8-slim
FROM python:3.9-slim
ARG TARGETARCH
MAINTAINER JumpServer Team <ibuler@qq.com>
@ -18,7 +18,6 @@ ARG BUILD_DEPENDENCIES=" \
pkg-config"
ARG DEPENDENCIES=" \
default-libmysqlclient-dev \
freetds-dev \
libpq-dev \
libffi-dev \
@ -28,21 +27,20 @@ ARG DEPENDENCIES=" \
libxml2-dev \
libxmlsec1-dev \
libxmlsec1-openssl \
libaio-dev \
openssh-client \
sshpass"
libaio-dev"
ARG TOOLS=" \
ca-certificates \
curl \
default-libmysqlclient-dev \
default-mysql-client \
iputils-ping \
locales \
netcat \
redis-server \
openssh-client \
procps \
sshpass \
telnet \
vim \
unzip \
vim \
wget"
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core \
@ -69,13 +67,15 @@ ENV PIP_MIRROR=$PIP_MIRROR
ARG PIP_JMS_MIRROR=https://pypi.douban.com/simple
ENV PIP_JMS_MIRROR=$PIP_JMS_MIRROR
ARG DEBUG
RUN --mount=type=cache,target=/root/.cache/pip \
set -ex \
&& pip config set global.index-url ${PIP_MIRROR} \
&& pip install --upgrade pip \
&& pip install --upgrade setuptools wheel \
&& pip install https://download.jumpserver.org/pypi/simple/cryptography/cryptography-36.0.1-cp38-cp38-linux_loongarch64.whl \
&& pip install https://download.jumpserver.org/pypi/simple/greenlet/greenlet-1.1.2-cp38-cp38-linux_loongarch64.whl \
&& pip install https://download.jumpserver.org/pypi/simple/cryptography/cryptography-38.0.4-cp39-cp39-linux_loongarch64.whl \
&& pip install https://download.jumpserver.org/pypi/simple/greenlet/greenlet-1.1.2-cp39-cp39-linux_loongarch64.whl \
&& pip install $(grep 'PyNaCl' requirements/requirements.txt) \
&& GRPC_PYTHON_BUILD_SYSTEM_OPENSSL=true pip install grpcio \
&& pip install $(grep -E 'jms|jumpserver' requirements/requirements.txt) -i ${PIP_JMS_MIRROR} \

View File

@ -28,6 +28,7 @@ class Migration(migrations.Migration):
('type', models.CharField(choices=[('command', 'Command'), ('regex', 'Regex')], default='command',
max_length=16, verbose_name='Type')),
('content', models.TextField(help_text='One line one command', verbose_name='Content')),
('comment', models.TextField(blank=True, default='', verbose_name='Comment')),
('ignore_case', models.BooleanField(default=True, verbose_name='Ignore case')),
],
options={
@ -57,7 +58,8 @@ class Migration(migrations.Migration):
('assets', models.JSONField(verbose_name='Asset')),
('commands', models.ManyToManyField(to='acls.CommandGroup', verbose_name='Commands')),
(
'reviewers', models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL, verbose_name='Reviewers')),
'reviewers',
models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL, verbose_name='Reviewers')),
],
options={
'verbose_name': 'Command acl',

View File

@ -1,18 +1,33 @@
# Generated by Django 3.2.14 on 2022-12-02 04:25
from django.db import migrations, models
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('acls', '0007_auto_20221202_1048'),
]
operations = [
migrations.AddField(
model_name='commandgroup',
name='comment',
field=models.TextField(blank=True, verbose_name='Comment'),
migrations.AlterModelOptions(
name='commandgroup',
options={'verbose_name': 'Command group'},
),
migrations.RenameField(
model_name='commandfilteracl',
old_name='commands',
new_name='command_groups',
),
migrations.AlterModelOptions(
name='commandfilteracl',
options={'ordering': ('priority', 'name'), 'verbose_name': 'Command acl'},
),
migrations.AlterModelOptions(
name='loginacl',
options={'ordering': ('priority', 'name'), 'verbose_name': 'Login acl'},
),
migrations.AlterModelOptions(
name='loginassetacl',
options={'ordering': ('priority', 'name'), 'verbose_name': 'Login asset acl'},
),
]

View File

@ -1,22 +0,0 @@
# Generated by Django 3.2.14 on 2022-12-03 16:01
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('acls', '0008_commandgroup_comment'),
]
operations = [
migrations.AlterModelOptions(
name='commandgroup',
options={'verbose_name': 'Command group'},
),
migrations.RenameField(
model_name='commandfilteracl',
old_name='commands',
new_name='command_groups',
),
]

View File

@ -0,0 +1,53 @@
# Generated by Django 3.2.14 on 2022-12-20 11:56
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('acls', '0008_commandgroup_comment'),
]
operations = [
migrations.AddField(
model_name='commandfilteracl',
name='updated_by',
field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'),
),
migrations.AddField(
model_name='loginacl',
name='updated_by',
field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'),
),
migrations.AddField(
model_name='loginassetacl',
name='updated_by',
field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'),
),
migrations.AlterField(
model_name='commandfilteracl',
name='created_by',
field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'),
),
migrations.AlterField(
model_name='commandgroup',
name='created_by',
field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'),
),
migrations.AlterField(
model_name='commandgroup',
name='updated_by',
field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'),
),
migrations.AlterField(
model_name='loginacl',
name='created_by',
field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'),
),
migrations.AlterField(
model_name='loginassetacl',
name='created_by',
field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'),
),
]

View File

@ -1,25 +0,0 @@
# Generated by Django 3.2.14 on 2022-12-05 03:22
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('acls', '0009_auto_20221204_0001'),
]
operations = [
migrations.AlterModelOptions(
name='commandfilteracl',
options={'ordering': ('priority', 'name'), 'verbose_name': 'Command acl'},
),
migrations.AlterModelOptions(
name='loginacl',
options={'ordering': ('priority', 'name'), 'verbose_name': 'Login acl'},
),
migrations.AlterModelOptions(
name='loginassetacl',
options={'ordering': ('priority', 'name'), 'verbose_name': 'Login asset acl'},
),
]

View File

@ -3,7 +3,7 @@ from django.db import models
from django.db.models import Q
from django.utils.translation import ugettext_lazy as _
from common.mixins import CommonModelMixin
from common.db.models import JMSBaseModel
from common.utils import contains_ip
from orgs.mixins.models import OrgModelMixin
@ -58,7 +58,7 @@ class UserAssetAccountACLQuerySet(BaseACLQuerySet):
def filter_account(self, username):
q = Q(accounts__username_group__contains=username) | \
Q(accounts__username_group__contains='*')
Q(accounts__username_group__contains='*')
return self.filter(q)
@ -67,7 +67,7 @@ class ACLManager(models.Manager):
return self.get_queryset().valid()
class BaseACL(CommonModelMixin):
class BaseACL(JMSBaseModel):
name = models.CharField(max_length=128, verbose_name=_('Name'))
priority = models.IntegerField(
default=50, verbose_name=_("Priority"),
@ -77,7 +77,6 @@ class BaseACL(CommonModelMixin):
action = models.CharField(max_length=64, default=ActionChoices.reject, verbose_name=_('Action'))
reviewers = models.ManyToManyField('users.User', blank=True, verbose_name=_("Reviewers"))
is_active = models.BooleanField(default=True, verbose_name=_("Active"))
comment = models.TextField(default='', blank=True, verbose_name=_('Comment'))
ActionChoices = ActionChoices
objects = ACLManager.from_queryset(BaseACLQuerySet)()

View File

@ -7,7 +7,6 @@ from django.utils.translation import ugettext_lazy as _
from common.utils import lazyproperty, get_logger
from orgs.mixins.models import JMSOrgBaseModel
from .base import UserAssetAccountBaseACL
logger = get_logger(__file__)
@ -26,7 +25,6 @@ class CommandGroup(JMSOrgBaseModel):
)
content = models.TextField(verbose_name=_("Content"), help_text=_("One line one command"))
ignore_case = models.BooleanField(default=True, verbose_name=_('Ignore case'))
comment = models.TextField(blank=True, verbose_name=_("Comment"))
TypeChoices = TypeChoices

View File

@ -1,17 +1,17 @@
# Generated by Django 3.1.12 on 2021-08-26 09:07
import assets.models.base
import common.db.fields
from django.conf import settings
import uuid
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
import simple_history.models
import uuid
from django.conf import settings
from django.db import migrations, models
import common.db.fields
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('assets', '0076_delete_assetuser'),
@ -22,14 +22,19 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='HistoricalAccount',
fields=[
('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
('org_id',
models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
('id', models.UUIDField(db_index=True, default=uuid.uuid4)),
('name', models.CharField(max_length=128, verbose_name='Name')),
('username', models.CharField(blank=True, db_index=True, max_length=128, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username')),
('password', common.db.fields.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password')),
('private_key', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH private key')),
('username', models.CharField(blank=True, db_index=True, max_length=128, validators=[
django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')],
verbose_name='Username')),
('password',
common.db.fields.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password')),
('private_key',
common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH private key')),
('public_key', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH public key')),
('comment', models.TextField(blank=True, verbose_name='Comment')),
('comment', models.TextField(blank=True, default='', verbose_name='Comment')),
('date_created', models.DateTimeField(blank=True, editable=False, verbose_name='Date created')),
('date_updated', models.DateTimeField(blank=True, editable=False, verbose_name='Date updated')),
('created_by', models.CharField(max_length=128, null=True, verbose_name='Created by')),
@ -37,10 +42,17 @@ class Migration(migrations.Migration):
('history_id', models.AutoField(primary_key=True, serialize=False)),
('history_date', models.DateTimeField()),
('history_change_reason', models.CharField(max_length=100, null=True)),
('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
('app', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='applications.application', verbose_name='Database')),
('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
('systemuser', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='assets.systemuser', verbose_name='System user')),
('history_type',
models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
('app', models.ForeignKey(blank=True, db_constraint=False, null=True,
on_delete=django.db.models.deletion.DO_NOTHING, related_name='+',
to='applications.application', verbose_name='Database')),
('history_user',
models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+',
to=settings.AUTH_USER_MODEL)),
('systemuser', models.ForeignKey(blank=True, db_constraint=False, null=True,
on_delete=django.db.models.deletion.DO_NOTHING, related_name='+',
to='assets.systemuser', verbose_name='System user')),
],
options={
'verbose_name': 'historical Account',
@ -52,20 +64,28 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='Account',
fields=[
('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
('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)),
('name', models.CharField(max_length=128, verbose_name='Name')),
('username', models.CharField(blank=True, db_index=True, max_length=128, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username')),
('password', common.db.fields.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password')),
('private_key', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH private key')),
('username', models.CharField(blank=True, db_index=True, max_length=128, validators=[
django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')],
verbose_name='Username')),
('password',
common.db.fields.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password')),
('private_key',
common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH private key')),
('public_key', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH public key')),
('comment', models.TextField(blank=True, verbose_name='Comment')),
('comment', models.TextField(blank=True, default='', verbose_name='Comment')),
('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date created')),
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
('created_by', models.CharField(max_length=128, null=True, verbose_name='Created by')),
('version', models.IntegerField(default=1, verbose_name='Version')),
('app', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='applications.application', verbose_name='Database')),
('systemuser', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='assets.systemuser', verbose_name='System user')),
('app', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE,
to='applications.application', verbose_name='Database')),
('systemuser',
models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='assets.systemuser',
verbose_name='System user')),
],
options={
'verbose_name': 'Account',

View File

@ -0,0 +1,28 @@
# Generated by Django 3.2.14 on 2022-12-20 11:56
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('applications', '0026_auto_20220817_1716'),
]
operations = [
migrations.AddField(
model_name='application',
name='updated_by',
field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'),
),
migrations.AlterField(
model_name='application',
name='comment',
field=models.TextField(blank=True, default='', verbose_name='Comment'),
),
migrations.AlterField(
model_name='application',
name='created_by',
field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'),
),
]

View File

@ -1,12 +1,11 @@
from django.db import models
from django.utils.translation import ugettext_lazy as _
from common.db.models import JMSBaseModel
from orgs.mixins.models import OrgModelMixin
from common.mixins import CommonModelMixin
class Application(CommonModelMixin, OrgModelMixin):
class Application(JMSBaseModel, OrgModelMixin):
name = models.CharField(max_length=128, verbose_name=_('Name'))
category = models.CharField(
max_length=16, verbose_name=_('Category')
@ -15,9 +14,6 @@ class Application(CommonModelMixin, OrgModelMixin):
max_length=16, verbose_name=_('Type')
)
attrs = models.JSONField(default=dict, verbose_name=_('Attrs'))
comment = models.TextField(
max_length=128, default='', blank=True, verbose_name=_('Comment')
)
class Meta:
verbose_name = _('Application')

View File

@ -1,11 +1,11 @@
from .mixin import *
from .category import *
from .platform import *
from .asset import *
from .label import *
from .account import *
from .node import *
from .domain import *
from .asset import *
from .automations import *
from .gathered_user import *
from .category import *
from .domain import *
from .favorite_asset import *
from .label import *
from .mixin import *
from .node import *
from .platform import *
from .tree import *

View File

@ -1,19 +1,22 @@
from django.shortcuts import get_object_or_404
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.generics import CreateAPIView, ListAPIView
from rest_framework.response import Response
from orgs.mixins.api import OrgBulkModelViewSet
from rbac.permissions import RBACPermission
from assets import serializers
from assets.filters import AccountFilterSet
from assets.models import Account, Asset
from assets.tasks import verify_accounts_connectivity
from authentication.const import ConfirmType
from common.mixins import RecordViewLogMixin
from common.permissions import UserConfirmation
from authentication.const import ConfirmType
from assets.models import Account
from assets.filters import AccountFilterSet
from assets.tasks import verify_accounts_connectivity
from assets import serializers
from orgs.mixins.api import OrgBulkModelViewSet
__all__ = ['AccountViewSet', 'AccountSecretsViewSet', 'AccountTaskCreateAPI', 'AccountHistoriesSecretAPI']
__all__ = [
'AccountViewSet', 'AccountSecretsViewSet', 'AccountTaskCreateAPI', 'AccountHistoriesSecretAPI'
]
from rbac.permissions import RBACPermission
class AccountViewSet(OrgBulkModelViewSet):
@ -30,10 +33,18 @@ class AccountViewSet(OrgBulkModelViewSet):
'su_from_accounts': 'assets.view_account',
}
@action(methods=['get'], detail=True, url_path='su-from-accounts')
@action(methods=['get'], detail=False, url_path='su-from-accounts')
def su_from_accounts(self, request, *args, **kwargs):
account = super().get_object()
accounts = account.get_su_from_accounts()
account_id = request.query_params.get('account')
asset_id = request.query_params.get('asset')
if account_id:
account = get_object_or_404(Account, pk=account_id)
accounts = account.get_su_from_accounts()
elif asset_id:
asset = get_object_or_404(Asset, pk=asset_id)
accounts = asset.accounts.all()
else:
accounts = []
serializer = serializers.AccountSerializer(accounts, many=True)
return Response(data=serializer.data)
@ -54,8 +65,7 @@ class AccountSecretsViewSet(RecordViewLogMixin, AccountViewSet):
'default': serializers.AccountSecretSerializer,
}
http_method_names = ['get', 'options']
# Todo: 记得打开
# permission_classes = [RBACPermission, UserConfirmation.require(ConfirmType.MFA)]
permission_classes = [RBACPermission, UserConfirmation.require(ConfirmType.MFA)]
rbac_perms = {
'list': 'assets.view_accountsecret',
'retrieve': 'assets.view_accountsecret',
@ -66,8 +76,7 @@ class AccountHistoriesSecretAPI(RecordViewLogMixin, ListAPIView):
model = Account.history.model
serializer_class = serializers.AccountHistorySerializer
http_method_names = ['get', 'options']
# Todo: 记得打开
# permission_classes = [RBACPermission, UserConfirmation.require(ConfirmType.MFA)]
permission_classes = [RBACPermission, UserConfirmation.require(ConfirmType.MFA)]
rbac_perms = {
'list': 'assets.view_accountsecret',
}
@ -102,4 +111,5 @@ class AccountTaskCreateAPI(CreateAPIView):
def get_exception_handler(self):
def handler(e, context):
return Response({"error": str(e)}, status=400)
return handler

View File

@ -1,9 +1,6 @@
from assets import serializers
from assets.models import AccountTemplate
from rbac.permissions import RBACPermission
from authentication.const import ConfirmType
from common.mixins import RecordViewLogMixin
from common.permissions import UserConfirmation
from orgs.mixins.api import OrgBulkModelViewSet

View File

@ -30,18 +30,23 @@ __all__ = [
class AssetFilterSet(BaseFilterSet):
type = django_filters.CharFilter(field_name="platform__type", lookup_expr="exact")
category = django_filters.CharFilter(
field_name="platform__category", lookup_expr="exact"
)
hostname = django_filters.CharFilter(field_name="name", lookup_expr="exact")
category = django_filters.CharFilter(field_name="platform__category", lookup_expr="exact")
platform = django_filters.CharFilter(method='filter_platform')
class Meta:
model = Asset
fields = [
"id", "name", "address", "is_active",
"type", "category", "hostname"
"type", "category", "platform"
]
@staticmethod
def filter_platform(queryset, name, value):
if value.isdigit():
return queryset.filter(platform_id=value)
else:
return queryset.filter(platform__name=value)
class AssetViewSet(SuggestionMixin, NodeFilterMixin, OrgBulkModelViewSet):
"""
@ -55,8 +60,9 @@ class AssetViewSet(SuggestionMixin, NodeFilterMixin, OrgBulkModelViewSet):
ordering = ("name",)
serializer_classes = (
("default", serializers.AssetSerializer),
("suggestion", serializers.MiniAssetSerializer),
("retrieve", serializers.AssetDetailSerializer),
("platform", serializers.PlatformSerializer),
("suggestion", serializers.MiniAssetSerializer),
("gateways", serializers.GatewaySerializer),
)
rbac_perms = (

View File

@ -1,4 +1,4 @@
from assets.models import Cloud
from assets.models import Cloud, Asset
from assets.serializers import CloudSerializer
from .asset import AssetViewSet
@ -8,6 +8,7 @@ __all__ = ['CloudViewSet']
class CloudViewSet(AssetViewSet):
model = Cloud
perm_model = Asset
def get_serializer_classes(self):
serializer_classes = super().get_serializer_classes()

View File

@ -1,4 +1,4 @@
from assets.models import Database
from assets.models import Database, Asset
from assets.serializers import DatabaseSerializer
from .asset import AssetViewSet
@ -8,6 +8,7 @@ __all__ = ['DatabaseViewSet']
class DatabaseViewSet(AssetViewSet):
model = Database
perm_model = Asset
def get_serializer_classes(self):
serializer_classes = super().get_serializer_classes()

View File

@ -1,6 +1,5 @@
from assets.serializers import DeviceSerializer
from assets.models import Device
from assets.models import Device, Asset
from .asset import AssetViewSet
__all__ = ['DeviceViewSet']
@ -8,6 +7,7 @@ __all__ = ['DeviceViewSet']
class DeviceViewSet(AssetViewSet):
model = Device
perm_model = Asset
def get_serializer_classes(self):
serializer_classes = super().get_serializer_classes()

View File

@ -1,4 +1,4 @@
from assets.models import Host
from assets.models import Host, Asset
from assets.serializers import HostSerializer
from .asset import AssetViewSet
@ -7,6 +7,7 @@ __all__ = ['HostViewSet']
class HostViewSet(AssetViewSet):
model = Host
perm_model = Asset
def get_serializer_classes(self):
serializer_classes = super().get_serializer_classes()

View File

@ -1,4 +1,4 @@
from assets.models import Web
from assets.models import Web, Asset
from assets.serializers import WebSerializer
from .asset import AssetViewSet
@ -8,6 +8,7 @@ __all__ = ['WebViewSet']
class WebViewSet(AssetViewSet):
model = Web
perm_model = Asset
def get_serializer_classes(self):
serializer_classes = super().get_serializer_classes()

View File

@ -1,17 +1,18 @@
from django.shortcuts import get_object_or_404
from django.utils.translation import ugettext_lazy as _
from rest_framework.response import Response
from rest_framework import status, mixins, viewsets
from rest_framework.response import Response
from orgs.mixins import generics
from assets import serializers
from assets.tasks import execute_automation
from assets.models import BaseAutomation, AutomationExecution
from assets.tasks import execute_automation
from common.const.choices import Trigger
from orgs.mixins import generics
__all__ = [
'AutomationAssetsListApi', 'AutomationRemoveAssetApi',
'AutomationAddAssetApi', 'AutomationNodeAddRemoveApi', 'AutomationExecutionViewSet'
'AutomationAddAssetApi', 'AutomationNodeAddRemoveApi',
'AutomationExecutionViewSet',
]

View File

@ -3,14 +3,15 @@
from rest_framework import mixins
from assets import serializers
from assets.models import ChangeSecretAutomation, ChangeSecretRecord, AutomationExecution
from common.utils import get_object_or_none
from orgs.mixins.api import OrgBulkModelViewSet, OrgGenericViewSet
from assets.models import ChangeSecretAutomation, ChangeSecretRecord, AutomationExecution
from assets import serializers
from .base import AutomationExecutionViewSet
__all__ = [
'ChangeSecretAutomationViewSet', 'ChangeSecretRecordViewSet'
'ChangeSecretAutomationViewSet', 'ChangeSecretRecordViewSet',
'ChangSecretExecutionViewSet'
]
@ -38,3 +39,11 @@ class ChangeSecretRecordViewSet(mixins.ListModelMixin, OrgGenericViewSet):
queryset = queryset.filter(execution=execution)
queryset = queryset.order_by('-date_started')
return queryset
class ChangSecretExecutionViewSet(AutomationExecutionViewSet):
rbac_perms = (
("list", "assets.view_changesecretexecution"),
("retrieve", "assets.view_changesecretexecution"),
("create", "assets.add_changesecretexecution"),
)

View File

@ -1,12 +1,12 @@
# -*- coding: utf-8 -*-
#
from orgs.mixins.api import OrgBulkModelViewSet
from assets.models import GatherAccountsAutomation
from assets import serializers
from assets.models import GatherAccountsAutomation
from orgs.mixins.api import OrgBulkModelViewSet
from .base import AutomationExecutionViewSet
__all__ = [
'GatherAccountsAutomationViewSet',
'GatherAccountsAutomationViewSet', 'GatherAccountsExecutionViewSet'
]
@ -16,3 +16,11 @@ class GatherAccountsAutomationViewSet(OrgBulkModelViewSet):
search_fields = filter_fields
ordering_fields = ('name',)
serializer_class = serializers.GatherAccountAutomationSerializer
class GatherAccountsExecutionViewSet(AutomationExecutionViewSet):
rbac_perms = (
("list", "assets.view_gatheraccountsexecution"),
("retrieve", "assets.view_gatheraccountsexecution"),
("create", "assets.add_gatheraccountsexecution"),
)

View File

@ -1,20 +0,0 @@
# -*- coding: utf-8 -*-
#
from orgs.mixins.api import OrgModelViewSet
from assets.models import GatheredUser
from ..serializers import GatheredUserSerializer
from ..filters import AssetRelatedByNodeFilterBackend
__all__ = ['GatheredUserViewSet']
class GatheredUserViewSet(OrgModelViewSet):
model = GatheredUser
serializer_class = GatheredUserSerializer
extra_filter_backends = [AssetRelatedByNodeFilterBackend]
filterset_fields = ['asset', 'username', 'present', 'asset__address', 'asset__name', 'asset_id']
search_fields = ['username', 'asset__address', 'asset__name']

View File

@ -61,6 +61,7 @@ class SerializeToTreeNodeMixin:
'meta': {
'type': 'asset',
'data': {
'platform_type': asset.platform.type,
'org_name': asset.org_name,
'sftp': asset.platform_id in sftp_enabled_platform,
},

View File

@ -1,43 +1,37 @@
# ~*~ coding: utf-8 ~*~
from functools import partial
from collections import namedtuple, defaultdict
from django.core.exceptions import PermissionDenied
from functools import partial
from rest_framework import status
from rest_framework.generics import get_object_or_404
from rest_framework.serializers import ValidationError
from rest_framework.response import Response
from rest_framework.decorators import action
from django.utils.translation import ugettext_lazy as _
from django.db.models.signals import m2m_changed
from django.utils.translation import ugettext_lazy as _
from rest_framework import status
from rest_framework.decorators import action
from rest_framework.generics import get_object_or_404
from rest_framework.response import Response
from rest_framework.serializers import ValidationError
from common.const.http import POST
from common.exceptions import SomeoneIsDoingThis
from common.const.signals import PRE_REMOVE, POST_REMOVE
from common.mixins.api import SuggestionMixin
from assets.models import Asset
from common.const.http import POST
from common.const.signals import PRE_REMOVE, POST_REMOVE
from common.exceptions import SomeoneIsDoingThis
from common.mixins.api import SuggestionMixin
from common.utils import get_logger
from common.tree import TreeNodeSerializer
from orgs.mixins.api import OrgBulkModelViewSet
from orgs.mixins import generics
from orgs.mixins.api import OrgBulkModelViewSet
from orgs.utils import current_org
from .. import serializers
from ..models import Node
from ..tasks import (
update_node_assets_hardware_info_manual,
test_node_assets_connectivity_manual,
check_node_assets_amount_task
)
from .. import serializers
from ..const import AllTypes
from .mixin import SerializeToTreeNodeMixin
from assets.locks import NodeAddChildrenLock
logger = get_logger(__file__)
__all__ = [
'NodeViewSet', 'NodeChildrenApi', 'NodeAssetsApi',
'NodeAddAssetsApi', 'NodeRemoveAssetsApi', 'MoveAssetsToNodeApi',
'NodeAddChildrenApi', 'NodeListAsTreeApi', 'NodeChildrenAsTreeApi',
'NodeTaskCreateApi', 'CategoryTreeApi',
'NodeViewSet', 'NodeAssetsApi', 'NodeAddAssetsApi',
'NodeRemoveAssetsApi', 'MoveAssetsToNodeApi',
'NodeAddChildrenApi', 'NodeTaskCreateApi',
]
@ -74,153 +68,6 @@ class NodeViewSet(SuggestionMixin, OrgBulkModelViewSet):
return super().destroy(request, *args, **kwargs)
class NodeListAsTreeApi(generics.ListAPIView):
"""
获取节点列表树
[
{
"id": "",
"name": "",
"pId": "",
"meta": ""
}
]
"""
model = Node
serializer_class = TreeNodeSerializer
@staticmethod
def to_tree_queryset(queryset):
queryset = [node.as_tree_node() for node in queryset]
return queryset
def filter_queryset(self, queryset):
queryset = super().filter_queryset(queryset)
queryset = self.to_tree_queryset(queryset)
return queryset
class NodeChildrenApi(generics.ListCreateAPIView):
serializer_class = serializers.NodeSerializer
search_fields = ('value',)
instance = None
is_initial = False
def initial(self, request, *args, **kwargs):
self.instance = self.get_object()
return super().initial(request, *args, **kwargs)
def perform_create(self, serializer):
with NodeAddChildrenLock(self.instance):
data = serializer.validated_data
_id = data.get("id")
value = data.get("value")
if not value:
value = self.instance.get_next_child_preset_name()
node = self.instance.create_child(value=value, _id=_id)
# 避免查询 full value
node._full_value = node.value
serializer.instance = node
def get_object(self):
pk = self.kwargs.get('pk') or self.request.query_params.get('id')
key = self.request.query_params.get("key")
if not pk and not key:
self.is_initial = True
if current_org.is_root():
node = None
else:
node = Node.org_root()
return node
if pk:
node = get_object_or_404(Node, pk=pk)
else:
node = get_object_or_404(Node, key=key)
return node
def get_org_root_queryset(self, query_all):
if query_all:
return Node.objects.all()
else:
return Node.org_root_nodes()
def get_queryset(self):
query_all = self.request.query_params.get("all", "0") == "all"
if self.is_initial and current_org.is_root():
return self.get_org_root_queryset(query_all)
if self.is_initial:
with_self = True
else:
with_self = False
if not self.instance:
return Node.objects.none()
if query_all:
queryset = self.instance.get_all_children(with_self=with_self)
else:
queryset = self.instance.get_children(with_self=with_self)
return queryset
class NodeChildrenAsTreeApi(SerializeToTreeNodeMixin, NodeChildrenApi):
"""
节点子节点作为树返回
[
{
"id": "",
"name": "",
"pId": "",
"meta": ""
}
]
"""
model = Node
def filter_queryset(self, queryset):
if not self.request.GET.get('search'):
return queryset
queryset = super().filter_queryset(queryset)
queryset = self.model.get_ancestor_queryset(queryset)
return queryset
def list(self, request, *args, **kwargs):
nodes = self.filter_queryset(self.get_queryset()).order_by('value')
nodes = self.serialize_nodes(nodes, with_asset_amount=True)
assets = self.get_assets()
data = [*nodes, *assets]
return Response(data=data)
def get_assets(self):
include_assets = self.request.query_params.get('assets', '0') == '1'
if not self.instance or not include_assets:
return []
assets = self.instance.get_assets().only(
"id", "name", "address", "platform_id",
"org_id", "is_active",
).prefetch_related('platform')
return self.serialize_assets(assets, self.instance.key)
class CategoryTreeApi(SerializeToTreeNodeMixin, generics.ListAPIView):
serializer_class = TreeNodeSerializer
def check_permissions(self, request):
if not request.user.has_perm('assets.view_asset'):
raise PermissionDenied
return True
def list(self, request, *args, **kwargs):
nodes = AllTypes.to_tree_nodes()
serializer = self.get_serializer(nodes, many=True)
return Response(data=serializer.data)
class NodeAssetsApi(generics.ListAPIView):
serializer_class = serializers.AssetSerializer

View File

@ -1,10 +1,10 @@
from jumpserver.utils import has_valid_xpack_license
from common.drf.api import JMSModelViewSet
from common.drf.serializers import GroupedChoiceSerializer
from assets.models import Platform
from assets.const import AllTypes
from assets.serializers import PlatformSerializer
__all__ = ['AssetPlatformViewSet']
@ -22,6 +22,11 @@ class AssetPlatformViewSet(JMSModelViewSet):
'ops_methods': 'assets.view_platform'
}
def get_queryset(self):
queryset = super().get_queryset()
queryset = queryset.filter(type__in=AllTypes.get_types())
return queryset
def get_object(self):
pk = self.kwargs.get('pk', '')
if pk.isnumeric():

153
apps/assets/api/tree.py Normal file
View File

@ -0,0 +1,153 @@
# ~*~ coding: utf-8 ~*~
from rest_framework.generics import get_object_or_404
from rest_framework.response import Response
from assets.locks import NodeAddChildrenLock
from common.tree import TreeNodeSerializer
from common.utils import get_logger
from orgs.mixins import generics
from orgs.utils import current_org
from .mixin import SerializeToTreeNodeMixin
from .. import serializers
from ..const import AllTypes
from ..models import Node, Platform, Asset
logger = get_logger(__file__)
__all__ = [
'NodeChildrenApi',
'NodeChildrenAsTreeApi',
'CategoryTreeApi',
]
class NodeChildrenApi(generics.ListCreateAPIView):
"""
节点的增删改查
"""
serializer_class = serializers.NodeSerializer
search_fields = ('value',)
instance = None
is_initial = False
def initial(self, request, *args, **kwargs):
self.instance = self.get_object()
return super().initial(request, *args, **kwargs)
def perform_create(self, serializer):
with NodeAddChildrenLock(self.instance):
data = serializer.validated_data
_id = data.get("id")
value = data.get("value")
if not value:
value = self.instance.get_next_child_preset_name()
node = self.instance.create_child(value=value, _id=_id)
# 避免查询 full value
node._full_value = node.value
serializer.instance = node
def get_object(self):
pk = self.kwargs.get('pk') or self.request.query_params.get('id')
key = self.request.query_params.get("key")
if not pk and not key:
self.is_initial = True
if current_org.is_root():
node = None
else:
node = Node.org_root()
return node
if pk:
node = get_object_or_404(Node, pk=pk)
else:
node = get_object_or_404(Node, key=key)
return node
def get_org_root_queryset(self, query_all):
if query_all:
return Node.objects.all()
else:
return Node.org_root_nodes()
def get_queryset(self):
query_all = self.request.query_params.get("all", "0") == "all"
if self.is_initial and current_org.is_root():
return self.get_org_root_queryset(query_all)
if self.is_initial:
with_self = True
else:
with_self = False
if not self.instance:
return Node.objects.none()
if query_all:
queryset = self.instance.get_all_children(with_self=with_self)
else:
queryset = self.instance.get_children(with_self=with_self)
return queryset
class NodeChildrenAsTreeApi(SerializeToTreeNodeMixin, NodeChildrenApi):
"""
节点子节点作为树返回
[
{
"id": "",
"name": "",
"pId": "",
"meta": ""
}
]
"""
model = Node
def filter_queryset(self, queryset):
if not self.request.GET.get('search'):
return queryset
queryset = super().filter_queryset(queryset)
queryset = self.model.get_ancestor_queryset(queryset)
return queryset
def list(self, request, *args, **kwargs):
nodes = self.filter_queryset(self.get_queryset()).order_by('value')
nodes = self.serialize_nodes(nodes, with_asset_amount=True)
assets = self.get_assets_as_node()
data = [*nodes, *assets]
return Response(data=data)
def get_assets_as_node(self):
include_assets = self.request.query_params.get('assets', '0') == '1'
if not self.instance or not include_assets:
return []
assets = self.instance.get_assets_for_tree()
return self.serialize_assets(assets, self.instance.key)
class CategoryTreeApi(SerializeToTreeNodeMixin, generics.ListAPIView):
serializer_class = TreeNodeSerializer
rbac_perms = {
'GET': 'assets.view_asset',
'list': 'assets.view_asset',
}
def get_assets(self):
key = self.request.query_params.get('key')
platform = Platform.objects.filter(id=key).first()
if not platform:
return []
assets = Asset.objects.filter(platform=platform).prefetch_related('platform')
return self.serialize_assets(assets, key)
def list(self, request, *args, **kwargs):
include_asset = self.request.query_params.get('assets', '0') == '1'
if include_asset and self.request.query_params.get('key'):
nodes = self.get_assets()
else:
nodes = AllTypes.to_tree_nodes(include_asset)
return Response(data=nodes)

View File

@ -49,23 +49,6 @@ class BaseAccountHandler:
header_fields[field] = str(v.label)
return header_fields
@staticmethod
def load_auth(tp, value, system_user):
if value:
return value
if system_user:
return getattr(system_user, tp, '')
return ''
@classmethod
def replace_auth(cls, account, system_user_dict):
system_user = system_user_dict.get(account.systemuser_id)
account.username = cls.load_auth('username', account.username, system_user)
account.password = cls.load_auth('password', account.password, system_user)
account.private_key = cls.load_auth('private_key', account.private_key, system_user)
account.public_key = cls.load_auth('public_key', account.public_key, system_user)
return account
@classmethod
def create_row(cls, data, header_fields):
data = cls.unpack_data(data)
@ -94,30 +77,30 @@ class AssetAccountHandler(BaseAccountHandler):
return filename
@classmethod
def create_data_map(cls, categories: list):
def create_data_map(cls, types: list):
data_map = defaultdict(list)
# TODO 可以优化一下查询 在账号上做 category 的缓存 避免数据量大时连表操作
qs = Account.objects.filter(
asset__platform__type__in=categories
).annotate(category=F('asset__platform__type'))
print(qs, categories)
asset__platform__type__in=types
).annotate(type=F('asset__platform__type'))
if not qs.exists():
return data_map
category_dict = {}
type_dict = {}
for i in AllTypes.grouped_choices_to_objs():
for j in i['children']:
category_dict[j['value']] = j['display_name']
type_dict[j['value']] = j['display_name']
header_fields = cls.get_header_fields(AccountSecretSerializer(qs.first()))
account_category_map = defaultdict(list)
account_type_map = defaultdict(list)
for account in qs:
account_category_map[account.category].append(account)
account_type_map[account.type].append(account)
data_map = {}
for category, accounts in account_category_map.items():
sheet_name = category_dict.get(category, category)
for tp, accounts in account_type_map.items():
sheet_name = type_dict.get(tp, tp)
data = AccountSecretSerializer(accounts, many=True).data
data_map.update(cls.add_rows(data, header_fields, sheet_name))
@ -140,9 +123,9 @@ class AccountBackupHandler:
# Print task start date
time_start = time.time()
files = []
categories = self.execution.categories
types = self.execution.types
data_map = AssetAccountHandler.create_data_map(categories)
data_map = AssetAccountHandler.create_data_map(types)
if not data_map:
return files

View File

@ -1,19 +1,19 @@
import os
import yaml
import shutil
from hashlib import md5
from copy import deepcopy
from socket import gethostname
from collections import defaultdict
from copy import deepcopy
from hashlib import md5
from socket import gethostname
import yaml
from django.conf import settings
from django.utils import timezone
from django.utils.translation import gettext as _
from common.utils import get_logger
from common.utils import ssh_pubkey_gen, ssh_key_string_to_obj
from assets.const import SecretType
from assets.automations.methods import platform_automation_methods
from assets.const import SecretType
from common.utils import get_logger, lazyproperty
from common.utils import ssh_pubkey_gen, ssh_key_string_to_obj
from ops.ansible import JMSInventory, PlaybookRunner, DefaultCallback
logger = get_logger(__name__)
@ -93,7 +93,7 @@ class BasePlaybookManager:
def get_assets_group_by_platform(self):
return self.automation.all_assets_group_by_platform()
@property
@lazyproperty
def runtime_dir(self):
ansible_dir = settings.ANSIBLE_DIR
dir_name = '{}_{}'.format(self.automation.name.replace(' ', '_'), self.execution.id)
@ -105,11 +105,6 @@ class BasePlaybookManager:
os.makedirs(path, exist_ok=True, mode=0o755)
return path
def prepare_playbook_dir(self):
for d in [self.runtime_dir]:
if not os.path.exists(d):
os.makedirs(d, exist_ok=True, mode=0o755)
def host_callback(self, host, automation=None, **kwargs):
enabled_attr = '{}_enabled'.format(self.__class__.method_type())
method_attr = '{}_method'.format(self.__class__.method_type())

View File

@ -1,8 +1,9 @@
from django.utils.translation import ugettext_lazy as _
from common.utils import get_logger
from assets.const import AutomationTypes
from assets.const import AutomationTypes, Source
from orgs.utils import tmp_to_org
from .filter import GatherAccountsFilter
from ...models import GatheredUser
from ..base.manager import BasePlaybookManager
logger = get_logger(__name__)
@ -26,20 +27,33 @@ class GatherAccountsManager(BasePlaybookManager):
result = GatherAccountsFilter(host).run(self.method_id_meta_mapper, result)
return result
@staticmethod
def bulk_create_accounts(asset, result):
account_objs = []
account_model = asset.accounts.model
account_usernames = set(asset.accounts.values_list('username', flat=True))
with tmp_to_org(asset.org_id):
accounts_dict = {}
for username, data in result.items():
comment = ''
d = {'asset': asset, 'username': username, 'name': username, 'source': Source.COLLECTED}
if data.get('date'):
comment += f"{_('Date last login')}: {data['date']}\n "
if data.get('address'):
comment += f"{_('IP last login')}: {data['address'][:32]}"
d['comment'] = comment
accounts_dict[username] = d
for username, data in accounts_dict.items():
if username in account_usernames:
continue
account_objs.append(account_model(**data))
account_model.objects.bulk_create(account_objs)
def on_host_success(self, host, result):
info = result.get('debug', {}).get('res', {}).get('info', {})
asset = self.host_asset_mapper.get(host)
org_id = asset.org_id
if asset and info:
result = self.filter_success_result(host, info)
with tmp_to_org(org_id):
GatheredUser.objects.filter(asset=asset, present=True).update(present=False)
for username, data in result.items():
defaults = {'asset': asset, 'present': True, 'username': username}
if data.get('date'):
defaults['date_last_login'] = data['date']
if data.get('address'):
defaults['ip_last_login'] = data['address'][:32]
GatheredUser.objects.update_or_create(defaults=defaults, asset=asset, username=username)
self.bulk_create_accounts(asset, result)
else:
logger.error("Not found info".format(host))

View File

@ -1,16 +0,0 @@
- hosts: mongodb
gather_facts: no
vars:
ansible_python_interpreter: /usr/local/bin/python
tasks:
- name: Add user account.username
mongodb_user:
login_user: "{{ jms_account.username }}"
login_password: "{{ jms_account.secret }}"
login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}"
login_database: "{{ jms_asset.specific.db_name }}"
db: "{{ jms_asset.specific.db_name }}"
name: "{{ account.username }}"
password: "{{ account.secret }}"

View File

@ -1,6 +0,0 @@
id: push_account_mongodb
name: Push account from MongoDB
category: database
type:
- mongodb
method: push_account

View File

@ -1,15 +0,0 @@
- hosts: mysql
gather_facts: no
vars:
ansible_python_interpreter: /usr/local/bin/python
tasks:
- name: Add user account.username
community.mysql.mysql_user:
login_user: "{{ jms_account.username }}"
login_password: "{{ jms_account.secret }}"
login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}"
name: "{{ account.username }}"
password: "{{ account.secret }}"
host: "%"

View File

@ -1,6 +0,0 @@
id: push_account_mysql
name: Push account from MySQL
category: database
type:
- mysql
method: push_account

View File

@ -1,16 +0,0 @@
- hosts: oracle
gather_facts: no
vars:
ansible_python_interpreter: /usr/local/bin/python
tasks:
- name: Add user account.username
oracle_user:
login_user: "{{ jms_account.username }}"
login_password: "{{ jms_account.secret }}"
login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}"
login_database: "{{ jms_asset.specific.db_name }}"
mode: "{{ jms_account.mode }}"
name: "{{ account.username }}"
password: "{{ account.secret }}"

View File

@ -1,6 +0,0 @@
id: push_account_oracle
name: Push account from Oracle
category: database
type:
- oracle
method: push_account

View File

@ -1,16 +0,0 @@
- hosts: postgresql
gather_facts: no
vars:
ansible_python_interpreter: /usr/local/bin/python
tasks:
- name: Add user account.username
community.postgresql.postgresql_user:
login_user: "{{ jms_account.username }}"
login_password: "{{ jms_account.secret }}"
login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}"
db: "{{ jms_asset.specific.db_name }}"
name: "{{ account.username }}"
password: "{{ account.secret }}"

View File

@ -1,6 +0,0 @@
id: push_account_postgresql
name: Push account for PostgreSQL
category: database
type:
- postgresql
method: push_account

View File

@ -1,19 +0,0 @@
- hosts: demo
gather_facts: no
tasks:
- name: Add user account.username
ansible.builtin.user:
name: "{{ account.username }}"
- name: Set account.username password
ansible.builtin.user:
name: "{{ account.username }}"
password: "{{ account.secret | password_hash('sha512') }}"
update_password: always
when: secret_type == "password"
- name: Set account.username SSH key
ansible.builtin.authorized_key:
user: "{{ account.username }}"
key: "{{ account.secret }}"
when: secret_type == "ssh_key"

View File

@ -1,7 +0,0 @@
id: push_account_posix
name: Push posix account
category: host
type:
- linux
- unix
method: push_account

View File

@ -1,13 +0,0 @@
- hosts: windows
gather_facts: yes
tasks:
- name: Add user account.username
ansible.windows.win_user:
vars:
fullname: "{{ account.username }}"
name: "{{ account.username }}"
password: "{{ account.secret }}"
state: present
password_expired: no
update_password: always
password_never_expires: yes

View File

@ -1,7 +0,0 @@
id: push_account_windows
name: Push account windows
version: 1
method: push_account
category: host
type:
- windows

View File

@ -13,3 +13,14 @@ class SecretType(TextChoices):
SSH_KEY = 'ssh_key', _('SSH key')
ACCESS_KEY = 'access_key', _('Access key')
TOKEN = 'token', _('Token')
class AliasAccount(TextChoices):
ALL = '@ALL', _('All')
INPUT = '@INPUT', _('Manual input')
USER = '@USER', _('Dynamic user')
class Source(TextChoices):
LOCAL = 'local', _('Local')
COLLECTED = 'collected', _('Collected')

View File

@ -12,7 +12,7 @@ DEFAULT_PASSWORD_RULES = {
class AutomationTypes(TextChoices):
ping = 'ping', _('Ping')
gather_facts = 'gather_facts', _('Gather facts')
push_account = 'push_account', _('Create account')
push_account = 'push_account', _('Push account')
change_secret = 'change_secret', _('Change secret')
verify_account = 'verify_account', _('Verify account')
gather_accounts = 'gather_accounts', _('Gather accounts')
@ -20,8 +20,9 @@ class AutomationTypes(TextChoices):
@classmethod
def get_type_model(cls, tp):
from assets.models import (
PingAutomation, GatherFactsAutomation, PushAccountAutomation,
ChangeSecretAutomation, VerifyAccountAutomation, GatherAccountsAutomation,
PingAutomation, GatherFactsAutomation,
PushAccountAutomation, ChangeSecretAutomation,
VerifyAccountAutomation, GatherAccountsAutomation,
)
type_model_dict = {
cls.ping: PingAutomation,

View File

@ -1,5 +1,6 @@
from django.db.models import TextChoices
from jumpserver.utils import has_valid_xpack_license
from .protocol import Protocol
@ -53,3 +54,25 @@ class BaseType(TextChoices):
@classmethod
def internal_platforms(cls):
raise NotImplementedError
@classmethod
def get_community_types(cls):
raise NotImplementedError
@classmethod
def get_types(cls):
tps = [tp for tp in cls]
if not has_valid_xpack_license():
tps = cls.get_community_types()
return tps
@classmethod
def get_choices(cls):
tps = cls.get_types()
cls_choices = cls.choices
return [
choice for choice in cls_choices
if choice[0] in tps
]

View File

@ -25,7 +25,6 @@ class CloudTypes(BaseType):
'gather_facts_enabled': False,
'verify_account_enabled': False,
'change_secret_enabled': False,
'push_account_enabled': False,
'gather_accounts_enabled': False,
}
}
@ -49,3 +48,9 @@ class CloudTypes(BaseType):
cls.PRIVATE: [{'name': 'Vmware-vSphere'}],
cls.K8S: [{'name': 'Kubernetes'}],
}
@classmethod
def get_community_types(cls):
return [
cls.K8S, cls.PUBLIC, cls.PRIVATE
]

View File

@ -1,4 +1,3 @@
from .base import BaseType
@ -34,7 +33,6 @@ class DatabaseTypes(BaseType):
'gather_accounts_enabled': True,
'verify_account_enabled': True,
'change_secret_enabled': True,
'push_account_enabled': True,
}
}
return constrains
@ -62,3 +60,8 @@ class DatabaseTypes(BaseType):
cls.REDIS: [{'name': 'Redis'}],
}
@classmethod
def get_community_types(cls):
return [
cls.MYSQL, cls.MARIADB, cls.MONGODB, cls.REDIS
]

View File

@ -40,7 +40,6 @@ class DeviceTypes(BaseType):
'gather_accounts_enabled': False,
'verify_account_enabled': False,
'change_secret_enabled': False,
'push_account_enabled': False,
}
}
@ -52,3 +51,9 @@ class DeviceTypes(BaseType):
cls.ROUTER: [],
cls.FIREWALL: []
}
@classmethod
def get_community_types(cls):
return [
cls.GENERAL, cls.SWITCH, cls.ROUTER, cls.FIREWALL
]

View File

@ -34,7 +34,7 @@ class HostTypes(BaseType):
def _get_protocol_constrains(cls) -> dict:
return {
'*': {
'choices': ['ssh', 'telnet', 'vnc', 'rdp']
'choices': ['ssh', 'telnet', 'vnc', 'rdp']
},
cls.WINDOWS: {
'choices': ['rdp', 'ssh', 'vnc']
@ -54,7 +54,6 @@ class HostTypes(BaseType):
'gather_accounts_enabled': True,
'verify_account_enabled': True,
'change_secret_enabled': True,
'push_account_enabled': True,
},
cls.WINDOWS: {
'ansible_config': {
@ -76,7 +75,6 @@ class HostTypes(BaseType):
{'name': 'macOS'},
{'name': 'BSD'},
{'name': 'AIX', 'automation': {
'push_account_method': 'push_account_aix',
'change_secret_method': 'push_secret_aix'
}}
],
@ -97,7 +95,7 @@ class HostTypes(BaseType):
{
'name': 'RemoteAppHost',
'_protocols': ['rdp', 'ssh'],
'protocols_setting': {
'protocols_setting': {
'ssh': {
'required': True
}
@ -106,3 +104,9 @@ class HostTypes(BaseType):
],
cls.OTHER_HOST: []
}
@classmethod
def get_community_types(cls) -> list:
return [
cls.LINUX, cls.UNIX, cls.WINDOWS, cls.OTHER_HOST
]

View File

@ -1,4 +1,5 @@
from django.db import models
from common.db.models import ChoicesMixin
__all__ = ['Protocol']
@ -102,9 +103,9 @@ class Protocol(ChoicesMixin, models.TextChoices):
'port': 80,
'secret_types': ['password'],
'setting': {
'username_selector': 'input[type=text]',
'password_selector': 'input[type=password]',
'submit_selector': 'button[type=submit]',
'username_selector': 'name=username',
'password_selector': 'name=password',
'submit_selector': 'id=longin_button',
}
},
}
@ -112,7 +113,7 @@ class Protocol(ChoicesMixin, models.TextChoices):
@classmethod
def settings(cls):
return {
**cls.device_protocols(),
**cls.database_protocols(),
**cls.cloud_protocols()
**cls.device_protocols(),
**cls.database_protocols(),
**cls.cloud_protocols()
}

View File

@ -1,14 +1,15 @@
from collections import defaultdict
from copy import deepcopy
from common.db.models import ChoicesMixin
from common.tree import TreeNode
from django.utils.translation import gettext as _
from common.db.models import ChoicesMixin
from .category import Category
from .host import HostTypes
from .device import DeviceTypes
from .database import DatabaseTypes
from .web import WebTypes
from .cloud import CloudTypes
from .database import DatabaseTypes
from .device import DeviceTypes
from .host import HostTypes
from .web import WebTypes
class AllTypes(ChoicesMixin):
@ -54,7 +55,7 @@ class AllTypes(ChoicesMixin):
item_name = item.replace('_enabled', '')
methods = filter_platform_methods(category, tp, item_name)
methods = [{'name': m['name'], 'id': m['id']} for m in methods]
automation_methods[item_name+'_methods'] = methods
automation_methods[item_name + '_methods'] = methods
automation.update(automation_methods)
constraints['automation'] = automation
return constraints
@ -62,14 +63,18 @@ class AllTypes(ChoicesMixin):
@classmethod
def types(cls, with_constraints=True):
types = []
for category, tps in cls.category_types():
for category, type_cls in cls.category_types():
tps = type_cls.get_types()
types.extend([cls.serialize_type(category, tp, with_constraints) for tp in tps])
return types
@classmethod
def categories(cls, with_constraints=True):
categories = []
for category, tps in cls.category_types():
for category, type_cls in cls.category_types():
tps = type_cls.get_types()
if not tps:
continue
category_data = {
'value': category.value,
'label': category.label,
@ -121,30 +126,82 @@ class AllTypes(ChoicesMixin):
(Category.CLOUD, CloudTypes)
)
@classmethod
def get_types(cls):
tps = []
for i in dict(cls.category_types()).values():
tps.extend(i.get_types())
return tps
@staticmethod
def choice_to_node(choice, pid, opened=True, is_parent=True, meta=None):
node = TreeNode(**{
'id': choice.name,
node = {
'id': pid + '_' + choice.name,
'name': choice.label,
'title': choice.label,
'pId': pid,
'open': opened,
'isParent': is_parent,
})
}
if meta:
node.meta = meta
node['meta'] = meta
return node
@classmethod
def to_tree_nodes(cls):
root = TreeNode(id='ROOT', name='类型节点', title='类型节点')
def platform_to_node(cls, p, pid, include_asset):
node = {
'id': '{}'.format(p.id),
'name': p.name,
'title': p.name,
'pId': pid,
'isParent': include_asset,
'meta': {
'type': 'platform'
}
}
return node
@classmethod
def to_tree_nodes(cls, include_asset):
from ..models import Asset, Platform
asset_platforms = Asset.objects.all().values_list('platform_id', flat=True)
platform_count = defaultdict(int)
for platform_id in asset_platforms:
platform_count[platform_id] += 1
category_type_mapper = defaultdict(int)
platforms = Platform.objects.all()
tp_platforms = defaultdict(list)
for p in platforms:
category_type_mapper[p.category + '_' + p.type] += platform_count[p.id]
category_type_mapper[p.category] += platform_count[p.id]
tp_platforms[p.category + '_' + p.type].append(p)
root = dict(id='ROOT', name=_('All types'), title='所有类型', open=True, isParent=True)
nodes = [root]
for category, types in cls.category_types():
category_node = cls.choice_to_node(category, 'ROOT', meta={'type': 'category'})
for category, type_cls in cls.category_types():
# Category 格式化
meta = {'type': 'category', 'category': category.value}
category_node = cls.choice_to_node(category, 'ROOT', meta=meta)
category_count = category_type_mapper.get(category, 0)
category_node['name'] += f'({category_count})'
nodes.append(category_node)
# Type 格式化
types = type_cls.get_types()
for tp in types:
tp_node = cls.choice_to_node(tp, category_node.id, meta={'type': 'type'})
meta = {'type': 'type', 'category': category.value, '_type': tp.value}
tp_node = cls.choice_to_node(tp, category_node['id'], opened=False, meta=meta)
tp_count = category_type_mapper.get(category + '_' + tp, 0)
tp_node['name'] += f'({tp_count})'
nodes.append(tp_node)
# Platform 格式化
for p in tp_platforms.get(category + '_' + tp, []):
platform_node = cls.platform_to_node(p, tp_node['id'], include_asset)
platform_node['name'] += f'({platform_count.get(p.id, 0)})'
nodes.append(platform_node)
return nodes
@classmethod
@ -253,8 +310,3 @@ class AllTypes(ChoicesMixin):
print("\t- Update platform: {}".format(platform.name))
platform_data = cls.get_type_default_platform(platform.category, platform.type)
cls.create_or_update_by_platform_data(platform.name, platform_data)

View File

@ -44,3 +44,9 @@ class WebTypes(BaseType):
{'name': 'Website'},
],
}
@classmethod
def get_community_types(cls):
return [
cls.WEBSITE,
]

View File

@ -126,17 +126,6 @@ class LabelFilterBackend(filters.BaseFilterBackend):
return queryset
class AssetRelatedByNodeFilterBackend(AssetByNodeFilterBackend):
def filter_node_related_all(self, queryset, node):
return queryset.filter(
Q(asset__nodes__key__istartswith=f'{node.key}:') |
Q(asset__nodes__key=node.key)
).distinct()
def filter_node_related_direct(self, queryset, node):
return queryset.filter(asset__nodes__key=node.key).distinct()
class IpInFilterBackend(filters.BaseFilterBackend):
def filter_queryset(self, request, queryset, view):
ips = request.query_params.get('ips')

View File

@ -12,7 +12,6 @@ def migrate_platform_type_to_lower(apps, *args):
class Migration(migrations.Migration):
dependencies = [
('assets', '0094_auto_20220402_1736'),
]
@ -51,7 +50,7 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='platform',
name='su_method',
field=models.CharField(blank=True, max_length=32, null=True, verbose_name='SU method'),
field=models.CharField(blank=True, max_length=32, null=True, verbose_name='Su method'),
),
migrations.RunPython(migrate_platform_type_to_lower)
]

View File

@ -1,11 +1,9 @@
# Generated by Django 3.1.14 on 2022-04-26 07:54
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('assets', '0095_auto_20220407_1726'),
]
@ -18,7 +16,8 @@ class Migration(migrations.Migration):
('name', models.CharField(max_length=32, verbose_name='Name')),
('port', models.IntegerField(verbose_name='Port')),
('setting', models.JSONField(default=dict, verbose_name='Setting')),
('platform', models.ForeignKey(on_delete=models.deletion.CASCADE, related_name='protocols', to='assets.platform'),),
('platform',
models.ForeignKey(on_delete=models.deletion.CASCADE, related_name='protocols', to='assets.platform'),),
('default', models.BooleanField(default=True, verbose_name='Default')),
('required', models.BooleanField(default=False, verbose_name='Required')),
],
@ -32,20 +31,24 @@ class Migration(migrations.Migration):
('ping_enabled', models.BooleanField(default=False, verbose_name='Ping enabled')),
('ping_method', models.CharField(blank=True, max_length=32, null=True, verbose_name='Ping method')),
('gather_facts_enabled', models.BooleanField(default=False, verbose_name='Gather facts enabled')),
('gather_facts_method', models.TextField(blank=True, max_length=32, null=True, verbose_name='Gather facts method')),
('push_account_enabled', models.BooleanField(default=False, verbose_name='Create account enabled')),
('push_account_method', models.TextField(blank=True, max_length=32, null=True, verbose_name='Create account method')),
('gather_facts_method',
models.TextField(blank=True, max_length=32, null=True, verbose_name='Gather facts method')),
('change_secret_enabled', models.BooleanField(default=False, verbose_name='Change password enabled')),
('change_secret_method', models.TextField(blank=True, max_length=32, null=True, verbose_name='Change password method')),
('change_secret_method',
models.TextField(blank=True, max_length=32, null=True, verbose_name='Change password method')),
('verify_account_enabled', models.BooleanField(default=False, verbose_name='Verify account enabled')),
('verify_account_method', models.TextField(blank=True, max_length=32, null=True, verbose_name='Verify account method')),
('verify_account_method',
models.TextField(blank=True, max_length=32, null=True, verbose_name='Verify account method')),
('gather_accounts_enabled', models.BooleanField(default=False, verbose_name='Gather facts enabled')),
('gather_accounts_method', models.TextField(blank=True, max_length=32, null=True, verbose_name='Gather facts method')),
('gather_accounts_method',
models.TextField(blank=True, max_length=32, null=True, verbose_name='Gather facts method')),
],
),
migrations.AddField(
model_name='platform',
name='automation',
field=models.OneToOneField(blank=True, null=True, on_delete=models.deletion.CASCADE, related_name='platform', to='assets.platformautomation', verbose_name='Automation'),
field=models.OneToOneField(blank=True, null=True, on_delete=models.deletion.CASCADE,
related_name='platform', to='assets.platformautomation',
verbose_name='Automation'),
),
]

View File

@ -1,15 +1,16 @@
# Generated by Django 3.2.12 on 2022-07-11 08:59
import common.db.fields
from django.conf import settings
from django.db import migrations, models
import uuid
import django.db.models.deletion
import simple_history.models
import uuid
from django.conf import settings
from django.db import migrations, models
import common.db.fields
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('assets', '0098_auto_20220430_2126'),
@ -20,14 +21,19 @@ class Migration(migrations.Migration):
name='HistoricalAccount',
fields=[
('id', models.UUIDField(db_index=True, default=uuid.uuid4)),
('secret_type', models.CharField(choices=[('password', 'Password'), ('ssh_key', 'SSH key'), ('access_key', 'Access key'), ('token', 'Token')], default='password', max_length=16, verbose_name='Secret type')),
('secret_type', models.CharField(
choices=[('password', 'Password'), ('ssh_key', 'SSH key'), ('access_key', 'Access key'),
('token', 'Token')], default='password', max_length=16, verbose_name='Secret type')),
('secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Secret')),
('version', models.IntegerField(default=0, verbose_name='Version'),),
('history_id', models.AutoField(primary_key=True, serialize=False)),
('history_date', models.DateTimeField(db_index=True)),
('history_change_reason', models.CharField(max_length=100, null=True)),
('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
('history_type',
models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
('history_user',
models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+',
to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'historical Account',
@ -40,43 +46,55 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='Account',
fields=[
('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
('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)),
('name', models.CharField(max_length=128, verbose_name='Name')),
('username', models.CharField(blank=True, db_index=True, max_length=128, verbose_name='Username')),
('secret_type', models.CharField(choices=[('password', 'Password'), ('ssh_key', 'SSH key'), ('access_key', 'Access key'), ('token', 'Token')], default='password', max_length=16, verbose_name='Secret type')),
('secret_type', models.CharField(
choices=[('password', 'Password'), ('ssh_key', 'SSH key'), ('access_key', 'Access key'),
('token', 'Token')], default='password', max_length=16, verbose_name='Secret type')),
('secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Secret')),
('comment', models.TextField(blank=True, verbose_name='Comment')),
('connectivity', models.CharField(choices=[('unknown', 'Unknown'), ('ok', 'Ok'), ('failed', 'Failed')], default='unknown', max_length=16, verbose_name='Connectivity')),
('comment', models.TextField(blank=True, default='', verbose_name='Comment')),
('connectivity', models.CharField(choices=[('unknown', 'Unknown'), ('ok', 'Ok'), ('failed', 'Failed')],
default='unknown', max_length=16, verbose_name='Connectivity')),
('date_verified', models.DateTimeField(null=True, verbose_name='Date verified')),
('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date created')),
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
('created_by', models.CharField(max_length=128, null=True, verbose_name='Created by')),
('privileged', models.BooleanField(default=False, verbose_name='Privileged')),
('version', models.IntegerField(default=0, verbose_name='Version')),
('asset', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='accounts', to='assets.asset', verbose_name='Asset')),
('asset', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='accounts',
to='assets.asset', verbose_name='Asset')),
],
options={
'verbose_name': 'Account',
'permissions': [('view_accountsecret', 'Can view asset account secret'), ('change_accountsecret', 'Can change asset account secret'), ('view_historyaccount', 'Can view asset history account'), ('view_historyaccountsecret', 'Can view asset history account secret')],
'permissions': [('view_accountsecret', 'Can view asset account secret'),
('change_accountsecret', 'Can change asset account secret'),
('view_historyaccount', 'Can view asset history account'),
('view_historyaccountsecret', 'Can view asset history account secret')],
'unique_together': {('name', 'asset'), ('username', 'asset', 'secret_type')},
},
),
migrations.AddField(
model_name='account',
name='su_from',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='su_to', to='assets.account', verbose_name='Su from'),
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='su_to',
to='assets.account', verbose_name='Su from'),
),
migrations.CreateModel(
name='AccountTemplate',
fields=[
('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
('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)),
('name', models.CharField(max_length=128, verbose_name='Name')),
('username', models.CharField(blank=True, db_index=True, max_length=128, verbose_name='Username')),
('secret_type', models.CharField(choices=[('password', 'Password'), ('ssh_key', 'SSH key'), ('access_key', 'Access key'), ('token', 'Token')], default='password', max_length=16, verbose_name='Secret type'),),
('secret_type', models.CharField(
choices=[('password', 'Password'), ('ssh_key', 'SSH key'), ('access_key', 'Access key'),
('token', 'Token')], default='password', max_length=16, verbose_name='Secret type'),),
('secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Secret')),
('comment', models.TextField(blank=True, verbose_name='Comment')),
('comment', models.TextField(blank=True, default='', verbose_name='Comment')),
('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date created')),
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
('created_by', models.CharField(max_length=128, null=True, verbose_name='Created by')),

View File

@ -75,33 +75,11 @@ class Migration(migrations.Migration):
name='updated_by',
field=models.CharField(blank=True, max_length=32, null=True, verbose_name='Updated by'),
),
migrations.AlterField(
model_name='platformautomation',
name='push_account_enabled',
field=models.BooleanField(default=False, verbose_name='Push account enabled'),
),
migrations.AlterField(
model_name='platformautomation',
name='push_account_method',
field=models.TextField(blank=True, max_length=32, null=True, verbose_name='Push account method'),
),
migrations.AlterField(
model_name='platformprotocol',
name='default',
field=models.BooleanField(default=False, verbose_name='Default'),
),
migrations.CreateModel(
name='DiscoveryAccountAutomation',
fields=[
('baseautomation_ptr',
models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True,
primary_key=True, serialize=False, to='assets.baseautomation')),
],
options={
'verbose_name': 'Discovery account automation',
},
bases=('assets.baseautomation',),
),
migrations.CreateModel(
name='GatherFactsAutomation',
fields=[

View File

@ -0,0 +1,34 @@
# Generated by Django 3.2.14 on 2022-12-15 07:08
from django.db import migrations
def migrate_del_macos(apps, schema_editor):
db_alias = schema_editor.connection.alias
asset_model = apps.get_model('assets', 'Asset')
platform_model = apps.get_model('assets', 'Platform')
old_macos = platform_model.objects.using(db_alias).filter(
name='MacOS', type='macos'
).first()
new_macos = platform_model.objects.using(db_alias).filter(
name='macOS', type='unix'
).first()
if not old_macos or not new_macos:
return
asset_model.objects.using(db_alias).filter(
platform=old_macos
).update(platform=new_macos)
platform_model.objects.using(db_alias).filter(id=old_macos.id).delete()
class Migration(migrations.Migration):
dependencies = [
('assets', '0113_auto_20221122_2015'),
]
operations = [
migrations.RunPython(migrate_del_macos),
]

View File

@ -0,0 +1,183 @@
# Generated by Django 3.2.14 on 2022-12-20 11:56
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0114_remove_redundant_macos'),
]
operations = [
migrations.AddField(
model_name='accountbackupplan',
name='updated_by',
field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'),
),
migrations.AddField(
model_name='baseautomation',
name='updated_by',
field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'),
),
migrations.AddField(
model_name='changesecretrecord',
name='comment',
field=models.TextField(blank=True, default='', verbose_name='Comment'),
),
migrations.AddField(
model_name='domain',
name='created_by',
field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'),
),
migrations.AddField(
model_name='domain',
name='date_updated',
field=models.DateTimeField(auto_now=True, verbose_name='Date updated'),
),
migrations.AddField(
model_name='domain',
name='updated_by',
field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'),
),
migrations.AddField(
model_name='favoriteasset',
name='comment',
field=models.TextField(blank=True, default='', verbose_name='Comment'),
),
migrations.AddField(
model_name='favoriteasset',
name='updated_by',
field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'),
),
migrations.AddField(
model_name='gathereduser',
name='comment',
field=models.TextField(blank=True, default='', verbose_name='Comment'),
),
migrations.AddField(
model_name='gathereduser',
name='created_by',
field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'),
),
migrations.AddField(
model_name='gathereduser',
name='updated_by',
field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'),
),
migrations.AddField(
model_name='node',
name='comment',
field=models.TextField(blank=True, default='', verbose_name='Comment'),
),
migrations.AddField(
model_name='node',
name='created_by',
field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'),
),
migrations.AddField(
model_name='node',
name='date_created',
field=models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created'),
),
migrations.AddField(
model_name='node',
name='date_updated',
field=models.DateTimeField(auto_now=True, verbose_name='Date updated'),
),
migrations.AddField(
model_name='node',
name='updated_by',
field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'),
),
migrations.AlterField(
model_name='account',
name='created_by',
field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'),
),
migrations.AlterField(
model_name='account',
name='updated_by',
field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'),
),
migrations.AlterField(
model_name='accountbackupplan',
name='comment',
field=models.TextField(blank=True, default='', verbose_name='Comment'),
),
migrations.AlterField(
model_name='accountbackupplan',
name='created_by',
field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'),
),
migrations.AlterField(
model_name='accounttemplate',
name='created_by',
field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'),
),
migrations.AlterField(
model_name='accounttemplate',
name='updated_by',
field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'),
),
migrations.AlterField(
model_name='asset',
name='created_by',
field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'),
),
migrations.AlterField(
model_name='asset',
name='updated_by',
field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'),
),
migrations.AlterField(
model_name='baseautomation',
name='comment',
field=models.TextField(blank=True, default='', verbose_name='Comment'),
),
migrations.AlterField(
model_name='baseautomation',
name='created_by',
field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'),
),
migrations.AlterField(
model_name='changesecretrecord',
name='created_by',
field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'),
),
migrations.AlterField(
model_name='changesecretrecord',
name='updated_by',
field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'),
),
migrations.AlterField(
model_name='domain',
name='comment',
field=models.TextField(blank=True, default='', verbose_name='Comment'),
),
migrations.AlterField(
model_name='favoriteasset',
name='created_by',
field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'),
),
migrations.AlterField(
model_name='gathereduser',
name='date_created',
field=models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created'),
),
migrations.AlterField(
model_name='label',
name='comment',
field=models.TextField(blank=True, default='', verbose_name='Comment'),
),
migrations.AlterField(
model_name='label',
name='created_by',
field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'),
),
migrations.AlterField(
model_name='label',
name='updated_by',
field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'),
),
]

View File

@ -0,0 +1,17 @@
# Generated by Django 3.2.16 on 2022-12-22 11:50
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('assets', '0115_auto_20221220_1956'),
]
operations = [
migrations.AlterModelOptions(
name='automationexecution',
options={'permissions': [('view_changesecretexecution', 'Can view change secret execution'), ('add_changesecretexection', 'Can add change secret execution'), ('view_gatheraccountsexecution', 'Can view gather accounts execution'), ('add_gatheraccountsexecution', 'Can add gather accounts execution')], 'verbose_name': 'Automation task execution'},
),
]

View File

@ -0,0 +1,17 @@
# Generated by Django 3.2.16 on 2022-12-23 07:36
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('assets', '0116_alter_automationexecution_options'),
]
operations = [
migrations.AlterModelOptions(
name='gateway',
options={'verbose_name': 'Gateway'},
),
]

View File

@ -0,0 +1,50 @@
# Generated by Django 3.2.14 on 2022-12-27 07:04
import common.db.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0117_alter_gateway_options'),
]
operations = [
migrations.AddField(
model_name='pushaccountautomation',
name='password_rules',
field=models.JSONField(default=dict, verbose_name='Password rules'),
),
migrations.AddField(
model_name='pushaccountautomation',
name='secret',
field=common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Secret'),
),
migrations.AddField(
model_name='pushaccountautomation',
name='secret_strategy',
field=models.CharField(choices=[('specific', 'Specific'), ('random_one', 'All assets use the same random password'), ('random_all', 'All assets use different random password')], default='specific', max_length=16, verbose_name='Secret strategy'),
),
migrations.AddField(
model_name='pushaccountautomation',
name='secret_type',
field=models.CharField(choices=[('password', 'Password'), ('ssh_key', 'SSH key'), ('access_key', 'Access key'), ('token', 'Token')], default='password', max_length=16, verbose_name='Secret type'),
),
migrations.AddField(
model_name='pushaccountautomation',
name='ssh_key_change_strategy',
field=models.CharField(choices=[('add', 'Append SSH KEY'), ('set', 'Empty and append SSH KEY'), ('set_jms', 'Replace (The key generated by JumpServer) ')], default='add', max_length=16, verbose_name='SSH key change strategy'),
),
migrations.AddField(
model_name='pushaccountautomation',
name='username',
field=models.CharField(default='', max_length=128, verbose_name='Username'),
preserve_default=False,
),
migrations.AlterField(
model_name='baseautomation',
name='type',
field=models.CharField(choices=[('ping', 'Ping'), ('gather_facts', 'Gather facts'), ('push_account', 'Push account'), ('change_secret', 'Change secret'), ('verify_account', 'Verify account'), ('gather_accounts', 'Gather accounts')], max_length=16, verbose_name='Type'),
),
]

View File

@ -0,0 +1,21 @@
# Generated by Django 3.2.16 on 2022-12-27 09:40
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0118_auto_20221227_1504'),
]
operations = [
migrations.AddField(
model_name='account',
name='source',
field=models.CharField(default='local', max_length=30, verbose_name='Source'),
),
migrations.DeleteModel(
name='GatheredUser',
),
]

View File

@ -7,7 +7,6 @@ from .gateway import *
from .domain import *
from .node import *
from .utils import *
from .gathered_user import *
from .favorite_asset import *
from .account import *
from .backup import *

View File

@ -3,7 +3,7 @@ from django.utils.translation import gettext_lazy as _
from simple_history.models import HistoricalRecords
from common.utils import lazyproperty
from ..const import AliasAccount, Source
from .base import AbsConnectivity, BaseAccount
__all__ = ['Account', 'AccountTemplate']
@ -41,11 +41,6 @@ class AccountHistoricalRecords(HistoricalRecords):
class Account(AbsConnectivity, BaseAccount):
class AliasAccount(models.TextChoices):
ALL = '@ALL', _('All')
INPUT = '@INPUT', _('Manual input')
USER = '@USER', _('Dynamic user')
asset = models.ForeignKey(
'assets.Asset', related_name='accounts',
on_delete=models.CASCADE, verbose_name=_('Asset')
@ -56,6 +51,7 @@ class Account(AbsConnectivity, BaseAccount):
)
version = models.IntegerField(default=0, verbose_name=_('Version'))
history = AccountHistoricalRecords(included_fields=['id', 'secret', 'secret_type', 'version'])
source = models.CharField(max_length=30, default=Source.LOCAL, verbose_name=_('Source'))
class Meta:
verbose_name = _('Account')
@ -74,21 +70,32 @@ class Account(AbsConnectivity, BaseAccount):
def platform(self):
return self.asset.platform
@lazyproperty
def alias(self):
if self.username.startswith('@'):
return self.username
return self.name
def __str__(self):
return '{}'.format(self.username)
@lazyproperty
def has_secret(self):
return bool(self.secret)
@classmethod
def get_manual_account(cls):
""" @INPUT 手动登录的账号(any) """
return cls(name=cls.AliasAccount.INPUT.label, username=cls.AliasAccount.INPUT.value, secret=None)
return cls(name=AliasAccount.INPUT.label, username=AliasAccount.INPUT.value, secret=None)
@classmethod
def get_user_account(cls, username):
""" @USER 动态用户的账号(self) """
return cls(name=cls.AliasAccount.USER.label, username=cls.AliasAccount.USER.value)
return cls(name=AliasAccount.USER.label, username=AliasAccount.USER.value)
def get_su_from_accounts(self):
return self.asset.accounts.exclude(id=self.id)
""" 排除自己和以自己为 su-from 的账号 """
return self.asset.accounts.exclude(id=self.id).exclude(su_from=self)
class AccountTemplate(BaseAccount):

View File

@ -2,18 +2,16 @@
# -*- coding: utf-8 -*-
#
import uuid
import logging
from collections import defaultdict
from django.db import models
from django.db.models import Q
from django.utils.translation import ugettext_lazy as _
from common.utils import lazyproperty
from orgs.mixins.models import OrgManager, JMSOrgBaseModel
from ..platform import Platform
from ..base import AbsConnectivity
from ..platform import Platform
__all__ = ['Asset', 'AssetQuerySet', 'default_node', 'Protocol']
logger = logging.getLogger(__name__)
@ -53,7 +51,7 @@ class NodesRelationMixin:
NODES_CACHE_KEY = 'ASSET_NODES_{}'
ALL_ASSET_NODES_CACHE_KEY = 'ALL_ASSETS_NODES'
CACHE_TIME = 3600 * 24 * 7
id = ""
id: str
_all_nodes_keys = None
def get_nodes(self):
@ -65,16 +63,29 @@ class NodesRelationMixin:
def get_all_nodes(self, flat=False):
from ..node import Node
node_keys = self.get_all_node_keys()
nodes = Node.objects.filter(key__in=node_keys).distinct()
if not flat:
return nodes
node_ids = set(nodes.values_list('id', flat=True))
return node_ids
def get_all_node_keys(self):
node_keys = set()
for node in self.get_nodes():
ancestor_keys = node.get_ancestor_keys(with_self=True)
node_keys.update(ancestor_keys)
nodes = Node.objects.filter(key__in=node_keys).distinct()
if flat:
node_ids = set(nodes.values_list('id', flat=True))
return node_ids
else:
return nodes
return node_keys
@classmethod
def get_all_nodes_for_assets(cls, assets):
from ..node import Node
node_keys = set()
for asset in assets:
asset_node_keys = asset.get_all_node_keys()
node_keys.update(asset_node_keys)
nodes = Node.objects.filter(key__in=node_keys)
return nodes
class Protocol(models.Model):
@ -87,7 +98,6 @@ class Protocol(models.Model):
class Asset(NodesRelationMixin, AbsConnectivity, JMSOrgBaseModel):
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
name = models.CharField(max_length=128, verbose_name=_('Name'))
address = models.CharField(max_length=128, verbose_name=_('IP'), db_index=True)
platform = models.ForeignKey(Platform, on_delete=models.PROTECT, verbose_name=_("Platform"), related_name='assets')
@ -97,7 +107,6 @@ class Asset(NodesRelationMixin, AbsConnectivity, JMSOrgBaseModel):
verbose_name=_("Nodes"))
is_active = models.BooleanField(default=True, verbose_name=_('Is active'))
labels = models.ManyToManyField('assets.Label', blank=True, related_name='assets', verbose_name=_("Labels"))
comment = models.TextField(default='', blank=True, verbose_name=_('Comment'))
info = models.JSONField(verbose_name='Info', default=dict, blank=True)
objects = AssetManager.from_queryset(AssetQuerySet)()

View File

@ -5,4 +5,3 @@ from .gather_facts import *
from .change_secret import *
from .verify_account import *
from .gather_accounts import *
from .discovery_account import *

View File

@ -1,25 +1,24 @@
import uuid
from celery import current_task
from django.db import models
from django.utils.translation import ugettext_lazy as _
from common.const.choices import Trigger
from common.mixins.models import CommonModelMixin
from common.db.fields import EncryptJsonDictTextField
from orgs.mixins.models import OrgModelMixin
from ops.mixin import PeriodTaskModelMixin
from assets.const import AutomationTypes
from assets.models import Node, Asset
from assets.tasks import execute_automation
from assets.const import AutomationTypes
from common.const.choices import Trigger
from common.db.fields import EncryptJsonDictTextField
from ops.mixin import PeriodTaskModelMixin
from orgs.mixins.models import OrgModelMixin, JMSOrgBaseModel
class BaseAutomation(CommonModelMixin, PeriodTaskModelMixin, OrgModelMixin):
class BaseAutomation(PeriodTaskModelMixin, JMSOrgBaseModel):
accounts = models.JSONField(default=list, verbose_name=_("Accounts"))
nodes = models.ManyToManyField('assets.Node', blank=True, verbose_name=_("Nodes"))
assets = models.ManyToManyField('assets.Asset', blank=True, verbose_name=_("Assets"))
type = models.CharField(max_length=16, choices=AutomationTypes.choices, verbose_name=_('Type'))
is_active = models.BooleanField(default=True, verbose_name=_("Is active"))
comment = models.TextField(blank=True, verbose_name=_('Comment'))
def __str__(self):
return self.name + '@' + str(self.created_by)
@ -102,6 +101,12 @@ class AutomationExecution(OrgModelMixin):
class Meta:
verbose_name = _('Automation task execution')
permissions = [
('view_changesecretexecution', _('Can view change secret execution')),
('add_changesecretexection', _('Can add change secret execution')),
('view_gatheraccountsexecution', _('Can view gather accounts execution')),
('add_gatheraccountsexecution', _('Can add gather accounts execution')),
]
@property
def manager_type(self):

View File

@ -1,15 +1,15 @@
from django.db import models
from django.utils.translation import ugettext_lazy as _
from assets.const import AutomationTypes, SecretType, SecretStrategy, SSHKeyStrategy
from common.db import fields
from common.db.models import JMSBaseModel
from assets.const import AutomationTypes, SecretType, SecretStrategy, SSHKeyStrategy
from .base import BaseAutomation
__all__ = ['ChangeSecretAutomation', 'ChangeSecretRecord']
__all__ = ['ChangeSecretAutomation', 'ChangeSecretRecord', 'ChangeSecretMixin']
class ChangeSecretAutomation(BaseAutomation):
class ChangeSecretMixin(models.Model):
secret_type = models.CharField(
choices=SecretType.choices, max_length=16,
default=SecretType.PASSWORD, verbose_name=_('Secret type')
@ -24,6 +24,12 @@ class ChangeSecretAutomation(BaseAutomation):
choices=SSHKeyStrategy.choices, max_length=16,
default=SSHKeyStrategy.add, verbose_name=_('SSH key change strategy')
)
class Meta:
abstract = True
class ChangeSecretAutomation(BaseAutomation, ChangeSecretMixin):
recipients = models.ManyToManyField('users.User', verbose_name=_("Recipient"), blank=True)
def save(self, *args, **kwargs):

View File

@ -1,15 +0,0 @@
from django.utils.translation import ugettext_lazy as _
from .base import BaseAutomation
class DiscoveryAccountAutomation(BaseAutomation):
class Meta:
verbose_name = _("Discovery account automation")
def to_attr_json(self):
attr_json = super().to_attr_json()
attr_json.update({
'type': 'discover_account'
})
return attr_json

View File

@ -1,12 +1,16 @@
from django.db import models
from django.utils.translation import ugettext_lazy as _
from assets.const import AutomationTypes
from .base import BaseAutomation
from .change_secret import ChangeSecretMixin
__all__ = ['PushAccountAutomation']
class PushAccountAutomation(BaseAutomation):
class PushAccountAutomation(BaseAutomation, ChangeSecretMixin):
accounts = None
username = models.CharField(max_length=128, verbose_name=_('Username'))
def save(self, *args, **kwargs):
self.type = AutomationTypes.push_account

View File

@ -7,26 +7,23 @@ from celery import current_task
from django.db import models
from django.utils.translation import ugettext_lazy as _
from orgs.mixins.models import OrgModelMixin
from ops.mixin import PeriodTaskModelMixin
from common.utils import get_logger
from common.const.choices import Trigger
from common.db.encoder import ModelJSONFieldEncoder
from common.mixins.models import CommonModelMixin
from common.utils import get_logger
from ops.mixin import PeriodTaskModelMixin
from orgs.mixins.models import OrgModelMixin, JMSOrgBaseModel
__all__ = ['AccountBackupPlan', 'AccountBackupPlanExecution']
logger = get_logger(__file__)
class AccountBackupPlan(CommonModelMixin, PeriodTaskModelMixin, OrgModelMixin):
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
class AccountBackupPlan(PeriodTaskModelMixin, JMSOrgBaseModel):
types = models.JSONField(default=list)
recipients = models.ManyToManyField(
'users.User', related_name='recipient_escape_route_plans', blank=True,
verbose_name=_("Recipient")
)
comment = models.TextField(blank=True, verbose_name=_('Comment'))
def __str__(self):
return f'{self.name}({self.org_id})'

View File

@ -69,8 +69,6 @@ class BaseAccount(JMSOrgBaseModel):
secret = fields.EncryptTextField(blank=True, null=True, verbose_name=_('Secret'))
privileged = models.BooleanField(verbose_name=_("Privileged"), default=False)
is_active = models.BooleanField(default=True, verbose_name=_("Is active"))
comment = models.TextField(blank=True, verbose_name=_('Comment'))
created_by = models.CharField(max_length=128, null=True, verbose_name=_('Created by'))
objects = BaseAccountManager.from_queryset(BaseAccountQuerySet)()

View File

@ -7,11 +7,6 @@ from django.db import models
from django.utils.translation import ugettext_lazy as _
from common.utils import get_logger
from users.models import User, UserGroup
from applications.models import Application
from ..models import SystemUser, Asset, Node
from common.utils import lazyproperty, get_logger, get_object_or_none
from orgs.mixins.models import OrgModelMixin
logger = get_logger(__file__)

View File

@ -1,14 +1,12 @@
# -*- coding: utf-8 -*-
#
import uuid
import random
from django.db import models
from django.utils.translation import ugettext_lazy as _
from common.utils import get_logger, lazyproperty
from orgs.mixins.models import OrgModelMixin
from orgs.mixins.models import JMSOrgBaseModel
from .gateway import Gateway
logger = get_logger(__file__)
@ -16,11 +14,8 @@ logger = get_logger(__file__)
__all__ = ['Domain']
class Domain(OrgModelMixin):
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
class Domain(JMSOrgBaseModel):
name = models.CharField(max_length=128, verbose_name=_('Name'))
comment = models.TextField(blank=True, verbose_name=_('Comment'))
date_created = models.DateTimeField(auto_now_add=True, null=True, verbose_name=_('Date created'))
class Meta:
verbose_name = _("Domain")
@ -51,5 +46,3 @@ class Domain(OrgModelMixin):
@classmethod
def get_gateway_queryset(cls):
return Gateway.objects.all()

View File

@ -2,13 +2,12 @@
#
from django.db import models
from common.mixins.models import CommonModelMixin
from common.db.models import JMSBaseModel
__all__ = ['FavoriteAsset']
class FavoriteAsset(CommonModelMixin):
class FavoriteAsset(JMSBaseModel):
user = models.ForeignKey('users.User', on_delete=models.CASCADE)
asset = models.ForeignKey('assets.Asset', on_delete=models.CASCADE)

View File

@ -37,6 +37,7 @@ class Gateway(Host):
class Meta:
proxy = True
verbose_name = _("Gateway")
def save(self, *args, **kwargs):
self.platform = self.default_platform()

View File

@ -1,38 +0,0 @@
# -*- coding: utf-8 -*-
#
import uuid
from django.db import models
from django.utils.translation import ugettext_lazy as _
from orgs.mixins.models import OrgModelMixin
__all__ = ['GatheredUser']
class GatheredUser(OrgModelMixin):
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
asset = models.ForeignKey('assets.Asset', on_delete=models.CASCADE, verbose_name=_("Asset"))
username = models.CharField(max_length=32, blank=True, db_index=True, verbose_name=_('Username'))
present = models.BooleanField(default=True, verbose_name=_("Present"))
date_last_login = models.DateTimeField(null=True, verbose_name=_("Date last login"))
ip_last_login = models.CharField(max_length=39, default='', verbose_name=_("IP last login"))
date_created = models.DateTimeField(auto_now_add=True, verbose_name=_("Date created"))
date_updated = models.DateTimeField(auto_now=True, verbose_name=_("Date updated"))
@property
def name(self):
return self.asset.name
@property
def ip(self):
return self.asset.address
class Meta:
verbose_name = _('GatherUser')
ordering = ['asset']
def __str__(self):
return '{}: {}'.format(self.asset.name, self.username)

View File

@ -4,6 +4,7 @@
from django.db import models
from django.utils.translation import ugettext_lazy as _
from common.utils import lazyproperty
from orgs.mixins.models import JMSOrgBaseModel
@ -19,7 +20,6 @@ class Label(JMSOrgBaseModel):
category = models.CharField(max_length=128, choices=CATEGORY_CHOICES,
default=USER_CATEGORY, verbose_name=_("Category"))
is_active = models.BooleanField(default=True, verbose_name=_("Is active"))
comment = models.TextField(blank=True, null=True, verbose_name=_("Comment"))
@classmethod
def get_queryset_group_by_name(cls):
@ -27,6 +27,10 @@ class Label(JMSOrgBaseModel):
for name in names:
yield name, cls.objects.filter(name=name)
@lazyproperty
def asset_count(self):
return self.assets.count()
def __str__(self):
return "{}:{}".format(self.name, self.value)

View File

@ -1,29 +1,24 @@
# -*- coding: utf-8 -*-
#
import re
import time
import uuid
import threading
import os
import time
import uuid
from collections import defaultdict
from django.core.cache import cache
from django.db import models, transaction
from django.db.models import Q, Manager
from django.db.utils import IntegrityError
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ugettext
from django.db.transaction import atomic
from django.core.cache import cache
from django.utils.translation import ugettext
from django.utils.translation import ugettext_lazy as _
from common.utils.lock import DistributedLock
from common.utils.common import timeit
from common.db.models import output_as_string
from common.utils import get_logger
from orgs.mixins.models import OrgModelMixin, OrgManager
from orgs.utils import get_current_org, tmp_to_org, tmp_to_root_org
from common.utils.lock import DistributedLock
from orgs.mixins.models import OrgManager, JMSOrgBaseModel
from orgs.models import Organization
from orgs.utils import get_current_org, tmp_to_org, tmp_to_root_org
__all__ = ['Node', 'FamilyMixin', 'compute_parent_key', 'NodeQuerySet']
logger = get_logger(__name__)
@ -178,9 +173,7 @@ class FamilyMixin:
return parent_keys
def get_ancestor_keys(self, with_self=False):
return self.get_node_ancestor_keys(
self.key, with_self=with_self
)
return self.get_node_ancestor_keys(self.key, with_self=with_self)
@property
def ancestors(self):
@ -437,6 +430,12 @@ class NodeAssetsMixin(NodeAllAssetsMappingMixin):
assets = Asset.objects.filter(nodes=self)
return assets.distinct()
def get_assets_for_tree(self):
return self.get_assets().only(
"id", "name", "address", "platform_id",
"org_id", "is_active"
).prefetch_related('platform')
def get_valid_assets(self):
return self.get_assets().valid()
@ -547,7 +546,7 @@ class SomeNodesMixin:
return root_nodes
class Node(OrgModelMixin, SomeNodesMixin, FamilyMixin, NodeAssetsMixin):
class Node(JMSOrgBaseModel, SomeNodesMixin, FamilyMixin, NodeAssetsMixin):
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
key = models.CharField(unique=True, max_length=64, verbose_name=_("Key")) # '1:1:1:1'
value = models.CharField(max_length=128, verbose_name=_("Value"))

View File

@ -2,9 +2,8 @@ from django.db import models
from django.utils.translation import gettext_lazy as _
from assets.const import AllTypes
from common.db.fields import JsonDictTextField
from assets.const import Protocol
from common.db.fields import JsonDictTextField
__all__ = ['Platform', 'PlatformProtocol', 'PlatformAutomation']
@ -45,8 +44,6 @@ class PlatformAutomation(models.Model):
ping_method = models.CharField(max_length=32, blank=True, null=True, verbose_name=_("Ping method"))
gather_facts_enabled = models.BooleanField(default=False, verbose_name=_("Gather facts enabled"))
gather_facts_method = models.TextField(max_length=32, blank=True, null=True, verbose_name=_("Gather facts method"))
push_account_enabled = models.BooleanField(default=False, verbose_name=_("Push account enabled"))
push_account_method = models.TextField(max_length=32, blank=True, null=True, verbose_name=_("Push account method"))
change_secret_enabled = models.BooleanField(default=False, verbose_name=_("Change password enabled"))
change_secret_method = models.TextField(
max_length=32, blank=True, null=True, verbose_name=_("Change password method"))
@ -83,7 +80,7 @@ class Platform(models.Model):
protocols_enabled = models.BooleanField(default=True, verbose_name=_("Protocols enabled"))
# 账号有关的
su_enabled = models.BooleanField(default=False, verbose_name=_("Su enabled"))
su_method = models.CharField(max_length=32, blank=True, null=True, verbose_name=_("SU method"))
su_method = models.CharField(max_length=32, blank=True, null=True, verbose_name=_("Su method"))
automation = models.OneToOneField(PlatformAutomation, on_delete=models.CASCADE, related_name='platform',
blank=True, null=True, verbose_name=_("Automation"))

View File

@ -7,7 +7,6 @@ from django.utils.translation import ugettext_lazy as _
from common.utils import validate_ssh_private_key
__all__ = [
'private_key_validator',
]
@ -22,8 +21,6 @@ def private_key_validator(value):
def update_internal_platforms(platform_model):
from assets.const import AllTypes
platforms = [
{'name': 'Linux', 'category': 'host', 'type': 'linux'},
{'name': 'BSD', 'category': 'host', 'type': 'unix'},
@ -32,7 +29,6 @@ def update_internal_platforms(platform_model):
{'name': 'Windows', 'category': 'host', 'type': 'unix'},
{
'name': 'AIX', 'category': 'host', 'type': 'unix',
'push_account_method': 'create_account_aix',
'change_secret_method': 'change_secret_aix',
},
{'name': 'Windows', 'category': 'host', 'type': 'windows'},

View File

@ -6,7 +6,6 @@ from .label import *
from .node import *
from .gateway import *
from .domain import *
from .gathered_user import *
from .favorite_asset import *
from .account import *
from .platform import *

View File

@ -1,15 +1,15 @@
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
from common.drf.serializers import SecretReadableMixin
from common.drf.fields import ObjectRelatedField, LabeledChoiceField
from assets.tasks import push_accounts_to_assets
from assets.const import SecretType, Source
from assets.models import Account, AccountTemplate, Asset
from assets.tasks import push_accounts_to_assets
from common.drf.fields import ObjectRelatedField, LabeledChoiceField
from common.drf.serializers import SecretReadableMixin, BulkModelSerializer
from .base import BaseAccountSerializer
from assets.const import SecretType
class AccountSerializerCreateMixin(serializers.ModelSerializer):
class AccountSerializerCreateMixin(BulkModelSerializer):
template = serializers.UUIDField(
required=False, allow_null=True, write_only=True,
label=_('Account template')
@ -53,35 +53,47 @@ class AccountSerializerCreateMixin(serializers.ModelSerializer):
return instance
class AccountAssetSerializer(serializers.ModelSerializer):
platform = ObjectRelatedField(read_only=True)
class Meta:
model = Asset
fields = ['id', 'name', 'address', 'platform']
def to_internal_value(self, data):
if isinstance(data, dict):
i = data.get('id')
else:
i = data
try:
return Asset.objects.get(id=i)
except Asset.DoesNotExist:
raise serializers.ValidationError(_('Asset not found'))
class AccountSerializer(AccountSerializerCreateMixin, BaseAccountSerializer):
asset = ObjectRelatedField(
required=False, queryset=Asset.objects,
label=_('Asset'), attrs=('id', 'name', 'address', 'platform_id')
)
asset = AccountAssetSerializer(label=_('Asset'))
source = LabeledChoiceField(choices=Source.choices, label=_("Source"), read_only=True)
su_from = ObjectRelatedField(
required=False, queryset=Account.objects,
label=_('Account'), attrs=('id', 'name', 'username')
required=False, queryset=Account.objects, allow_null=True, allow_empty=True,
label=_('Su from'), attrs=('id', 'name', 'username')
)
class Meta(BaseAccountSerializer.Meta):
model = Account
fields = BaseAccountSerializer.Meta.fields \
+ ['su_from', 'version', 'asset'] \
+ ['template', 'push_now']
+ ['su_from', 'version', 'asset'] \
+ ['template', 'push_now', 'source']
extra_kwargs = {
**BaseAccountSerializer.Meta.extra_kwargs,
'name': {'required': False, 'allow_null': True},
}
def __init__(self, *args, data=None, **kwargs):
super().__init__(*args, data=data, **kwargs)
if data and 'name' not in data:
username = data.get('username')
if username is not None:
data['name'] = username
if hasattr(self, 'initial_data') and \
not getattr(self, 'initial_data', None):
delattr(self, 'initial_data')
def validate_name(self, value):
if not value:
value = self.initial_data.get('username')
return value
@classmethod
def setup_eager_loading(cls, queryset):

View File

@ -34,7 +34,6 @@ class AccountBackupPlanSerializer(PeriodTaskSerializerMixin, BulkOrgResourceMode
class AccountBackupPlanExecutionSerializer(serializers.ModelSerializer):
trigger = LabeledChoiceField(choices=Trigger.choices, label=_('Trigger mode'))
class Meta:
model = AccountBackupPlanExecution

View File

@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from assets.models import BaseAccount
from assets.serializers.base import AuthValidateMixin
@ -9,6 +10,8 @@ __all__ = ['BaseAccountSerializer']
class BaseAccountSerializer(AuthValidateMixin, BulkOrgResourceModelSerializer):
has_secret = serializers.BooleanField(label=_("Has secret"), read_only=True)
class Meta:
model = BaseAccount
fields_mini = ['id', 'name', 'username']

View File

@ -1,5 +1,5 @@
from common.drf.serializers import SecretReadableMixin
from assets.models import AccountTemplate
from common.drf.serializers import SecretReadableMixin
from .base import BaseAccountSerializer

View File

@ -1,21 +1,22 @@
# -*- coding: utf-8 -*-
#
from rest_framework import serializers
from django.utils.translation import ugettext_lazy as _
from django.db.transaction import atomic
from django.db.models import F
from django.db.transaction import atomic
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
from common.drf.serializers import WritableNestedModelSerializer
from common.drf.fields import LabeledChoiceField, ObjectRelatedField
from orgs.mixins.serializers import OrgResourceSerializerMixin
from common.drf.serializers import WritableNestedModelSerializer
from orgs.mixins.serializers import BulkOrgResourceSerializerMixin
from ..account import AccountSerializer
from ...models import Asset, Node, Platform, Label, Domain, Account, Protocol
from ...const import Category, AllTypes
from ...models import Asset, Node, Platform, Label, Domain, Account, Protocol
__all__ = [
'AssetSerializer', 'AssetSimpleSerializer', 'MiniAssetSerializer',
'AssetTaskSerializer', 'AssetsTaskSerializer', 'AssetProtocolsSerializer',
'AssetDetailSerializer',
]
@ -58,7 +59,7 @@ class AssetAccountSerializer(AccountSerializer):
fields = fields_mini + fields_write_only
class AssetSerializer(OrgResourceSerializerMixin, WritableNestedModelSerializer):
class AssetSerializer(BulkOrgResourceSerializerMixin, WritableNestedModelSerializer):
category = LabeledChoiceField(choices=Category.choices, read_only=True, label=_('Category'))
type = LabeledChoiceField(choices=AllTypes.choices(), read_only=True, label=_('Type'))
domain = ObjectRelatedField(required=False, queryset=Domain.objects, label=_('Domain'), allow_null=True)
@ -66,49 +67,26 @@ class AssetSerializer(OrgResourceSerializerMixin, WritableNestedModelSerializer)
nodes = ObjectRelatedField(many=True, required=False, queryset=Node.objects, label=_('Nodes'))
labels = AssetLabelSerializer(many=True, required=False, label=_('Labels'))
protocols = AssetProtocolsSerializer(many=True, required=False, label=_('Protocols'))
accounts = AssetAccountSerializer(many=True, required=False, label=_('Accounts'))
enabled_info = serializers.SerializerMethodField()
accounts = AssetAccountSerializer(many=True, required=False, label=_('Account'))
class Meta:
model = Asset
fields_mini = ['id', 'name', 'address', 'enabled_info']
fields_mini = ['id', 'name', 'address']
fields_small = fields_mini + ['is_active', 'comment']
fields_fk = ['domain', 'platform', 'platform']
fields_m2m = [
'nodes', 'labels', 'protocols', 'accounts', 'nodes_display',
]
read_only_fields = [
'category', 'type', 'specific', 'info',
'connectivity', 'date_verified', 'created_by',
'date_created'
'category', 'type', 'info',
'connectivity', 'date_verified',
'created_by', 'date_created'
]
fields = fields_small + fields_fk + fields_m2m + read_only_fields
extra_kwargs = {
'name': {'label': _("Name")},
'address': {'label': _('Address')},
}
def get_field_names(self, declared_fields, info):
names = super().get_field_names(declared_fields, info)
if self.__class__.__name__ != 'AssetSerializer':
names.remove('specific')
return names
@staticmethod
def get_enabled_info(obj):
platform = obj.platform
automation = platform.automation
return {
'su_enabled': platform.su_enabled,
'ping_enabled': automation.ping_enabled,
'domain_enabled': platform.domain_enabled,
'ansible_enabled': automation.ansible_enabled,
'protocols_enabled': platform.protocols_enabled,
'gather_facts_enabled': automation.gather_facts_enabled,
'push_account_enabled': automation.push_account_enabled,
'change_secret_enabled': automation.change_secret_enabled,
'verify_account_enabled': automation.verify_account_enabled,
'gather_accounts_enabled': automation.gather_accounts_enabled,
'nodes_display': {'label': _('Node path')},
}
@classmethod
@ -117,7 +95,7 @@ class AssetSerializer(OrgResourceSerializerMixin, WritableNestedModelSerializer)
queryset = queryset.prefetch_related('domain', 'platform', 'protocols') \
.annotate(category=F("platform__category")) \
.annotate(type=F("platform__type"))
queryset = queryset.prefetch_related('nodes', 'labels')
queryset = queryset.prefetch_related('nodes', 'labels', 'accounts')
return queryset
def perform_nodes_display_create(self, instance, nodes_display):
@ -188,6 +166,30 @@ class AssetSerializer(OrgResourceSerializerMixin, WritableNestedModelSerializer)
return instance
class AssetDetailSerializer(AssetSerializer):
accounts = AssetAccountSerializer(many=True, required=False, label=_('Accounts'))
enabled_info = serializers.SerializerMethodField()
class Meta(AssetSerializer.Meta):
fields = AssetSerializer.Meta.fields + ['accounts', 'enabled_info', 'info', 'specific']
@staticmethod
def get_enabled_info(obj):
platform = obj.platform
automation = platform.automation
return {
'su_enabled': platform.su_enabled,
'ping_enabled': automation.ping_enabled,
'domain_enabled': platform.domain_enabled,
'ansible_enabled': automation.ansible_enabled,
'protocols_enabled': platform.protocols_enabled,
'gather_facts_enabled': automation.gather_facts_enabled,
'change_secret_enabled': automation.change_secret_enabled,
'verify_account_enabled': automation.verify_account_enabled,
'gather_accounts_enabled': automation.gather_accounts_enabled,
}
class MiniAssetSerializer(serializers.ModelSerializer):
class Meta:
model = Asset

View File

@ -1,4 +1,3 @@
from assets.models import Web
from .common import AssetSerializer
@ -19,12 +18,12 @@ class WebSerializer(AssetSerializer):
'label': 'URL'
},
'username_selector': {
'default': 'input[type=text]'
'default': 'name=username'
},
'password_selector': {
'default': 'input[type=password]'
'default': 'name=password'
},
'submit_selector': {
'default': 'button[type=submit]',
'default': 'id=longin_button',
},
}

View File

@ -1,26 +0,0 @@
# -*- coding: utf-8 -*-
#
from django.utils.translation import ugettext_lazy as _
from orgs.mixins.serializers import OrgResourceModelSerializerMixin
from common.drf.fields import ObjectRelatedField
from ..models import GatheredUser, Asset
class GatheredUserSerializer(OrgResourceModelSerializerMixin):
asset = ObjectRelatedField(queryset=Asset.objects, label=_('Asset'))
class Meta:
model = GatheredUser
fields_mini = ['id']
fields_small = fields_mini + [
'username', 'ip_last_login', 'present', 'name',
'date_last_login', 'date_created', 'date_updated'
]
fields_fk = ['asset', 'ip']
fields = fields_small + fields_fk
read_only_fields = fields
extra_kwargs = {
'name': {'label': _("Hostname")},
'ip': {'label': 'IP'},
}

View File

@ -1,25 +1,22 @@
# -*- coding: utf-8 -*-
#
from rest_framework import serializers
from django.db.models import Count
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from ..models import Label
class LabelSerializer(BulkOrgResourceModelSerializer):
asset_count = serializers.SerializerMethodField(label=_("Assets amount"))
category_display = serializers.ReadOnlyField(source='get_category_display', label=_('Category display'))
asset_count = serializers.ReadOnlyField(label=_("Assets amount"))
class Meta:
model = Label
fields_mini = ['id', 'name']
fields_small = fields_mini + [
'value', 'category', 'category_display',
'is_active',
'date_created',
'comment',
'value', 'category', 'is_active',
'date_created', 'comment',
]
fields_m2m = ['asset_count', 'assets']
fields = fields_small + fields_m2m
@ -30,14 +27,10 @@ class LabelSerializer(BulkOrgResourceModelSerializer):
'assets': {'required': False, 'label': _('Asset')}
}
@staticmethod
def get_asset_count(obj):
return obj.assets.count()
def get_field_names(self, declared_fields, info):
fields = super().get_field_names(declared_fields, info)
fields.extend(['get_category_display'])
return fields
@classmethod
def setup_eager_loading(cls, queryset):
queryset = queryset.annotate(asset_count=Count('assets'))
return queryset
class LabelDistinctSerializer(BulkOrgResourceModelSerializer):

View File

@ -25,7 +25,7 @@ class ProtocolSettingSerializer(serializers.Serializer):
sftp_home = serializers.CharField(default="/tmp", label=_("SFTP home"))
# HTTP
auto_fill = serializers.BooleanField(default=False, label=_("Auto fill"))
autofile = serializers.BooleanField(default=False, label=_("Autofill"))
username_selector = serializers.CharField(
default="", allow_blank=True, label=_("Username selector")
)
@ -38,37 +38,26 @@ class ProtocolSettingSerializer(serializers.Serializer):
class PlatformAutomationSerializer(serializers.ModelSerializer):
class Meta:
model = PlatformAutomation
fields = [
"id",
"ansible_enabled",
"ansible_config",
"ping_enabled",
"ping_method",
"gather_facts_enabled",
"gather_facts_method",
"push_account_enabled",
"push_account_method",
"change_secret_enabled",
"change_secret_method",
"verify_account_enabled",
"verify_account_method",
"gather_accounts_enabled",
"gather_accounts_method",
"ansible_enabled", "ansible_config",
"ping_enabled", "ping_method",
"gather_facts_enabled", "gather_facts_method",
"change_secret_enabled", "change_secret_method",
"verify_account_enabled", "verify_account_method",
"gather_accounts_enabled", "gather_accounts_method",
]
extra_kwargs = {
"ping_enabled": {"label": "启用资产探测"},
"ping_method": {"label": "探测方式"},
"gather_facts_enabled": {"label": "启用收集信息"},
"ping_method": {"label": "资产探测方式"},
"gather_facts_enabled": {"label": "收集资产信息"},
"gather_facts_method": {"label": "收集信息方式"},
"verify_account_enabled": {"label": "启用校验账号"},
"verify_account_method": {"label": "校验账号方式"},
"push_account_enabled": {"label": "启用推送账号"},
"push_account_method": {"label": "推送账号方式"},
"change_secret_enabled": {"label": "启用账号改密"},
"change_secret_method": {"label": "账号创建改密方式"},
"change_secret_method": {"label": "账号改密方式"},
"gather_accounts_enabled": {"label": "启用账号收集"},
"gather_accounts_method": {"label": "收集账号方式"},
}
@ -81,13 +70,8 @@ class PlatformProtocolsSerializer(serializers.ModelSerializer):
class Meta:
model = PlatformProtocol
fields = [
"id",
"name",
"port",
"primary",
"default",
"required",
"secret_types",
"id", "name", "port", "primary",
"default", "required", "secret_types",
"setting",
]
@ -113,17 +97,12 @@ class PlatformSerializer(WritableNestedModelSerializer):
model = Platform
fields_mini = ["id", "name", "internal"]
fields_small = fields_mini + [
"category",
"type",
"charset",
"category", "type", "charset",
]
fields = fields_small + [
"protocols_enabled",
"protocols",
"domain_enabled",
"su_enabled",
"su_method",
"automation",
"protocols_enabled", "protocols",
"domain_enabled", "su_enabled",
"su_method", "automation",
"comment",
]
extra_kwargs = {

View File

@ -103,6 +103,7 @@ def on_asset_nodes_add(instance, action, reverse, pk_set, **kwargs):
# m2m_model.objects.bulk_create(to_create)
#
RELATED_NODE_IDS = '_related_node_ids'

View File

@ -20,13 +20,9 @@ logger = get_logger(__file__)
# ------------------------------------
def get_node_assets_mapping_for_memory_pub_sub():
return RedisPubSub('fm.node_all_asset_ids_memory_mapping')
class NodeAssetsMappingForMemoryPubSub(LazyObject):
def _setup(self):
self._wrapped = get_node_assets_mapping_for_memory_pub_sub()
self._wrapped = RedisPubSub('fm.node_all_asset_ids_memory_mapping')
node_assets_mapping_for_memory_pub_sub = NodeAssetsMappingForMemoryPubSub()

View File

@ -23,13 +23,13 @@ router.register(r'labels', api.LabelViewSet, 'label')
router.register(r'nodes', api.NodeViewSet, 'node')
router.register(r'domains', api.DomainViewSet, 'domain')
router.register(r'gateways', api.GatewayViewSet, 'gateway')
router.register(r'gathered-users', api.GatheredUserViewSet, 'gathered-user')
router.register(r'favorite-assets', api.FavoriteAssetViewSet, 'favorite-asset')
router.register(r'account-backup-plans', api.AccountBackupPlanViewSet, 'account-backup')
router.register(r'account-backup-plan-executions', api.AccountBackupPlanExecutionViewSet, 'account-backup-execution')
router.register(r'change-secret-automations', api.ChangeSecretAutomationViewSet, 'change-secret-automation')
router.register(r'automation-executions', api.AutomationExecutionViewSet, 'automation-execution')
router.register(r'change-secret-executions', api.ChangSecretExecutionViewSet, 'change-secret-execution')
router.register(r'gather-account-executions', api.GatherAccountsExecutionViewSet, 'gather-account-execution')
router.register(r'change-secret-records', api.ChangeSecretRecordViewSet, 'change-secret-record')
router.register(r'gather-account-automations', api.GatherAccountsAutomationViewSet, 'gather-account-automation')
@ -50,7 +50,6 @@ urlpatterns = [
name='account-secret-history'),
path('nodes/category/tree/', api.CategoryTreeApi.as_view(), name='asset-category-tree'),
path('nodes/tree/', api.NodeListAsTreeApi.as_view(), name='node-tree'),
path('nodes/children/tree/', api.NodeChildrenAsTreeApi.as_view(), name='node-children-tree'),
path('nodes/<uuid:pk>/children/', api.NodeChildrenApi.as_view(), name='node-children'),
path('nodes/children/', api.NodeChildrenApi.as_view(), name='node-children-2'),

View File

@ -0,0 +1,2 @@
from .k8s import *
from .node import *

Some files were not shown because too many files have changed in this diff Show More