Merge branch 'v3' of github.com:jumpserver/jumpserver into v3

pull/9147/head
Bai 2022-12-02 12:41:05 +08:00
commit 563b9f77a6
72 changed files with 2212 additions and 503 deletions

View File

@ -99,6 +99,7 @@ VOLUME /opt/jumpserver/data
VOLUME /opt/jumpserver/logs
ENV LANG=zh_CN.UTF-8
ENV ANSIBLE_LIBRARY=/opt/jumpserver/apps/ops/ansible/modules
EXPOSE 8080

View File

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

View File

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

View File

@ -0,0 +1,52 @@
from rest_framework.views import APIView
from rest_framework import status
from django.http.response import JsonResponse
from django.utils.translation import ugettext_lazy as _
from common.drf.api import JMSBulkModelViewSet
from common.const.choices import ConnectMethodChoices
from ..models import ConnectACL
from .. import serializers
__all__ = ['ConnectACLViewSet', 'ConnectMethodsAPI', 'ConnectMethodPermissionsAPI']
class ConnectACLViewSet(JMSBulkModelViewSet):
queryset = ConnectACL.objects.all()
filterset_fields = ('name', )
search_fields = ('name',)
serializer_class = serializers.ConnectACLSerializer
class ConnectMethodsAPI(APIView):
rbac_perms = {
'GET': 'acls.view_connnectacl',
}
@staticmethod
def get(request, *args, **kwargs):
data = []
for m in ConnectMethodChoices.choices:
data.append({'label': m[1], 'value': m[0]})
return JsonResponse(data, safe=False)
class ConnectMethodPermissionsAPI(APIView):
rbac_perms = {
'GET': 'acls.view_connnectacl',
}
@staticmethod
def get(request, *args, **kwargs):
login_type = request.query_params.get('login_type')
if not login_type:
rules = ConnectACL().all_rules(request.user)
return JsonResponse({'rules': rules})
acl = ConnectACL.match(request.user, login_type)
if acl:
err = _('The current user is not allowed to login in this way')
return JsonResponse({'error': err})
else:
return JsonResponse({'msg': 'ok'})

View File

@ -5,7 +5,6 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('acls', '0004_auto_20220831_1658'),
@ -15,7 +14,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='loginacl',
name='action',
field=models.CharField(choices=[('reject', 'Reject'), ('allow', 'Allow'), ('confirm', 'Confirm')], default='reject', max_length=64, verbose_name='Action'),
field=models.CharField(default='reject', max_length=64, verbose_name='Action'),
),
migrations.AlterField(
model_name='loginacl',
@ -25,7 +24,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='loginassetacl',
name='action',
field=models.CharField(choices=[('reject', 'Reject'), ('allow', 'Allow'), ('confirm', 'Confirm')], default='reject', max_length=64, verbose_name='Action'),
field=models.CharField(default='reject', max_length=64, verbose_name='Action'),
),
migrations.AlterField(
model_name='loginassetacl',

View File

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

View File

@ -0,0 +1,21 @@
# Generated by Django 3.2.14 on 2022-12-02 02:48
from django.db import migrations
def migrate_login_type(apps, schema_editor):
login_asset_model = apps.get_model('acls', 'LoginAssetACL')
login_asset_model.objects.filter(action='login_confirm').update(action='review')
login_system_model = apps.get_model('acls', 'LoginACL')
login_system_model.objects.filter(action='confirm').update(action='review')
class Migration(migrations.Migration):
dependencies = [
('acls', '0006_commandfilteracl_commandgroup'),
]
operations = [
migrations.RunPython(migrate_login_type),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 3.2.14 on 2022-12-02 04:25
from django.db import migrations, models
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'),
),
]

View File

@ -1,16 +1,18 @@
from django.db import models
from django.utils.translation import ugettext_lazy as _
from django.core.validators import MinValueValidator, MaxValueValidator
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.utils import contains_ip
__all__ = ['BaseACL', 'BaseACLQuerySet', 'ACLManager']
__all__ = ['BaseACL', 'BaseACLQuerySet', 'ACLManager', 'AssetAccountUserACLQuerySet']
class ActionChoices(models.TextChoices):
reject = 'reject', _('Reject')
allow = 'allow', _('Allow')
confirm = 'confirm', _('Confirm')
accept = 'allow', _('Allow')
review = 'review', _('Review')
class BaseACLQuerySet(models.QuerySet):
@ -27,6 +29,32 @@ class BaseACLQuerySet(models.QuerySet):
return self.inactive()
class AssetAccountUserACLQuerySet(BaseACLQuerySet):
def filter_user(self, user):
return self.filter(
Q(users__username_group__contains=user.username) |
Q(users__username_group__contains='*')
)
def filter_asset(self, asset):
queryset = self.filter(
Q(assets__name_group__contains=asset.name) |
Q(assets__name_group__contains='*')
)
ids = [
q.id for q in queryset
if contains_ip(asset.address, q.assets.get('address_group', []))
]
queryset = self.filter(id__in=ids)
return queryset
def filter_account(self, account_username):
return self.filter(
Q(accounts__username_group__contains=account_username) |
Q(accounts__username_group__contains='*')
)
class ACLManager(models.Manager):
def valid(self):
return self.get_queryset().valid()
@ -39,10 +67,7 @@ class BaseACL(CommonModelMixin):
help_text=_("1-100, the lower the value will be match first"),
validators=[MinValueValidator(1), MaxValueValidator(100)]
)
action = models.CharField(
max_length=64, verbose_name=_('Action'),
choices=ActionChoices.choices, default=ActionChoices.reject
)
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'))

View File

@ -6,11 +6,11 @@ from django.db import models
from django.db.models import Q
from django.utils.translation import ugettext_lazy as _
from users.models import User, UserGroup
from orgs.mixins.models import JMSOrgBaseModel
from common.utils import lazyproperty, get_logger, get_object_or_none
from orgs.mixins.models import JMSOrgBaseModel
from orgs.mixins.models import OrgModelMixin
from .base import BaseACL
from users.models import User, UserGroup
from .base import BaseACL, AssetAccountUserACLQuerySet, ACLManager
logger = get_logger(__file__)
@ -24,6 +24,7 @@ class CommandGroup(JMSOrgBaseModel):
type = models.CharField(max_length=16, default=Type.command, choices=Type.choices, verbose_name=_("Type"))
content = models.TextField(verbose_name=_("Content"), help_text=_("One line one command"))
ignore_case = models.BooleanField(default=True, verbose_name=_('Ignore case'))
comment = models.TextField(blank=True, verbose_name=_("Comment"))
class Meta:
unique_together = [('org_id', 'name')]
@ -50,7 +51,6 @@ class CommandGroup(JMSOrgBaseModel):
if ' ' in _cmd:
regex.append(cmd)
continue
if not cmd:
continue
@ -89,6 +89,19 @@ class CommandGroup(JMSOrgBaseModel):
def __str__(self):
return '{} % {}'.format(self.type, self.content)
class CommandFilterACL(OrgModelMixin, BaseACL):
users = models.JSONField(verbose_name=_('User'))
assets = models.JSONField(verbose_name=_('Asset'))
accounts = models.JSONField(verbose_name=_('Account'))
commands = models.ManyToManyField(CommandGroup, verbose_name=_('Commands'))
objects = ACLManager.from_queryset(AssetAccountUserACLQuerySet)()
class Meta:
unique_together = ('name', 'org_id')
ordering = ('priority', '-date_updated', 'name')
verbose_name = _('Command acl')
def create_command_confirm_ticket(self, run_command, session, cmd_filter_rule, org_id):
from tickets.const import TicketType
from tickets.models import ApplyCommandTicket
@ -147,16 +160,3 @@ class CommandGroup(JMSOrgBaseModel):
else:
rules = cls.objects.none()
return rules
class CommandFilterACL(OrgModelMixin, BaseACL):
# 条件
users = models.JSONField(verbose_name=_('User'))
accounts = models.JSONField(verbose_name=_('Account'))
assets = models.JSONField(verbose_name=_('Asset'))
commands = models.ManyToManyField(CommandGroup, verbose_name=_('Commands'))
class Meta:
unique_together = ('name', 'org_id')
ordering = ('priority', '-date_updated', 'name')
verbose_name = _('Command acl')

View File

@ -0,0 +1,120 @@
from django.db import models
from django.core.cache import cache
from django.utils.translation import ugettext_lazy as _
from common.utils.connection import get_redis_client
from common.const.choices import ConnectMethodChoices
from orgs.mixins.models import OrgManager, OrgModelMixin
from .base import BaseACL, BaseACLQuerySet
class ACLManager(OrgManager):
def valid(self):
return self.get_queryset().valid()
class ConnectACL(BaseACL, OrgModelMixin):
ConnectACLUserCacheKey = 'CONNECT_ACL_USER_{}'
ConnectACLUserCacheTTL = 600
class ActionChoices(models.TextChoices):
reject = 'reject', _('Reject')
# 用户
users = models.ManyToManyField(
'users.User', related_name='connect_acls', blank=True,
verbose_name=_("User")
)
user_groups = models.ManyToManyField(
'users.UserGroup', related_name='connect_acls', blank=True,
verbose_name=_("User group"),
)
rules = models.JSONField(default=list, verbose_name=_('Rule'))
# 动作
action = models.CharField(
max_length=64, verbose_name=_('Action'),
choices=ActionChoices.choices, default=ActionChoices.reject
)
objects = ACLManager.from_queryset(BaseACLQuerySet)()
class Meta:
ordering = ('priority', '-date_updated', 'name')
verbose_name = _('Connect acl')
def __str__(self):
return self.name
@property
def rules_display(self):
return ', '.join(
[ConnectMethodChoices.get_label(i) for i in self.rules]
)
def is_action(self, action):
return self.action == action
@staticmethod
def match(user, connect_type):
if not user:
return
user_acls = user.connect_acls.all().valid().distinct()
for acl in user_acls:
if connect_type in acl.rules:
return acl
for user_group in user.groups.all():
acls = user_group.connect_acls.all().valid().distinct()
for acl in acls:
if connect_type in acl.rules:
return acl
def _get_all_rules_from_cache(self, user):
find = False
cache_key = self.ConnectACLUserCacheKey.format(user.id)
rules = cache.get(cache_key)
if rules is not None:
find = True
return rules, find
@staticmethod
def _get_all_rules_from_db(user):
connect_rules = set()
user_acls = user.connect_acls.all().valid()
user_acl_rules = user_acls.values_list('id', 'rules')
for r_id, rule in user_acl_rules:
connect_rules.update(rule)
for ug in user.groups.all():
user_group_acls = ug.connect_acls.all().valid()
user_group_rules = user_group_acls.values_list('id', 'rules')
for r_id, rule in user_group_rules:
connect_rules.update(rule)
return list(connect_rules)
def set_all_rules_to_cache(self, key, rules):
cache.set(key, rules, self.ConnectACLUserCacheTTL)
def all_rules(self, user):
rules, find = self._get_all_rules_from_cache(user)
if not find:
rules = self._get_all_rules_from_db(user)
self.set_all_rules_to_cache(
self.ConnectACLUserCacheKey.format(user.id), rules
)
return rules
def clear_rules_cache(self):
cache.delete_pattern(
self.ConnectACLUserCacheKey.format('*')
)
def save(self, *args, **kwargs):
self.clear_rules_cache()
return super().save(*args, **kwargs)
def delete(self, using=None, keep_parents=False):
self.clear_rules_cache()
return super().delete(using=using, keep_parents=keep_parents)

View File

@ -1,35 +1,8 @@
from django.db import models
from django.db.models import Q
from django.utils.translation import ugettext_lazy as _
from orgs.mixins.models import OrgModelMixin, OrgManager
from .base import BaseACL, BaseACLQuerySet, ACLManager
from common.utils.ip import contains_ip
class ACLQuerySet(BaseACLQuerySet):
def filter_user(self, user):
return self.filter(
Q(users__username_group__contains=user.username) |
Q(users__username_group__contains='*')
)
def filter_asset(self, asset):
queryset = self.filter(
Q(assets__name_group__contains=asset.name) |
Q(assets__name_group__contains='*')
)
ids = [
q.id for q in queryset
if contains_ip(asset.address, q.assets.get('address_group', []))
]
queryset = LoginAssetACL.objects.filter(id__in=ids)
return queryset
def filter_account(self, account_username):
return self.filter(
Q(accounts__username_group__contains=account_username) |
Q(accounts__username_group__contains='*')
)
from orgs.mixins.models import OrgModelMixin
from .base import BaseACL, ACLManager, AssetAccountUserACLQuerySet
class LoginAssetACL(BaseACL, OrgModelMixin):
@ -38,7 +11,7 @@ class LoginAssetACL(BaseACL, OrgModelMixin):
accounts = models.JSONField(verbose_name=_('Account'))
assets = models.JSONField(verbose_name=_('Asset'))
objects = ACLManager.from_queryset(ACLQuerySet)()
objects = ACLManager.from_queryset(AssetAccountUserACLQuerySet)()
class Meta:
unique_together = ('name', 'org_id')
@ -65,4 +38,3 @@ class LoginAssetACL(BaseACL, OrgModelMixin):
ticket = ApplyLoginAssetTicket.objects.create(**data)
ticket.open_by_system(assignees)
return ticket

View File

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

View File

@ -0,0 +1,94 @@
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
from acls.models.base import ActionChoices
from common.drf.fields import LabeledChoiceField, ObjectRelatedField
from orgs.models import Organization
from users.models import User
common_help_text = _(
"Format for comma-delimited string, with * indicating a match all. "
)
class ACLUsersSerializer(serializers.Serializer):
username_group = serializers.ListField(
default=["*"],
child=serializers.CharField(max_length=128),
label=_("Username"),
help_text=common_help_text,
)
class ACLAssestsSerializer(serializers.Serializer):
address_group_help_text = _(
"Format for comma-delimited string, with * indicating a match all. "
"Such as: "
"192.168.10.1, 192.168.1.0/24, 10.1.1.1-10.1.1.20, 2001:db8:2de::e13, 2001:db8:1a:1110::/64"
" (Domain name support)"
)
name_group = serializers.ListField(
default=["*"],
child=serializers.CharField(max_length=128),
label=_("Name"),
help_text=common_help_text,
)
address_group = serializers.ListField(
default=["*"],
child=serializers.CharField(max_length=1024),
label=_("IP/Host"),
help_text=address_group_help_text,
)
class ACLAccountsSerializer(serializers.Serializer):
username_group = serializers.ListField(
default=["*"],
child=serializers.CharField(max_length=128),
label=_("Username"),
help_text=common_help_text,
)
class BaseUserAssetAccountACLSerializerMixin(serializers.Serializer):
users = ACLUsersSerializer()
assets = ACLAssestsSerializer()
accounts = ACLAccountsSerializer()
reviewers = ObjectRelatedField(
queryset=User.objects, many=True, required=False, label=_('Reviewers')
)
reviewers_amount = serializers.IntegerField(read_only=True, source="reviewers.count")
action = LabeledChoiceField(
choices=ActionChoices.choices, label=_("Action")
)
class Meta:
fields_mini = ["id", "name"]
fields_small = fields_mini + [
"users", "accounts", "assets", "is_active",
"date_created", "date_updated", "priority",
"action", "comment", "created_by", "org_id",
]
fields_m2m = ["reviewers", "reviewers_amount"]
fields = fields_small + fields_m2m
extra_kwargs = {
"reviewers": {"allow_null": False, "required": True},
"priority": {"default": 50},
"is_active": {"default": True},
}
def validate_reviewers(self, reviewers):
org_id = self.fields["org_id"].default()
org = Organization.get_instance(org_id)
if not org:
error = _("The organization `{}` does not exist".format(org_id))
raise serializers.ValidationError(error)
users = org.get_members()
valid_reviewers = list(set(reviewers) & set(users))
if not valid_reviewers:
error = _(
"None of the reviewers belong to Organization `{}`".format(org.name)
)
raise serializers.ValidationError(error)
return valid_reviewers

View File

@ -0,0 +1,22 @@
from django.utils.translation import ugettext_lazy as _
from acls.models import CommandGroup, CommandFilterACL
from common.drf.fields import ObjectRelatedField
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from .base import BaseUserAssetAccountACLSerializerMixin as BaseSerializer
__all__ = ["CommandFilterACLSerializer", "CommandGroupSerializer"]
class CommandGroupSerializer(BulkOrgResourceModelSerializer):
class Meta:
model = CommandGroup
fields = ['id', 'name', 'type', 'content', 'comment']
class CommandFilterACLSerializer(BaseSerializer, BulkOrgResourceModelSerializer):
commands = ObjectRelatedField(queryset=CommandGroup.objects, many=True, required=False, label=_('Commands'))
class Meta(BaseSerializer.Meta):
model = CommandFilterACL
fields = BaseSerializer.Meta.fields + ['commands']

View File

@ -0,0 +1,36 @@
from django.utils.translation import ugettext as _
from rest_framework import serializers
from common.drf.serializers import BulkModelSerializer
from common.const.choices import ConnectMethodChoices
from ..models import ConnectACL
__all__ = ['ConnectACLSerializer', ]
class ConnectACLSerializer(BulkModelSerializer):
action_display = serializers.ReadOnlyField(source='get_action_display', label=_('Action'))
class Meta:
model = ConnectACL
fields_mini = ['id', 'name']
fields_small = fields_mini + [
'priority', 'rules', 'rules_display', 'action', 'action_display', 'is_active',
'date_created', 'date_updated', 'comment', 'created_by'
]
fields_m2m = ['users', 'user_groups']
fields = fields_small + fields_m2m
extra_kwargs = {
'priority': {'default': 50},
'is_active': {'default': True}
}
@staticmethod
def validate_rules(rules):
for r in rules:
label = ConnectMethodChoices.get_label(r)
if not label:
error = _('Invalid connection method: {}').format(r)
raise serializers.ValidationError(error)
return rules

View File

@ -1,12 +1,12 @@
from django.utils.translation import ugettext as _
from rest_framework import serializers
from common.drf.serializers import BulkModelSerializer
from common.drf.serializers import MethodSerializer
from common.drf.fields import ObjectRelatedField
from common.drf.fields import ObjectRelatedField, LabeledChoiceField
from common.drf.serializers import BulkModelSerializer, MethodSerializer
from jumpserver.utils import has_valid_xpack_license
from users.models import User
from ..models import LoginACL
from .rules import RuleSerializer
from ..models import LoginACL
__all__ = [
"LoginACLSerializer",
@ -22,9 +22,7 @@ class LoginACLSerializer(BulkModelSerializer):
reviewers = ObjectRelatedField(
queryset=User.objects, label=_("Reviewers"), many=True, required=False
)
action_display = serializers.ReadOnlyField(
source="get_action_display", label=_("Action")
)
action = LabeledChoiceField(choices=LoginACL.ActionChoices.choices)
reviewers_amount = serializers.IntegerField(
read_only=True, source="reviewers.count"
)
@ -34,17 +32,9 @@ class LoginACLSerializer(BulkModelSerializer):
model = LoginACL
fields_mini = ["id", "name"]
fields_small = fields_mini + [
"priority",
"rules",
"action",
"action_display",
"is_active",
"user",
"date_created",
"date_updated",
"reviewers_amount",
"comment",
"created_by",
"priority", "user", "rules", "action",
"is_active", "date_created", "date_updated",
"reviewers_amount", "comment", "created_by",
]
fields_fk = ["user"]
fields_m2m = ["reviewers"]
@ -65,7 +55,7 @@ class LoginACLSerializer(BulkModelSerializer):
return
choices = action._choices
if not has_valid_xpack_license():
choices.pop(LoginACL.ActionChoices.confirm, None)
choices.pop(LoginACL.ActionChoices.review, None)
action._choices = choices
def get_rules_serializer(self):

View File

@ -1,109 +1,11 @@
from rest_framework import serializers
from django.utils.translation import ugettext_lazy as _
from common.drf.fields import LabeledChoiceField
from common.drf.fields import ObjectRelatedField
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from orgs.models import Organization
from users.models import User
from acls import models
from .base import BaseUserAssetAccountACLSerializerMixin as BaseSerializer
from ..models import LoginAssetACL
__all__ = ["LoginAssetACLSerializer"]
common_help_text = _(
"Format for comma-delimited string, with * indicating a match all. "
)
class LoginAssetACLUsersSerializer(serializers.Serializer):
username_group = serializers.ListField(
default=["*"],
child=serializers.CharField(max_length=128),
label=_("Username"),
help_text=common_help_text,
)
class LoginAssetACLAssestsSerializer(serializers.Serializer):
address_group_help_text = _(
"Format for comma-delimited string, with * indicating a match all. "
"Such as: "
"192.168.10.1, 192.168.1.0/24, 10.1.1.1-10.1.1.20, 2001:db8:2de::e13, 2001:db8:1a:1110::/64"
" (Domain name support)"
)
name_group = serializers.ListField(
default=["*"],
child=serializers.CharField(max_length=128),
label=_("Name"),
help_text=common_help_text,
)
address_group = serializers.ListField(
default=["*"],
child=serializers.CharField(max_length=1024),
label=_("IP/Host"),
help_text=address_group_help_text,
)
class LoginAssetACLAccountsSerializer(serializers.Serializer):
username_group = serializers.ListField(
default=["*"],
child=serializers.CharField(max_length=128),
label=_("Username"),
help_text=common_help_text,
)
class LoginAssetACLSerializer(BulkOrgResourceModelSerializer):
users = LoginAssetACLUsersSerializer()
assets = LoginAssetACLAssestsSerializer()
accounts = LoginAssetACLAccountsSerializer()
reviewers = ObjectRelatedField(
queryset=User.objects, many=True, required=False, label=_('Reviewers')
)
reviewers_amount = serializers.IntegerField(read_only=True, source="reviewers.count")
action = LabeledChoiceField(
choices=models.LoginAssetACL.ActionChoices.choices, label=_("Action")
)
class Meta:
model = models.LoginAssetACL
fields_mini = ["id", "name"]
fields_small = fields_mini + [
"users",
"accounts",
"assets",
"is_active",
"date_created",
"date_updated",
"priority",
"action",
"comment",
"created_by",
"org_id",
]
fields_m2m = ["reviewers", "reviewers_amount"]
fields = fields_small + fields_m2m
extra_kwargs = {
"reviewers": {"allow_null": False, "required": True},
"priority": {"default": 50},
"is_active": {"default": True},
}
def validate_reviewers(self, reviewers):
org_id = self.fields["org_id"].default()
org = Organization.get_instance(org_id)
if not org:
error = _("The organization `{}` does not exist".format(org_id))
raise serializers.ValidationError(error)
users = org.get_members()
valid_reviewers = list(set(reviewers) & set(users))
if not valid_reviewers:
error = _(
"None of the reviewers belong to Organization `{}`".format(org.name)
)
raise serializers.ValidationError(error)
return valid_reviewers
class LoginAssetACLSerializer(BaseSerializer, BulkOrgResourceModelSerializer):
class Meta(BaseSerializer.Meta):
model = LoginAssetACL

View File

@ -1,14 +1,15 @@
from django.urls import path
from rest_framework_bulk.routes import BulkRouter
from .. import api
app_name = 'acls'
router = BulkRouter()
router.register(r'login-acls', api.LoginACLViewSet, 'login-acl')
router.register(r'login-asset-acls', api.LoginAssetACLViewSet, 'login-asset-acl')
router.register(r'command-groups', api.CommandGroupViewSet, 'command-group')
router.register(r'command-filter-acls', api.CommandFilterACLViewSet, 'command-filter-acl')
urlpatterns = [
path('login-asset/check/', api.LoginAssetCheckAPI.as_view(), name='login-asset-check'),

View File

@ -1,5 +1,4 @@
# ~*~ coding: utf-8 ~*~
from django.db.models import F
from django.views.generic.detail import SingleObjectMixin
from django.utils.translation import ugettext as _
from rest_framework.views import APIView, Response
@ -7,7 +6,7 @@ from rest_framework.serializers import ValidationError
from common.utils import get_logger
from orgs.mixins.api import OrgBulkModelViewSet
from ..models import Domain, Host
from ..models import Domain, Gateway
from .. import serializers
logger = get_logger(__file__)
@ -29,7 +28,7 @@ class DomainViewSet(OrgBulkModelViewSet):
class GatewayViewSet(OrgBulkModelViewSet):
perm_model = Host
perm_model = Gateway
filterset_fields = ("domain__name", "name", "domain")
search_fields = ("domain__name",)
serializer_class = serializers.GatewaySerializer

View File

@ -0,0 +1,43 @@
- hosts: mongodb
gather_facts: no
vars:
ansible_python_interpreter: /usr/local/bin/python
tasks:
- name: Test MongoDB connection
mongodb_ping:
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 }}"
register: db_info
- name: Display MongoDB version
debug:
var: db_info.server_version
when: db_info is succeeded
- name: Change MongoDB password
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 }}"
when: db_info is succeeded
register: change_info
- name: Verify password
mongodb_ping:
login_user: "{{ account.username }}"
login_password: "{{ account.secret }}"
login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}"
login_database: "{{ jms_asset.specific.db_name }}"
when:
- db_info is succeeded
- change_info is succeeded

View File

@ -0,0 +1,6 @@
id: change_secret_mongodb
name: Change password for MongoDB
category: database
type:
- mongodb
method: change_secret

View File

@ -0,0 +1,45 @@
- hosts: oracle
gather_facts: no
vars:
ansible_python_interpreter: /usr/local/bin/python
tasks:
- name: Test Oracle connection
oracle_ping:
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 }}"
register: db_info
- name: Display Oracle version
debug:
var: db_info.server_version
when: db_info is succeeded
- name: Change Oracle password
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 }}"
when: db_info is succeeded
register: change_info
- name: Verify password
oracle_ping:
login_user: "{{ account.username }}"
login_password: "{{ account.secret }}"
login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}"
login_database: "{{ jms_asset.specific.db_name }}"
mode: "{{ account.mode }}"
when:
- db_info is succeeded
- change_info is succeeded

View File

@ -0,0 +1,6 @@
id: change_secret_oracle
name: Change password for Oracle
category: database
type:
- oracle
method: change_secret

View File

@ -0,0 +1,47 @@
- hosts: sqlserver
gather_facts: no
vars:
ansible_python_interpreter: /usr/local/bin/python
tasks:
- name: Test SQLServer connection
community.general.mssql_script:
login_user: "{{ jms_account.username }}"
login_password: "{{ jms_account.secret }}"
login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}"
name: '{{ jms_asset.specific.db_name }}'
script: |
SELECT @@version
register: db_info
- name: SQLServer version
set_fact:
info:
version: "{{ db_info.query_results[0][0][0][0].splitlines()[0] }}"
- debug:
var: info
- name: Change SQLServer password
community.general.mssql_script:
login_user: "{{ jms_account.username }}"
login_password: "{{ jms_account.secret }}"
login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}"
name: '{{ jms_asset.specific.db_name }}'
script: "ALTER LOGIN {{ account.username }} WITH PASSWORD = '{{ account.secret }}'; select @@version"
when: db_info is succeeded
register: change_info
- name: Verify password
community.general.mssql_script:
login_user: "{{ account.username }}"
login_password: "{{ account.secret }}"
login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}"
name: '{{ jms_asset.specific.db_name }}'
script: |
SELECT @@version
when:
- db_info is succeeded
- change_info is succeeded

View File

@ -0,0 +1,6 @@
id: change_secret_sqlserver
name: Change password for SQLServer
category: database
type:
- sqlserver
method: change_secret

View File

@ -155,6 +155,8 @@ class ChangeSecretManager(BasePlaybookManager):
'secret': new_secret,
'private_key_path': private_key_path
}
if asset.platform.type == 'oracle':
h['account']['mode'] = 'sysdba' if account.privileged else None
inventory_hosts.append(h)
method_hosts.append(h['name'])
self.method_hosts_mapper[method_attr] = method_hosts

View File

@ -0,0 +1,22 @@
- hosts: mongodb
gather_facts: no
vars:
ansible_python_interpreter: /usr/local/bin/python
tasks:
- name: Get info
community.mongodb.mongodb_info:
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 }}"
filter: users
register: db_info
- name: Define info by set_fact
set_fact:
info: "{{ db_info.users }}"
- debug:
var: info

View File

@ -0,0 +1,6 @@
id: gather_accounts_mongodb
name: Gather account from MongoDB
category: database
type:
- mongodb
method: gather_accounts

View File

@ -9,7 +9,7 @@
login_user: "{{ jms_account.username }}"
login_password: "{{ jms_account.secret }}"
login_host: "{{ jms_asset.address }}"
login_port: 1234
login_port: "{{ jms_asset.port }}"
filter: users
register: db_info

View File

@ -0,0 +1,23 @@
- hosts: oralce
gather_facts: no
vars:
ansible_python_interpreter: /usr/local/bin/python
tasks:
- name: Get info
oracle_info:
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 }}"
filter: users
register: db_info
- name: Define info by set_fact
set_fact:
info: "{{ db_info.users }}"
- debug:
var: info

View File

@ -0,0 +1,6 @@
id: gather_accounts_oracle
name: Gather account from Oracle
category: database
type:
- oracle
method: gather_accounts

View File

@ -0,0 +1,22 @@
- hosts: mongodb
gather_facts: no
vars:
ansible_python_interpreter: /usr/local/bin/python
tasks:
- name: Get info
mongodb_ping:
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 }}"
register: db_info
- name: Define info by set_fact
set_fact:
info:
version: "{{ db_info.server_version }}"
- debug:
var: info

View File

@ -0,0 +1,6 @@
id: gather_facts_mongodb
name: Gather facts from MongoDB
category: database
type:
- mongodb
method: gather_facts

View File

@ -0,0 +1,23 @@
- hosts: oracle
gather_facts: no
vars:
ansible_python_interpreter: /usr/local/bin/python
tasks:
- name: Get info
oracle_ping:
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 }}"
register: db_info
- name: Define info by set_fact
set_fact:
info:
version: "{{ db_info.server_version }}"
- debug:
var: info

View File

@ -0,0 +1,6 @@
id: gather_facts_oracle
name: Gather facts from Oracle
category: database
type:
- oracle
method: gather_facts

View File

@ -0,0 +1,13 @@
- hosts: mongodb
gather_facts: no
vars:
ansible_python_interpreter: /usr/local/bin/python
tasks:
- name: Test MongoDB connection
mongodb_ping:
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 }}"

View File

@ -0,0 +1,6 @@
id: mongodb_ping
name: Ping MongoDB
category: database
type:
- mongodb
method: ping

View File

@ -0,0 +1,14 @@
- hosts: oracle
gather_facts: no
vars:
ansible_python_interpreter: /usr/local/bin/python
tasks:
- name: Test Oracle connection
oracle_ping:
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 }}"

View File

@ -0,0 +1,6 @@
id: oracle_ping
name: Ping Oracle
category: database
type:
- oracle
method: ping

View File

@ -0,0 +1,15 @@
- hosts: sqlserver
gather_facts: no
vars:
ansible_python_interpreter: /usr/local/bin/python
tasks:
- name: Test SQLServer connection
community.general.mssql_script:
login_user: "{{ jms_account.username }}"
login_password: "{{ jms_account.secret }}"
login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}"
name: '{{ jms_asset.specific.db_name }}'
script: |
SELECT @@version

View File

@ -0,0 +1,6 @@
id: sqlserver_ping
name: Ping SQLServer
category: database
type:
- sqlserver
method: ping

View File

@ -0,0 +1,16 @@
- 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

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

View File

@ -0,0 +1,16 @@
- 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

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

View File

@ -0,0 +1,13 @@
- hosts: mongdb
gather_facts: no
vars:
ansible_python_interpreter: /usr/local/bin/python
tasks:
- name: Verify account
mongodb_ping:
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 }}"

View File

@ -0,0 +1,6 @@
id: verify_account_mongodb
name: Verify account from MongoDB
category: database
type:
- mongodb
method: verify_account

View File

@ -0,0 +1,14 @@
- hosts: oracle
gather_facts: no
vars:
ansible_python_interpreter: /usr/local/bin/python
tasks:
- name: Verify account
oracle_ping:
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 }}"

View File

@ -0,0 +1,6 @@
id: verify_account_oracle
name: Verify account from Oracle
category: database
type:
- oracle
method: verify_account

View File

@ -0,0 +1,15 @@
- hosts: sqlserver
gather_facts: no
vars:
ansible_python_interpreter: /usr/local/bin/python
tasks:
- name: Verify account
community.general.mssql_script:
login_user: "{{ jms_account.username }}"
login_password: "{{ jms_account.secret }}"
login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}"
name: '{{ jms_asset.specific.db_name }}'
script: |
SELECT @@version

View File

@ -0,0 +1,6 @@
id: verify_account_sqlserver
name: Verify account from SQLServer
category: database
type:
- sqlserver
method: verify_account

View File

@ -86,8 +86,8 @@ class BaseAccount(JMSOrgBaseModel):
@lazyproperty
def public_key(self):
if self.secret_type == SecretType.SSH_KEY:
return parse_ssh_public_key_str(self.private_key)
if self.secret_type == SecretType.SSH_KEY and self.private_key:
return parse_ssh_public_key_str(private_key=self.private_key)
return None
@property

View File

@ -1,17 +1,13 @@
# -*- coding: utf-8 -*-
#
import re
import uuid
from django.db import models
from django.db.models import Q
from django.core.validators import MinValueValidator, MaxValueValidator
from django.db import models
from django.utils.translation import ugettext_lazy as _
from users.models import User, UserGroup
from common.utils import get_logger
from orgs.mixins.models import OrgModelMixin
from common.utils import lazyproperty, get_logger, get_object_or_none
from ..models import Asset, Account
logger = get_logger(__file__)
@ -93,125 +89,3 @@ class CommandFilterRule(OrgModelMixin):
class Meta:
ordering = ('priority', 'action')
verbose_name = _("Command filter rule")
@lazyproperty
def pattern(self):
if self.type == 'command':
s = self.construct_command_regex(content=self.content)
else:
s = r'{0}'.format(self.content)
return s
@classmethod
def construct_command_regex(cls, content):
regex = []
content = content.replace('\r\n', '\n')
for _cmd in content.split('\n'):
cmd = re.sub(r'\s+', ' ', _cmd)
cmd = re.escape(cmd)
cmd = cmd.replace('\\ ', '\s+')
# 有空格就不能 铆钉单词了
if ' ' in _cmd:
regex.append(cmd)
continue
if not cmd:
continue
# 如果是单个字符
if cmd[-1].isalpha():
regex.append(r'\b{0}\b'.format(cmd))
else:
regex.append(r'\b{0}'.format(cmd))
s = r'{}'.format('|'.join(regex))
return s
@staticmethod
def compile_regex(regex, ignore_case):
try:
if ignore_case:
pattern = re.compile(regex, re.IGNORECASE)
else:
pattern = re.compile(regex)
except Exception as e:
error = _('The generated regular expression is incorrect: {}').format(str(e))
logger.error(error)
return False, error, None
return True, '', pattern
def match(self, data):
succeed, error, pattern = self.compile_regex(self.pattern, self.ignore_case)
if not succeed:
return self.ACTION_UNKNOWN, ''
found = pattern.search(data)
if not found:
return self.ACTION_UNKNOWN, ''
if self.action == self.ActionChoices.allow:
return self.ActionChoices.allow, found.group()
else:
return self.ActionChoices.deny, found.group()
def __str__(self):
return '{} % {}'.format(self.type, self.content)
def create_command_confirm_ticket(self, run_command, session, cmd_filter_rule, org_id):
from tickets.const import TicketType
from tickets.models import ApplyCommandTicket
data = {
'title': _('Command confirm') + ' ({})'.format(session.user),
'type': TicketType.command_confirm,
'applicant': session.user_obj,
'apply_run_user_id': session.user_id,
'apply_run_asset': str(session.asset),
'apply_run_account': str(session.account),
'apply_run_command': run_command[:4090],
'apply_from_session_id': str(session.id),
'apply_from_cmd_filter_rule_id': str(cmd_filter_rule.id),
'apply_from_cmd_filter_id': str(cmd_filter_rule.filter.id),
'org_id': org_id,
}
ticket = ApplyCommandTicket.objects.create(**data)
assignees = self.reviewers.all()
ticket.open_by_system(assignees)
return ticket
@classmethod
def get_queryset(
cls, user_id=None, user_group_id=None, account=None,
asset_id=None, org_id=None
):
from assets.models import Account
user_groups = []
user = get_object_or_none(User, pk=user_id)
if user:
user_groups.extend(list(user.groups.all()))
user_group = get_object_or_none(UserGroup, pk=user_group_id)
if user_group:
org_id = user_group.org_id
user_groups.append(user_group)
asset = get_object_or_none(Asset, pk=asset_id)
q = Q()
if user:
q |= Q(users=user)
if user_groups:
q |= Q(user_groups__in=set(user_groups))
if account:
org_id = account.org_id
q |= Q(accounts__contains=account.username) | \
Q(accounts__contains=Account.AliasAccount.ALL)
if asset:
org_id = asset.org_id
q |= Q(assets=asset)
if q:
cmd_filters = CommandFilter.objects.filter(q).filter(is_active=True)
if org_id:
cmd_filters = cmd_filters.filter(org_id=org_id)
rule_ids = cmd_filters.values_list('rules', flat=True)
rules = cls.objects.filter(id__in=rule_ids)
else:
rules = cls.objects.none()
return rules

View File

@ -72,7 +72,9 @@ class AccountSerializer(AccountSerializerCreateMixin, BaseAccountSerializer):
def __init__(self, *args, data=None, **kwargs):
super().__init__(*args, data=data, **kwargs)
if data and 'name' not in data:
data['name'] = data.get('username')
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')

View File

@ -28,7 +28,7 @@ class AuthValidateMixin(serializers.Serializer):
def validate_secret(self, secret):
if not secret:
return
return ''
secret_type = self.initial_secret_type
if secret_type == SecretType.PASSWORD:
validate_password_for_ansible(secret)
@ -44,7 +44,7 @@ class AuthValidateMixin(serializers.Serializer):
def clean_auth_fields(validated_data):
for field in ('secret',):
value = validated_data.get(field)
if not value:
if value is None:
validated_data.pop(field, None)
validated_data.pop('passphrase', None)

View File

@ -1,16 +1,13 @@
# -*- coding: utf-8 -*-
#
from rest_framework import serializers
from rest_framework.generics import get_object_or_404
from django.utils.translation import ugettext_lazy as _
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from common.drf.serializers import SecretReadableMixin, WritableNestedModelSerializer
from common.drf.fields import ObjectRelatedField, EncryptedField
from assets.const import SecretType, GATEWAY_NAME
from ..serializers import AssetProtocolsSerializer
from ..models import Platform, Domain, Node, Asset, Account, Host
from .utils import validate_password_for_ansible, validate_ssh_key
from common.drf.serializers import SecretReadableMixin
from common.drf.fields import ObjectRelatedField
from ..serializers import HostSerializer
from ..models import Domain, Gateway, Asset
class DomainSerializer(BulkOrgResourceModelSerializer):
@ -41,100 +38,9 @@ class DomainSerializer(BulkOrgResourceModelSerializer):
return obj.gateways.count()
class GatewaySerializer(BulkOrgResourceModelSerializer, WritableNestedModelSerializer):
password = EncryptedField(
label=_('Password'), required=False, allow_blank=True, allow_null=True, max_length=1024,
validators=[validate_password_for_ansible], write_only=True
)
private_key = EncryptedField(
label=_('SSH private key'), required=False, allow_blank=True, allow_null=True,
max_length=16384, write_only=True
)
passphrase = serializers.CharField(
label=_('Key password'), allow_blank=True, allow_null=True, required=False, write_only=True,
max_length=512,
)
username = serializers.CharField(
label=_('Username'), allow_blank=True, max_length=128, required=True, write_only=True
)
username_display = serializers.SerializerMethodField(label=_('Username'))
protocols = AssetProtocolsSerializer(many=True, required=False, label=_('Protocols'))
class Meta:
model = Host
fields_mini = ['id', 'name', 'address']
fields_small = fields_mini + ['is_active', 'comment']
fields = fields_small + ['domain', 'protocols'] + [
'username', 'password', 'private_key', 'passphrase', 'username_display'
]
extra_kwargs = {
'name': {'label': _("Name")},
'address': {'label': _('Address')},
}
@staticmethod
def get_username_display(obj):
account = obj.accounts.order_by('-privileged').first()
return account.username if account else ''
def validate_private_key(self, secret):
if not secret:
return
passphrase = self.initial_data.get('passphrase')
passphrase = passphrase if passphrase else None
validate_ssh_key(secret, passphrase)
return secret
@staticmethod
def clean_auth_fields(validated_data):
username = validated_data.pop('username', None)
password = validated_data.pop('password', None)
private_key = validated_data.pop('private_key', None)
validated_data.pop('passphrase', None)
return username, password, private_key
@staticmethod
def create_accounts(instance, username, password, private_key):
account_name = f'{instance.name}-{_("Gateway")}'
account_data = {
'privileged': True,
'name': account_name,
'username': username,
'asset_id': instance.id,
'created_by': instance.created_by
}
if password:
Account.objects.create(
**account_data, secret=password, secret_type=SecretType.PASSWORD
)
if private_key:
Account.objects.create(
**account_data, secret=private_key, secret_type=SecretType.SSH_KEY
)
@staticmethod
def update_accounts(instance, username, password, private_key):
accounts = instance.accounts.filter(username=username)
if password:
account = get_object_or_404(accounts, SecretType.PASSWORD)
account.secret = password
account.save()
if private_key:
account = get_object_or_404(accounts, SecretType.SSH_KEY)
account.secret = private_key
account.save()
def create(self, validated_data):
auth_fields = self.clean_auth_fields(validated_data)
instance = super().create(validated_data)
self.create_accounts(instance, *auth_fields)
return instance
def update(self, instance, validated_data):
auth_fields = self.clean_auth_fields(validated_data)
instance = super().update(instance, validated_data)
self.update_accounts(instance, *auth_fields)
return instance
class GatewaySerializer(HostSerializer):
class Meta(HostSerializer.Meta):
model = Gateway
class GatewayWithAuthSerializer(SecretReadableMixin, GatewaySerializer):

View File

@ -1,25 +1,25 @@
# -*- coding: utf-8 -*-
#
import inspect
from functools import partial
import time
from functools import partial
from typing import Callable
from django.utils.http import urlencode
from django.core.cache import cache
from django.conf import settings
from django.contrib import auth
from django.utils.translation import ugettext as _
from rest_framework.request import Request
from django.contrib.auth import (
BACKEND_SESSION_KEY, load_backend,
PermissionDenied, user_login_failed, _clean_credentials,
)
from django.core.cache import cache
from django.core.exceptions import ImproperlyConfigured
from django.shortcuts import reverse, redirect, get_object_or_404
from django.utils.http import urlencode
from django.utils.translation import ugettext as _
from rest_framework.request import Request
from common.utils import get_request_ip, get_logger, bulk_get, FlashMessageUtil
from acls.models import LoginACL
from common.utils import get_request_ip, get_logger, bulk_get, FlashMessageUtil
from users.models import User
from users.utils import LoginBlockUtil, MFABlockUtils, LoginIpBlockUtil
from . import errors

View File

@ -1,3 +1,4 @@
import multiprocessing
from django.core.management.base import BaseCommand, CommandError
from django.db.models import TextChoices
from .utils import ServicesUtil
@ -91,11 +92,15 @@ class BaseActionCommand(BaseCommand):
super().__init__(*args, **kwargs)
def add_arguments(self, parser):
cores = 10
if (multiprocessing.cpu_count() * 2 + 1) < cores:
cores = multiprocessing.cpu_count() * 2 + 1
parser.add_argument(
'services', nargs='+', choices=Services.export_services_values(), help='Service',
)
parser.add_argument('-d', '--daemon', nargs="?", const=True)
parser.add_argument('-w', '--worker', type=int, nargs="?", default=4)
parser.add_argument('-w', '--worker', type=int, nargs="?", default=cores)
parser.add_argument('-f', '--force', nargs="?", const=True)
def initial_util(self, *args, **options):

View File

@ -16,11 +16,11 @@ class GunicornService(BaseService):
log_format = '%(h)s %(t)s %(L)ss "%(r)s" %(s)s %(b)s '
bind = f'{HTTP_HOST}:{HTTP_PORT}'
cmd = [
'gunicorn', 'jumpserver.asgi:application',
'-b', bind,
'-k', 'uvicorn.workers.UvicornWorker',
'--threads', '10',
'-w', str(self.worker),
'--max-requests', '4096',
'--access-logformat', log_format,

View File

@ -112,6 +112,9 @@ class JMSInventory:
'secret': account.secret, 'secret_type': account.secret_type
} if account else None
}
if host['jms_account'] and asset.platform.type == 'oracle':
host['jms_account']['mode'] = 'sysdba' if account.privileged else None
ansible_config = dict(automation.ansible_config)
ansible_connection = ansible_config.get('ansible_connection', 'ssh')
host.update(ansible_config)

View File

View File

@ -0,0 +1,126 @@
#!/usr/bin/python
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = '''
---
module: mongodb_ping
short_description: Check remote MongoDB server availability
description:
- Simple module to check remote MongoDB server availability.
requirements:
- "pymongo"
'''
EXAMPLES = '''
- name: >
Ping MongoDB server using non-default credentials and SSL
registering the return values into the result variable for future use
mongodb_ping:
login_db: test_db
login_host: jumpserver
login_user: jms
login_password: secret_pass
ssl: True
ssl_ca_certs: "/tmp/ca.crt"
ssl_certfile: "/tmp/tls.key" #cert and key in one file
connection_options:
- "tlsAllowInvalidHostnames=true"
'''
RETURN = '''
is_available:
description: MongoDB server availability.
returned: always
type: bool
sample: true
server_version:
description: MongoDB server version.
returned: always
type: str
sample: '4.0.0'
conn_err_msg:
description: Connection error message.
returned: always
type: str
sample: ''
'''
from pymongo.errors import PyMongoError
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils._text import to_native
from ansible_collections.community.mongodb.plugins.module_utils.mongodb_common import (
mongodb_common_argument_spec,
mongo_auth,
get_mongodb_client,
)
class MongoDBPing(object):
def __init__(self, module, client):
self.module = module
self.client = client
self.is_available = False
self.conn_err_msg = ''
self.version = ''
def do(self):
self.get_mongodb_version()
return self.is_available, self.version
def get_err(self):
return self.conn_err_msg
def get_mongodb_version(self):
try:
server_info = self.client.server_info()
self.is_available = True
self.version = server_info.get('version', '')
except PyMongoError as err:
self.is_available = False
self.version = ''
self.conn_err_msg = err
# =========================================
# Module execution.
#
def main():
argument_spec = mongodb_common_argument_spec()
module = AnsibleModule(
argument_spec=argument_spec,
supports_check_mode=True,
)
client = None
result = {
'changed': False, 'is_available': False, 'server_version': ''
}
try:
client = get_mongodb_client(module, directConnection=True)
client = mongo_auth(module, client, directConnection=True)
except Exception as e:
module.fail_json(msg='Unable to connect to database: %s' % to_native(e))
mongodb_ping = MongoDBPing(module, client)
result["is_available"], result["server_version"] = mongodb_ping.do()
conn_err_msg = mongodb_ping.get_err()
if conn_err_msg:
module.fail_json(msg='Unable to connect to database: %s' % conn_err_msg)
try:
client.close()
except Exception:
pass
return module.exit_json(**result)
if __name__ == '__main__':
main()

View File

@ -0,0 +1,426 @@
#!/usr/bin/python
# Modified from ansible_collections.community.mongodb.plugins.modules.mongodb_user
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = '''
---
module: mongodb_user
short_description: Adds or removes a user from a MongoDB database
description:
- Adds or removes a user from a MongoDB database.
version_added: "1.0.0"
extends_documentation_fragment:
- community.mongodb.login_options
- community.mongodb.ssl_options
options:
replica_set:
description:
- Replica set to connect to (automatically connects to primary for writes).
type: str
database:
description:
- The name of the database to add/remove the user from.
required: true
type: str
aliases: [db]
name:
description:
- The name of the user to add or remove.
required: true
aliases: [user]
type: str
password:
description:
- The password to use for the user.
type: str
aliases: [pass]
roles:
type: list
elements: raw
description:
- >
The database user roles valid values could either be one or more of the following strings:
'read', 'readWrite', 'dbAdmin', 'userAdmin', 'clusterAdmin', 'readAnyDatabase', 'readWriteAnyDatabase', 'userAdminAnyDatabase',
'dbAdminAnyDatabase'
- "Or the following dictionary '{ db: DATABASE_NAME, role: ROLE_NAME }'."
- "This param requires pymongo 2.5+. If it is a string, mongodb 2.4+ is also required. If it is a dictionary, mongo 2.6+ is required."
state:
description:
- The database user state.
default: present
choices: [absent, present]
type: str
update_password:
default: always
choices: [always, on_create]
description:
- C(always) will always update passwords and cause the module to return changed.
- C(on_create) will only set the password for newly created users.
- This must be C(always) to use the localhost exception when adding the first admin user.
- This option is effectively ignored when using x.509 certs. It is defaulted to 'on_create' to maintain a \
a specific module behaviour when the login_database is '$external'.
type: str
create_for_localhost_exception:
type: path
description:
- This is parmeter is only useful for handling special treatment around the localhost exception.
- If C(login_user) is defined, then the localhost exception is not active and this parameter has no effect.
- If this file is NOT present (and C(login_user) is not defined), then touch this file after successfully adding the user.
- If this file is present (and C(login_user) is not defined), then skip this task.
notes:
- Requires the pymongo Python package on the remote host, version 2.4.2+. This
can be installed using pip or the OS package manager. Newer mongo server versions require newer
pymongo versions. @see http://api.mongodb.org/python/current/installation.html
requirements:
- "pymongo"
author:
- "Elliott Foster (@elliotttf)"
- "Julien Thebault (@Lujeni)"
'''
EXAMPLES = '''
- name: Create 'burgers' database user with name 'bob' and password '12345'.
community.mongodb.mongodb_user:
database: burgers
name: bob
password: 12345
state: present
- name: Create a database user via SSL (MongoDB must be compiled with the SSL option and configured properly)
community.mongodb.mongodb_user:
database: burgers
name: bob
password: 12345
state: present
ssl: True
- name: Delete 'burgers' database user with name 'bob'.
community.mongodb.mongodb_user:
database: burgers
name: bob
state: absent
- name: Define more users with various specific roles (if not defined, no roles is assigned, and the user will be added via pre mongo 2.2 style)
community.mongodb.mongodb_user:
database: burgers
name: ben
password: 12345
roles: read
state: present
- name: Define roles
community.mongodb.mongodb_user:
database: burgers
name: jim
password: 12345
roles: readWrite,dbAdmin,userAdmin
state: present
- name: Define roles
community.mongodb.mongodb_user:
database: burgers
name: joe
password: 12345
roles: readWriteAnyDatabase
state: present
- name: Add a user to database in a replica set, the primary server is automatically discovered and written to
community.mongodb.mongodb_user:
database: burgers
name: bob
replica_set: belcher
password: 12345
roles: readWriteAnyDatabase
state: present
# add a user 'oplog_reader' with read only access to the 'local' database on the replica_set 'belcher'. This is useful for oplog access (MONGO_OPLOG_URL).
# please notice the credentials must be added to the 'admin' database because the 'local' database is not synchronized and can't receive user credentials
# To login with such user, the connection string should be MONGO_OPLOG_URL="mongodb://oplog_reader:oplog_reader_password@server1,server2/local?authSource=admin"
# This syntax requires mongodb 2.6+ and pymongo 2.5+
- name: Roles as a dictionary
community.mongodb.mongodb_user:
login_user: root
login_password: root_password
database: admin
user: oplog_reader
password: oplog_reader_password
state: present
replica_set: belcher
roles:
- db: local
role: read
- name: Adding a user with X.509 Member Authentication
community.mongodb.mongodb_user:
login_host: "mongodb-host.test"
login_port: 27001
login_database: "$external"
database: "admin"
name: "admin"
password: "test"
roles:
- dbAdminAnyDatabase
ssl: true
ssl_ca_certs: "/tmp/ca.crt"
ssl_certfile: "/tmp/tls.key" #cert and key in one file
state: present
auth_mechanism: "MONGODB-X509"
connection_options:
- "tlsAllowInvalidHostnames=true"
'''
RETURN = '''
user:
description: The name of the user to add or remove.
returned: success
type: str
'''
import os
import traceback
from operator import itemgetter
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.six import binary_type, text_type
from ansible.module_utils._text import to_native, to_bytes
from ansible_collections.community.mongodb.plugins.module_utils.mongodb_common import (
missing_required_lib,
mongodb_common_argument_spec,
mongo_auth,
PYMONGO_IMP_ERR,
pymongo_found,
get_mongodb_client,
)
def user_find(client, user, db_name):
"""Check if the user exists.
Args:
client (cursor): Mongodb cursor on admin database.
user (str): User to check.
db_name (str): User's database.
Returns:
dict: when user exists, False otherwise.
"""
try:
for mongo_user in client[db_name].command('usersInfo')['users']:
if mongo_user['user'] == user:
# NOTE: there is no 'db' field in mongo 2.4.
if 'db' not in mongo_user:
return mongo_user
# Workaround to make the condition works with AWS DocumentDB,
# since all users are in the admin database.
if mongo_user["db"] in [db_name, "admin"]:
return mongo_user
except Exception as excep:
if hasattr(excep, 'code') and excep.code == 11: # 11=UserNotFound
pass # Allow return False
else:
raise
return False
def user_add(module, client, db_name, user, password, roles):
# pymongo's user_add is a _create_or_update_user so we won't know if it was changed or updated
# without reproducing a lot of the logic in database.py of pymongo
db = client[db_name]
try:
exists = user_find(client, user, db_name)
except Exception as excep:
# We get this exception: "not authorized on admin to execute command"
# when auth is enabled on a new instance. The loalhost exception should
# allow us to create the first user. If the localhost exception does not apply,
# then user creation will also fail with unauthorized. So, ignore Unauthorized here.
if hasattr(excep, 'code') and excep.code == 13: # 13=Unauthorized
exists = False
else:
raise
if exists:
user_add_db_command = 'updateUser'
if not roles:
roles = None
else:
user_add_db_command = 'createUser'
user_dict = {}
if password is not None:
user_dict["pwd"] = password
if roles is not None:
user_dict["roles"] = roles
db.command(user_add_db_command, user, **user_dict)
def user_remove(module, client, db_name, user):
exists = user_find(client, user, db_name)
if exists:
if module.check_mode:
module.exit_json(changed=True, user=user)
db = client[db_name]
db.command("dropUser", user)
else:
module.exit_json(changed=False, user=user)
def check_if_roles_changed(uinfo, roles, db_name):
# We must be aware of users which can read the oplog on a replicaset
# Such users must have access to the local DB, but since this DB does not store users credentials
# and is not synchronized among replica sets, the user must be stored on the admin db
# Therefore their structure is the following :
# {
# "_id" : "admin.oplog_reader",
# "user" : "oplog_reader",
# "db" : "admin", # <-- admin DB
# "roles" : [
# {
# "role" : "read",
# "db" : "local" # <-- local DB
# }
# ]
# }
def make_sure_roles_are_a_list_of_dict(roles, db_name):
output = list()
for role in roles:
if isinstance(role, (binary_type, text_type)):
new_role = {"role": role, "db": db_name}
output.append(new_role)
else:
output.append(role)
return output
roles_as_list_of_dict = make_sure_roles_are_a_list_of_dict(roles, db_name)
uinfo_roles = uinfo.get('roles', [])
if sorted(roles_as_list_of_dict, key=itemgetter('db')) == sorted(uinfo_roles, key=itemgetter('db')):
return False
return True
# =========================================
# Module execution.
#
def main():
argument_spec = mongodb_common_argument_spec()
argument_spec.update(
database=dict(required=True, aliases=['db']),
name=dict(required=True, aliases=['user']),
password=dict(aliases=['pass'], no_log=True),
replica_set=dict(default=None),
roles=dict(default=None, type='list', elements='raw'),
state=dict(default='present', choices=['absent', 'present']),
update_password=dict(default="always", choices=["always", "on_create"], no_log=False),
create_for_localhost_exception=dict(default=None, type='path'),
)
module = AnsibleModule(
argument_spec=argument_spec,
supports_check_mode=True,
)
login_user = module.params['login_user']
# Certs don't have a password but we want this module behaviour
if module.params['login_database'] == '$external':
module.params['update_password'] = 'on_create'
if not pymongo_found:
module.fail_json(msg=missing_required_lib('pymongo'),
exception=PYMONGO_IMP_ERR)
create_for_localhost_exception = module.params['create_for_localhost_exception']
b_create_for_localhost_exception = (
to_bytes(create_for_localhost_exception, errors='surrogate_or_strict')
if create_for_localhost_exception is not None else None
)
db_name = module.params['database']
user = module.params['name']
password = module.params['password']
roles = module.params['roles'] or []
state = module.params['state']
update_password = module.params['update_password']
try:
directConnection = False
if module.params['replica_set'] is None:
directConnection = True
client = get_mongodb_client(module, directConnection=directConnection)
client = mongo_auth(module, client, directConnection=directConnection)
except Exception as e:
module.fail_json(msg='Unable to connect to database: %s' % to_native(e))
if state == 'present':
if password is None and update_password == 'always':
module.fail_json(msg='password parameter required when adding a user unless update_password is set to on_create')
if login_user is None and create_for_localhost_exception is not None:
if os.path.exists(b_create_for_localhost_exception):
try:
client.close()
except Exception:
pass
module.exit_json(changed=False, user=user, skipped=True, msg="The path in create_for_localhost_exception exists.")
try:
if update_password != 'always':
uinfo = user_find(client, user, db_name)
if uinfo:
password = None
if not check_if_roles_changed(uinfo, roles, db_name):
module.exit_json(changed=False, user=user)
if module.check_mode:
module.exit_json(changed=True, user=user)
user_add(module, client, db_name, user, password, roles)
except Exception as e:
module.fail_json(msg='Unable to add or update user: %s' % to_native(e), exception=traceback.format_exc())
finally:
try:
client.close()
except Exception:
pass
# Here we can check password change if mongo provide a query for that : https://jira.mongodb.org/browse/SERVER-22848
# newuinfo = user_find(client, user, db_name)
# if uinfo['role'] == newuinfo['role'] and CheckPasswordHere:
# module.exit_json(changed=False, user=user)
if login_user is None and create_for_localhost_exception is not None:
# localhost exception applied.
try:
# touch the file
open(b_create_for_localhost_exception, 'wb').close()
except Exception as e:
module.fail_json(
changed=True,
msg='Added user but unable to touch create_for_localhost_exception file %s: %s' % (create_for_localhost_exception, to_native(e)),
exception=traceback.format_exc()
)
elif state == 'absent':
try:
user_remove(module, client, db_name, user)
except Exception as e:
module.fail_json(msg='Unable to remove user: %s' % to_native(e), exception=traceback.format_exc())
finally:
try:
client.close()
except Exception:
pass
module.exit_json(changed=True, user=user)
if __name__ == '__main__':
main()

View File

@ -0,0 +1,261 @@
#!/usr/bin/python
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = r'''
---
module: oracle_info
short_description: Gather information about Oracle servers
description:
- Gathers information about Oracle servers.
options:
filter:
description:
- Limit the collected information by comma separated string or YAML list.
- Allowable values are C(version), C(databases), C(settings), C(users).
- By default, collects all subsets.
- You can use '!' before value (for example, C(!users)) to exclude it from the information.
- If you pass including and excluding values to the filter, for example, I(filter=!settings,version),
the excluding values, C(!settings) in this case, will be ignored.
type: list
elements: str
login_db:
description:
- Database name to connect to.
- It makes sense if I(login_user) is allowed to connect to a specific database only.
type: str
exclude_fields:
description:
- List of fields which are not needed to collect.
- "Supports elements: C(db_size). Unsupported elements will be ignored."
type: list
elements: str
'''
EXAMPLES = r'''
- name: Get Oracle version with non-default credentials
oracle_info:
login_user: mysuperuser
login_password: mysuperpass
login_database: service_name
filter: version
- name: Collect all info except settings and users by sys
oracle_info:
login_user: sys
login_password: sys_pass
login_database: service_name
filter: "!settings,!users"
exclude_fields: db_size
'''
RETURN = r'''
version:
description: Database server version.
returned: if not excluded by filter
type: dict
sample: { "version": {"full": "11.2.0.1.0"} }
contains:
full:
description: Full server version.
returned: if not excluded by filter
type: str
sample: "11.2.0.1.0"
databases:
description: Information about databases.
returned: if not excluded by filter
type: dict
sample:
- { "USERS": { "size": 5242880 }, "EXAMPLE": { "size": 104857600 } }
contains:
size:
description: Database size in bytes.
returned: if not excluded by filter
type: dict
sample: { 'size': 656594 }
settings:
description: Global settings (variables) information.
returned: if not excluded by filter
type: dict
sample:
- { "result_cache_mode": "MANUAL", "instance_type": "RDBMS" }
users:
description: Users information.
returned: if not excluded by filter
type: dict
sample:
- { "USERS": { "TEST": { "USERNAME": "TEST", "ACCOUNT_STATUS": "OPEN" } } }
'''
from ansible.module_utils.basic import AnsibleModule
from ops.ansible.modules_utils.oracle_common import (
OracleClient, oracle_common_argument_spec
)
class OracleInfo(object):
def __init__(self, module, oracle_client):
self.module = module
self.oracle_client = oracle_client
self.info = {
'version': {}, 'databases': {},
'settings': {}, 'users': {},
}
def get_info(self, filter_, exclude_fields):
include_list = []
exclude_list = []
if filter_:
partial_info = {}
for fi in filter_:
if fi.lstrip('!') not in self.info:
self.module.warn('filter element: %s is not allowable, ignored' % fi)
continue
if fi[0] == '!':
exclude_list.append(fi.lstrip('!'))
else:
include_list.append(fi)
if include_list:
self.__collect(exclude_fields, set(include_list))
for i in self.info:
if i in include_list:
partial_info[i] = self.info[i]
else:
not_in_exclude_list = list(set(self.info) - set(exclude_list))
self.__collect(exclude_fields, set(not_in_exclude_list))
for i in self.info:
if i not in exclude_list:
partial_info[i] = self.info[i]
return partial_info
else:
self.__collect(exclude_fields, set(self.info))
return self.info
def __collect(self, exclude_fields, wanted):
"""Collect all possible subsets."""
if 'version' in wanted:
self.__get_version()
if 'settings' in wanted:
self.__get_settings()
if 'databases' in wanted:
self.__get_databases(exclude_fields)
#
if 'users' in wanted:
self.__get_users()
def __get_version(self):
version_sql = 'SELECT VERSION FROM PRODUCT_COMPONENT_VERSION where ROWNUM=1'
rtn, err = self.oracle_client.execute(version_sql, exception_to_fail=True)
self.info['version'] = {'full': rtn.get('version')}
def __get_settings(self):
"""Get global variables (instance settings)."""
def _set_settings_value(item_dict):
try:
self.info['settings'][item_dict['name']] = item_dict['value']
except KeyError:
pass
settings_sql = "SELECT name, value FROM V$PARAMETER"
rtn, err = self.oracle_client.execute(settings_sql, exception_to_fail=True)
if isinstance(rtn, dict):
_set_settings_value(rtn)
elif isinstance(rtn, list):
for i in rtn:
_set_settings_value(i)
def __get_users(self):
"""Get user info."""
def _set_users_value(item_dict):
try:
tablespace = item_dict.pop('default_tablespace')
username = item_dict.pop('username')
partial_users = self.info['users'].get(tablespace, {})
partial_users[username] = item_dict
self.info['users'][tablespace] = partial_users
except KeyError:
pass
users_sql = "SELECT * FROM dba_users"
rtn, err = self.oracle_client.execute(users_sql, exception_to_fail=True)
if isinstance(rtn, dict):
_set_users_value(rtn)
elif isinstance(rtn, list):
for i in rtn:
_set_users_value(i)
def __get_databases(self, exclude_fields):
"""Get info about databases."""
def _set_databases_value(item_dict):
try:
tablespace_name = item_dict.pop('tablespace_name')
size = item_dict.get('size')
partial_params = {}
if size:
partial_params['size'] = size
self.info['databases'][tablespace_name] = partial_params
except KeyError:
pass
database_sql = 'SELECT ' \
' tablespace_name, sum(bytes) as "size"' \
'FROM dba_data_files GROUP BY tablespace_name'
if exclude_fields and 'db_size' in exclude_fields:
database_sql = "SELECT " \
" tablespace_name " \
"FROM dba_data_files GROUP BY tablespace_name"
rtn, err = self.oracle_client.execute(database_sql, exception_to_fail=True)
if isinstance(rtn, dict):
_set_databases_value(rtn)
elif isinstance(rtn, list):
for i in rtn:
_set_databases_value(i)
# ===========================================
# Module execution.
#
def main():
argument_spec = oracle_common_argument_spec()
argument_spec.update(
filter=dict(type='list'),
exclude_fields=dict(type='list'),
)
module = AnsibleModule(
argument_spec=argument_spec,
supports_check_mode=True,
)
filter_ = module.params['filter']
exclude_fields = module.params['exclude_fields']
if filter_:
filter_ = [f.strip() for f in filter_]
if exclude_fields:
exclude_fields = set([f.strip() for f in exclude_fields])
oracle_client = OracleClient(module)
oracle = OracleInfo(module, oracle_client)
module.exit_json(changed=False, **oracle.get_info(filter_, exclude_fields))
if __name__ == '__main__':
main()

View File

@ -0,0 +1,107 @@
#!/usr/bin/python
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = '''
---
module: oracle_ping
short_description: Check remote Oracle server availability
description:
- Simple module to check remote Oracle server availability.
requirements:
- "oracledb"
'''
EXAMPLES = '''
- name: >
Ping Oracle server using non-default credentials and SSL
registering the return values into the result variable for future use
oracle_ping:
login_host: jumpserver
login_port: 1521
login_user: jms
login_password: secret_pass
login_database: test_db
'''
RETURN = '''
is_available:
description: Oracle server availability.
returned: always
type: bool
sample: true
server_version:
description: Oracle server version.
returned: always
type: str
sample: '4.0.0'
conn_err_msg:
description: Connection error message.
returned: always
type: str
sample: ''
'''
from ansible.module_utils.basic import AnsibleModule
from ops.ansible.modules_utils.oracle_common import (
OracleClient, oracle_common_argument_spec
)
class OracleDBPing(object):
def __init__(self, module, oracle_client):
self.module = module
self.oracle_client = oracle_client
self.is_available = False
self.conn_err_msg = ''
self.version = ''
def do(self):
self.get_oracle_version()
return self.is_available, self.version
def get_err(self):
return self.conn_err_msg
def get_oracle_version(self):
version_sql = 'SELECT VERSION FROM PRODUCT_COMPONENT_VERSION where ROWNUM=1'
rtn, err = self.oracle_client.execute(version_sql)
if err:
self.conn_err_msg = err
else:
self.version = rtn.get('version')
self.is_available = True
# =========================================
# Module execution.
#
def main():
argument_spec = oracle_common_argument_spec()
module = AnsibleModule(
argument_spec=argument_spec,
supports_check_mode=True,
)
result = {
'changed': False, 'is_available': False, 'server_version': ''
}
oracle_client = OracleClient(module)
oracle_ping = OracleDBPing(module, oracle_client)
result["is_available"], result["server_version"] = oracle_ping.do()
conn_err_msg = oracle_ping.get_err()
oracle_client.close()
if conn_err_msg:
module.fail_json(msg='Unable to connect to database: %s' % conn_err_msg)
return module.exit_json(**result)
if __name__ == '__main__':
main()

View File

@ -0,0 +1,215 @@
#!/usr/bin/python
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = '''
---
module: oracle_user
short_description: Adds or removes a user from a Oracle database
description:
- Adds or removes a user from a Oracle database.
options:
authentication_type:
description:
- Authentication type of the user(default password)
required: false
type: str
choices: ['external', 'global', 'no_authentication', 'password']
default_tablespace:
description:
- The default tablespace for the user
- If not provided, the default is used
required: false
type: str
oracle_home:
description:
- Define the directory into which all Oracle software is installed.
- Define ORACLE_HOME environment variable if set.
type: str
state:
description:
- The database user state.
default: present
choices: [absent, present]
type: str
update_password:
default: always
choices: [always, on_create]
description:
- C(always) will always update passwords and cause the module to return changed.
- C(on_create) will only set the password for newly created users.
type: str
temporary_tablespace:
description:
- The default temporary tablespace for the user
- If not provided, the default is used
required: false
type: str
name:
description:
- The name of the user to add or remove.
required: true
aliases: [user]
type: str
password:
description:
- The password to use for the user.
type: str
aliases: [pass]
requirements:
- "oracledb"
'''
EXAMPLES = '''
- name: Create default tablespace user with name 'jms' and password '123456'.
oracle_user:
hostname: "remote server"
login_database: "helowin"
login_username: "system"
login_password: "123456"
name: "jms"
password: "123456"
- name: Delete user with name 'jms'.
oracle_user:
hostname: "remote server"
login_database: "helowin"
login_username: "system"
login_password: "123456"
name: "jms"
state: "absent"
'''
RETURN = '''
name:
description: The name of the user to add or remove.
returned: success
type: str
'''
from ansible.module_utils.basic import AnsibleModule
from ops.ansible.modules_utils.oracle_common import (
OracleClient, oracle_common_argument_spec
)
def user_find(oracle_client, username):
user = None
username = username.upper()
user_find_sql = "select username, " \
" authentication_type, " \
" default_tablespace, " \
" temporary_tablespace " \
"from dba_users where username='%s'" % username
rtn, err = oracle_client.execute(user_find_sql)
if isinstance(rtn, dict):
user = rtn
return user
def user_add(
module, oracle_client, username, password, auth_type,
default_tablespace, temporary_tablespace
):
username = username.upper()
extend_sql = None
user = user_find(oracle_client, username)
auth_type = auth_type.lower()
identified_suffix_map = {
'external': 'identified externally ',
'global': 'identified globally ',
'password': 'identified by "%s" ',
}
if user:
user_sql = "alter user %s " % username
user_sql += identified_suffix_map.get(auth_type, 'no authentication ') % password
if default_tablespace and default_tablespace.lower() != user['default_tablespace'].lower():
user_sql += 'default tablespace %s quota unlimited on %s ' % (default_tablespace, default_tablespace)
if temporary_tablespace and temporary_tablespace.lower() != user['temporary_tablespace'].lower():
user_sql += 'temporary tablespace %s ' % temporary_tablespace
else:
user_sql = "create user %s " % username
user_sql += identified_suffix_map.get(auth_type, 'no authentication ') % password
if default_tablespace:
user_sql += 'default tablespace %s quota unlimited on %s ' % (default_tablespace, default_tablespace)
if temporary_tablespace:
user_sql += 'temporary tablespace %s ' % temporary_tablespace
extend_sql = 'grant connect to %s' % username
rtn, err = oracle_client.execute(user_sql)
if err:
module.fail_json(msg='Cannot add/edit user %s: %s' % (username, err), changed=False)
else:
if extend_sql:
oracle_client.execute(extend_sql)
module.exit_json(msg='User %s has been created.' % username, changed=True, name=username)
def user_remove(module, oracle_client, username):
user = user_find(oracle_client, username)
if user:
rtn, err = oracle_client.execute('drop user %s cascade' % username)
if err:
module.fail_json(msg='Cannot drop user %s: %s' % (username, err), changed=False)
else:
module.exit_json(msg='User %s dropped.' % username, changed=True, name=username)
else:
module.exit_json(msg="User %s doesn't exist." % username, changed=False, name=username)
# =========================================
# Module execution.
#
def main():
argument_spec = oracle_common_argument_spec()
argument_spec.update(
authentication_type=dict(
type='str', required=False,
choices=['external', 'global', 'no_authentication', 'password']
),
default_tablespace=dict(required=False, aliases=['db']),
name=dict(required=True, aliases=['user']),
password=dict(aliases=['pass'], no_log=True),
state=dict(type='str', default='present', choices=['absent', 'present']),
update_password=dict(default="always", choices=["always", "on_create"], no_log=False),
temporary_tablespace=dict(type='str', default=None),
)
module = AnsibleModule(
argument_spec=argument_spec,
supports_check_mode=True,
)
authentication_type = module.params['authentication_type'] or 'password'
default_tablespace = module.params['default_tablespace']
user = module.params['name']
password = module.params['password']
state = module.params['state']
update_password = module.params['update_password']
temporary_tablespace = module.params['temporary_tablespace']
oracle_client = OracleClient(module)
if state == 'present':
if password is None and update_password == 'always':
module.fail_json(
msg='password parameter required when adding a user unless update_password is set to on_create'
)
user_add(
module, oracle_client, username=user, password=password,
auth_type=authentication_type, default_tablespace=default_tablespace,
temporary_tablespace=temporary_tablespace
)
elif state == 'absent':
user_remove(oracle_client)
module.exit_json(changed=True, user=user)
if __name__ == '__main__':
main()

View File

@ -0,0 +1,94 @@
import os
import oracledb
from oracledb.exceptions import DatabaseError
from ansible.module_utils._text import to_native
def oracle_common_argument_spec():
"""
Returns a dict containing common options shared across the Oracle modules.
"""
options = dict(
login_user=dict(type='str', required=False),
login_password=dict(type='str', required=False, no_log=True),
login_database=dict(type='str', required=False, default='test'),
login_host=dict(type='str', required=False, default='localhost'),
login_port=dict(type='int', required=False, default=1521),
oracle_home=dict(type='str', required=False),
mode=dict(type='str', required=False),
)
return options
class OracleClient(object):
def __init__(self, module):
self.module = module
self._conn = None
self._cursor = None
self.connect_params = {}
self.init_params()
def init_params(self):
params = self.module.params
hostname = params['login_host']
port = params['login_port']
service_name = params['login_database']
username = params['login_user']
password = params['login_password']
oracle_home = params['oracle_home']
mode = params['mode']
if oracle_home:
os.environ.setdefault('ORACLE_HOME', oracle_home)
if mode == 'sysdba':
self.connect_params['mode'] = oracledb.SYSDBA
self.connect_params['host'] = hostname
self.connect_params['port'] = port
self.connect_params['user'] = username
self.connect_params['password'] = password
self.connect_params['service_name'] = service_name
@property
def cursor(self):
if self._cursor is None:
try:
oracledb.init_oracle_client(lib_dir='/Users/jiangweidong/Downloads/instantclient_19_8')
self._conn = oracledb.connect(**self.connect_params)
self._cursor = self._conn.cursor()
except DatabaseError as err:
self.module.fail_json(
msg="Unable to connect to database: %s, %s" % (to_native(err), self.connect_params)
)
return self._cursor
def execute(self, sql, exception_to_fail=False):
sql = sql[:-1] if sql.endswith(';') else sql
result, error = None, None
try:
self.cursor.execute(sql)
sql_header = self.cursor.description or []
column_names = [description[0].lower() for description in sql_header]
if column_names:
result = [dict(zip(column_names, row)) for row in self.cursor]
result = result[0] if len(result) == 1 else result
else:
result = None
except DatabaseError as err:
error = err
if exception_to_fail and error:
self.module.fail_json(msg='Cannot execute sql: %s' % to_native(error))
return result, error
def close(self):
try:
if self._cursor:
self._cursor.close()
if self._conn:
self._conn.close()
except:
pass

View File

@ -1,18 +1,18 @@
import time
import asyncio
import os
import threading
import json
from channels.generic.websocket import JsonWebsocketConsumer
from common.utils import get_logger
import aiofiles
from channels.generic.websocket import AsyncJsonWebsocketConsumer
from common.db.utils import close_old_connections
from .celery.utils import get_celery_task_log_path
from common.utils import get_logger
from .ansible.utils import get_ansible_task_log_path
from .celery.utils import get_celery_task_log_path
logger = get_logger(__name__)
class TaskLogWebsocket(JsonWebsocketConsumer):
class TaskLogWebsocket(AsyncJsonWebsocketConsumer):
disconnected = False
log_types = {
@ -20,70 +20,59 @@ class TaskLogWebsocket(JsonWebsocketConsumer):
'ansible': get_ansible_task_log_path
}
def connect(self):
async def connect(self):
user = self.scope["user"]
if user.is_authenticated:
self.accept()
await self.accept()
else:
self.close()
await self.close()
def get_log_path(self, task_id):
func = self.log_types.get(self.log_type)
def get_log_path(self, task_id, log_type):
func = self.log_types.get(log_type)
if func:
return func(task_id)
def receive(self, text_data=None, bytes_data=None, **kwargs):
data = json.loads(text_data)
task_id = data.get('task')
self.log_type = data.get('type', 'celery')
if task_id:
self.handle_task(task_id)
async def receive_json(self, content, **kwargs):
task_id = content.get('task')
task_typ = content.get('type', 'celery')
log_path = self.get_log_path(task_id, task_typ)
await self.async_handle_task(task_id, log_path)
def wait_util_log_path_exist(self, task_id):
log_path = self.get_log_path(task_id)
async def async_handle_task(self, task_id, log_path):
logger.info("Task id: {}".format(task_id))
while not self.disconnected:
if not os.path.exists(log_path):
self.send_json({'message': '.', 'task': task_id})
time.sleep(0.5)
continue
self.send_json({'message': '\r\n'})
try:
logger.debug('Task log path: {}'.format(log_path))
task_log_f = open(log_path, 'rb')
return task_log_f
except OSError:
return None
def read_log_file(self, task_id):
task_log_f = self.wait_util_log_path_exist(task_id)
if not task_log_f:
logger.debug('Task log file is None: {}'.format(task_id))
return
task_end_mark = []
while not self.disconnected:
data = task_log_f.read(4096)
if data:
data = data.replace(b'\n', b'\r\n')
self.send_json(
{'message': data.decode(errors='ignore'), 'task': task_id}
)
if data.find(b'succeeded in') != -1:
task_end_mark.append(1)
if data.find(bytes(task_id, 'utf8')) != -1:
task_end_mark.append(1)
elif len(task_end_mark) == 2:
logger.debug('Task log end: {}'.format(task_id))
await self.send_json({'message': '.', 'task': task_id})
await asyncio.sleep(0.5)
else:
await self.send_task_log(task_id, log_path)
break
time.sleep(0.2)
task_log_f.close()
def handle_task(self, task_id):
logger.info("Task id: {}".format(task_id))
thread = threading.Thread(target=self.read_log_file, args=(task_id,))
thread.start()
async def send_task_log(self, task_id, log_path):
await self.send_json({'message': '\r\n'})
try:
logger.debug('Task log path: {}'.format(log_path))
task_end_mark = []
async with aiofiles.open(log_path, 'rb') as task_log_f:
while not self.disconnected:
data = await task_log_f.read(4096)
if data:
data = data.replace(b'\n', b'\r\n')
await self.send_json(
{'message': data.decode(errors='ignore'), 'task': task_id}
)
if data.find(b'succeeded in') != -1:
task_end_mark.append(1)
if data.find(bytes(task_id, 'utf8')) != -1:
task_end_mark.append(1)
elif len(task_end_mark) == 2:
logger.debug('Task log end: {}'.format(task_id))
break
await asyncio.sleep(0.2)
except OSError as e:
logger.warn('Task log path open failed: {}'.format(e))
def disconnect(self, close_code):
async def disconnect(self, close_code):
self.disconnected = True
self.close()
await self.close()
close_old_connections()

2
jms
View File

@ -177,7 +177,7 @@ if __name__ == '__main__':
help="The service to start",
)
parser.add_argument('-d', '--daemon', nargs="?", const=True)
parser.add_argument('-w', '--worker', type=int, nargs="?", default=4)
parser.add_argument('-w', '--worker', type=int, nargs="?")
parser.add_argument('-f', '--force', nargs="?", const=True)
args = parser.parse_args()

View File

@ -1,3 +1,4 @@
aiofiles==22.1.0
amqp==5.0.9
ansible==6.4.0
ansible-runner==2.2.1