mirror of https://github.com/jumpserver/jumpserver
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
462 lines
16 KiB
462 lines
16 KiB
from __future__ import unicode_literals |
|
|
|
import os |
|
import uuid |
|
import jms_storage |
|
|
|
from django.db import models |
|
from django.db.models.signals import post_save |
|
from django.utils.translation import ugettext_lazy as _ |
|
from django.utils import timezone |
|
from django.conf import settings |
|
from django.core.files.storage import default_storage |
|
from django.core.cache import cache |
|
|
|
from users.models import User |
|
from orgs.mixins.models import OrgModelMixin |
|
from common.mixins import CommonModelMixin |
|
from common.fields.model import EncryptJsonDictTextField |
|
from common.db.models import ChoiceSet |
|
from .backends import get_multi_command_storage |
|
from .backends.command.models import AbstractSessionCommand |
|
from . import const |
|
|
|
|
|
class Terminal(models.Model): |
|
id = models.UUIDField(default=uuid.uuid4, primary_key=True) |
|
name = models.CharField(max_length=32, verbose_name=_('Name')) |
|
remote_addr = models.CharField(max_length=128, blank=True, verbose_name=_('Remote Address')) |
|
ssh_port = models.IntegerField(verbose_name=_('SSH Port'), default=2222) |
|
http_port = models.IntegerField(verbose_name=_('HTTP Port'), default=5000) |
|
command_storage = models.CharField(max_length=128, verbose_name=_("Command storage"), default='default') |
|
replay_storage = models.CharField(max_length=128, verbose_name=_("Replay storage"), default='default') |
|
user = models.OneToOneField(User, related_name='terminal', verbose_name='Application User', null=True, on_delete=models.CASCADE) |
|
is_accepted = models.BooleanField(default=False, verbose_name='Is Accepted') |
|
is_deleted = models.BooleanField(default=False) |
|
date_created = models.DateTimeField(auto_now_add=True) |
|
comment = models.TextField(blank=True, verbose_name=_('Comment')) |
|
STATUS_KEY_PREFIX = 'terminal_status_' |
|
|
|
@property |
|
def is_alive(self): |
|
key = self.STATUS_KEY_PREFIX + str(self.id) |
|
return bool(cache.get(key)) |
|
|
|
@is_alive.setter |
|
def is_alive(self, value): |
|
key = self.STATUS_KEY_PREFIX + str(self.id) |
|
cache.set(key, value, 60) |
|
|
|
@property |
|
def is_active(self): |
|
if self.user and self.user.is_active: |
|
return True |
|
return False |
|
|
|
@is_active.setter |
|
def is_active(self, active): |
|
if self.user: |
|
self.user.is_active = active |
|
self.user.save() |
|
|
|
def get_command_storage(self): |
|
storage = CommandStorage.objects.filter(name=self.command_storage).first() |
|
return storage |
|
|
|
def get_command_storage_config(self): |
|
s = self.get_command_storage() |
|
if s: |
|
config = s.config |
|
else: |
|
config = settings.DEFAULT_TERMINAL_COMMAND_STORAGE |
|
return config |
|
|
|
def get_command_storage_setting(self): |
|
config = self.get_command_storage_config() |
|
return {"TERMINAL_COMMAND_STORAGE": config} |
|
|
|
def get_replay_storage(self): |
|
storage = ReplayStorage.objects.filter(name=self.replay_storage).first() |
|
return storage |
|
|
|
def get_replay_storage_config(self): |
|
s = self.get_replay_storage() |
|
if s: |
|
config = s.config |
|
else: |
|
config = settings.DEFAULT_TERMINAL_REPLAY_STORAGE |
|
return config |
|
|
|
def get_replay_storage_setting(self): |
|
config = self.get_replay_storage_config() |
|
return {"TERMINAL_REPLAY_STORAGE": config} |
|
|
|
@staticmethod |
|
def get_login_title_setting(): |
|
login_title = None |
|
if settings.XPACK_ENABLED: |
|
from xpack.plugins.interface.models import Interface |
|
login_title = Interface.get_login_title() |
|
return {'TERMINAL_HEADER_TITLE': login_title} |
|
|
|
@property |
|
def config(self): |
|
configs = {} |
|
for k in dir(settings): |
|
if not k.startswith('TERMINAL'): |
|
continue |
|
configs[k] = getattr(settings, k) |
|
configs.update(self.get_command_storage_setting()) |
|
configs.update(self.get_replay_storage_setting()) |
|
configs.update(self.get_login_title_setting()) |
|
configs.update({ |
|
'SECURITY_MAX_IDLE_TIME': settings.SECURITY_MAX_IDLE_TIME |
|
}) |
|
return configs |
|
|
|
@property |
|
def service_account(self): |
|
return self.user |
|
|
|
def create_app_user(self): |
|
random = uuid.uuid4().hex[:6] |
|
user, access_key = User.create_app_user( |
|
name="{}-{}".format(self.name, random), comment=self.comment |
|
) |
|
self.user = user |
|
self.save() |
|
return user, access_key |
|
|
|
def delete(self, using=None, keep_parents=False): |
|
if self.user: |
|
self.user.delete() |
|
self.user = None |
|
self.is_deleted = True |
|
self.save() |
|
return |
|
|
|
def __str__(self): |
|
status = "Active" |
|
if not self.is_accepted: |
|
status = "NotAccept" |
|
elif self.is_deleted: |
|
status = "Deleted" |
|
elif not self.is_active: |
|
status = "Disable" |
|
return '%s: %s' % (self.name, status) |
|
|
|
class Meta: |
|
ordering = ('is_accepted',) |
|
db_table = "terminal" |
|
|
|
|
|
class Status(models.Model): |
|
id = models.UUIDField(default=uuid.uuid4, primary_key=True) |
|
session_online = models.IntegerField(verbose_name=_("Session Online"), default=0) |
|
cpu_used = models.FloatField(verbose_name=_("CPU Usage")) |
|
memory_used = models.FloatField(verbose_name=_("Memory Used")) |
|
connections = models.IntegerField(verbose_name=_("Connections")) |
|
threads = models.IntegerField(verbose_name=_("Threads")) |
|
boot_time = models.FloatField(verbose_name=_("Boot Time")) |
|
terminal = models.ForeignKey(Terminal, null=True, on_delete=models.CASCADE) |
|
date_created = models.DateTimeField(auto_now_add=True) |
|
|
|
class Meta: |
|
db_table = 'terminal_status' |
|
get_latest_by = 'date_created' |
|
|
|
def __str__(self): |
|
return self.date_created.strftime("%Y-%m-%d %H:%M:%S") |
|
|
|
|
|
class Session(OrgModelMixin): |
|
class LOGIN_FROM(ChoiceSet): |
|
ST = 'ST', 'SSH Terminal' |
|
WT = 'WT', 'Web Terminal' |
|
|
|
class PROTOCOL(ChoiceSet): |
|
SSH = 'ssh', 'ssh' |
|
RDP = 'rdp', 'rdp' |
|
VNC = 'vnc', 'vnc' |
|
TELNET = 'telnet', 'telnet' |
|
MYSQL = 'mysql', 'mysql' |
|
K8S = 'k8s', 'kubernetes' |
|
|
|
id = models.UUIDField(default=uuid.uuid4, primary_key=True) |
|
user = models.CharField(max_length=128, verbose_name=_("User"), db_index=True) |
|
user_id = models.CharField(blank=True, default='', max_length=36, db_index=True) |
|
asset = models.CharField(max_length=128, verbose_name=_("Asset"), db_index=True) |
|
asset_id = models.CharField(blank=True, default='', max_length=36, db_index=True) |
|
system_user = models.CharField(max_length=128, verbose_name=_("System user"), db_index=True) |
|
system_user_id = models.CharField(blank=True, default='', max_length=36, db_index=True) |
|
login_from = models.CharField(max_length=2, choices=LOGIN_FROM.choices, default="ST", verbose_name=_("Login from")) |
|
remote_addr = models.CharField(max_length=128, verbose_name=_("Remote addr"), blank=True, null=True) |
|
is_success = models.BooleanField(default=True, db_index=True) |
|
is_finished = models.BooleanField(default=False, db_index=True) |
|
has_replay = models.BooleanField(default=False, verbose_name=_("Replay")) |
|
has_command = models.BooleanField(default=False, verbose_name=_("Command")) |
|
terminal = models.ForeignKey(Terminal, null=True, on_delete=models.SET_NULL) |
|
protocol = models.CharField(choices=PROTOCOL.choices, default='ssh', max_length=8, db_index=True) |
|
date_start = models.DateTimeField(verbose_name=_("Date start"), db_index=True, default=timezone.now) |
|
date_end = models.DateTimeField(verbose_name=_("Date end"), null=True) |
|
|
|
upload_to = 'replay' |
|
ACTIVE_CACHE_KEY_PREFIX = 'SESSION_ACTIVE_{}' |
|
_DATE_START_FIRST_HAS_REPLAY_RDP_SESSION = None |
|
|
|
def get_rel_replay_path(self, version=2): |
|
""" |
|
获取session日志的文件路径 |
|
:param version: 原来后缀是 .gz,为了统一新版本改为 .replay.gz |
|
:return: |
|
""" |
|
suffix = '.replay.gz' |
|
if version == 1: |
|
suffix = '.gz' |
|
date = self.date_start.strftime('%Y-%m-%d') |
|
return os.path.join(date, str(self.id) + suffix) |
|
|
|
def get_local_path(self, version=2): |
|
rel_path = self.get_rel_replay_path(version=version) |
|
if version == 2: |
|
local_path = os.path.join(self.upload_to, rel_path) |
|
else: |
|
local_path = rel_path |
|
return local_path |
|
|
|
@property |
|
def _date_start_first_has_replay_rdp_session(self): |
|
if self.__class__._DATE_START_FIRST_HAS_REPLAY_RDP_SESSION is None: |
|
instance = self.__class__.objects.filter( |
|
protocol='rdp', has_replay=True |
|
).order_by('date_start').first() |
|
if not instance: |
|
date_start = timezone.now() - timezone.timedelta(days=365) |
|
else: |
|
date_start = instance.date_start |
|
self.__class__._DATE_START_FIRST_HAS_REPLAY_RDP_SESSION = date_start |
|
return self.__class__._DATE_START_FIRST_HAS_REPLAY_RDP_SESSION |
|
|
|
def can_replay(self): |
|
if self.has_replay: |
|
return True |
|
if self.date_start < self._date_start_first_has_replay_rdp_session: |
|
return True |
|
return False |
|
|
|
@property |
|
def can_join(self): |
|
_PROTOCOL = self.PROTOCOL |
|
if self.is_finished: |
|
return False |
|
if self.protocol not in [_PROTOCOL.SSH, _PROTOCOL.TELNET, _PROTOCOL.MYSQL, _PROTOCOL.K8S]: |
|
return False |
|
return True |
|
|
|
def save_replay_to_storage(self, f): |
|
local_path = self.get_local_path() |
|
try: |
|
name = default_storage.save(local_path, f) |
|
except OSError as e: |
|
return None, e |
|
|
|
if settings.SERVER_REPLAY_STORAGE: |
|
from .tasks import upload_session_replay_to_external_storage |
|
upload_session_replay_to_external_storage.delay(str(self.id)) |
|
return name, None |
|
|
|
@classmethod |
|
def set_sessions_active(cls, sessions_id): |
|
data = {cls.ACTIVE_CACHE_KEY_PREFIX.format(i): i for i in sessions_id} |
|
cache.set_many(data, timeout=5*60) |
|
|
|
@classmethod |
|
def get_active_sessions(cls): |
|
return cls.objects.filter(is_finished=False) |
|
|
|
def is_active(self): |
|
if self.protocol in ['ssh', 'telnet', 'rdp', 'mysql']: |
|
key = self.ACTIVE_CACHE_KEY_PREFIX.format(self.id) |
|
return bool(cache.get(key)) |
|
return True |
|
|
|
@property |
|
def command_amount(self): |
|
command_store = get_multi_command_storage() |
|
return command_store.count(session=str(self.id)) |
|
|
|
@property |
|
def login_from_display(self): |
|
return self.get_login_from_display() |
|
|
|
@classmethod |
|
def generate_fake(cls, count=100, is_finished=True): |
|
import random |
|
from orgs.models import Organization |
|
from users.models import User |
|
from assets.models import Asset, SystemUser |
|
from orgs.utils import get_current_org |
|
from common.utils.random import random_datetime, random_ip |
|
|
|
org = get_current_org() |
|
if not org or not org.is_real(): |
|
Organization.default().change_to() |
|
i = 0 |
|
users = User.objects.all()[:100] |
|
assets = Asset.objects.all()[:100] |
|
system_users = SystemUser.objects.all()[:100] |
|
while i < count: |
|
user_random = random.choices(users, k=10) |
|
assets_random = random.choices(assets, k=10) |
|
system_users = random.choices(system_users, k=10) |
|
|
|
ziped = zip(user_random, assets_random, system_users) |
|
sessions = [] |
|
now = timezone.now() |
|
month_ago = now - timezone.timedelta(days=30) |
|
for user, asset, system_user in ziped: |
|
ip = random_ip() |
|
date_start = random_datetime(month_ago, now) |
|
date_end = random_datetime(date_start, date_start+timezone.timedelta(hours=2)) |
|
data = dict( |
|
user=str(user), user_id=user.id, |
|
asset=str(asset), asset_id=asset.id, |
|
system_user=str(system_user), system_user_id=system_user.id, |
|
remote_addr=ip, |
|
date_start=date_start, |
|
date_end=date_end, |
|
is_finished=is_finished, |
|
) |
|
sessions.append(Session(**data)) |
|
cls.objects.bulk_create(sessions) |
|
i += 10 |
|
|
|
class Meta: |
|
db_table = "terminal_session" |
|
ordering = ["-date_start"] |
|
|
|
def __str__(self): |
|
return "{0.id} of {0.user} to {0.asset}".format(self) |
|
|
|
|
|
class Task(models.Model): |
|
NAME_CHOICES = ( |
|
("kill_session", "Kill Session"), |
|
) |
|
|
|
id = models.UUIDField(default=uuid.uuid4, primary_key=True) |
|
name = models.CharField(max_length=128, choices=NAME_CHOICES, verbose_name=_("Name")) |
|
args = models.CharField(max_length=1024, verbose_name=_("Args")) |
|
terminal = models.ForeignKey(Terminal, null=True, on_delete=models.SET_NULL) |
|
is_finished = models.BooleanField(default=False) |
|
date_created = models.DateTimeField(auto_now_add=True) |
|
date_finished = models.DateTimeField(null=True) |
|
|
|
class Meta: |
|
db_table = "terminal_task" |
|
|
|
|
|
class CommandManager(models.Manager): |
|
def bulk_create(self, objs, **kwargs): |
|
resp = super().bulk_create(objs, **kwargs) |
|
for i in objs: |
|
post_save.send(i.__class__, instance=i, created=True) |
|
return resp |
|
|
|
|
|
class Command(AbstractSessionCommand): |
|
objects = CommandManager() |
|
|
|
class Meta: |
|
db_table = "terminal_command" |
|
ordering = ('-timestamp',) |
|
|
|
|
|
class CommandStorage(CommonModelMixin): |
|
TYPE_CHOICES = const.COMMAND_STORAGE_TYPE_CHOICES |
|
TYPE_DEFAULTS = dict(const.REPLAY_STORAGE_TYPE_CHOICES_DEFAULT).keys() |
|
TYPE_SERVER = const.COMMAND_STORAGE_TYPE_SERVER |
|
|
|
name = models.CharField(max_length=32, verbose_name=_("Name"), unique=True) |
|
type = models.CharField( |
|
max_length=16, choices=TYPE_CHOICES, verbose_name=_('Type'), |
|
default=TYPE_SERVER |
|
) |
|
meta = EncryptJsonDictTextField(default={}) |
|
comment = models.TextField( |
|
max_length=128, default='', blank=True, verbose_name=_('Comment') |
|
) |
|
|
|
def __str__(self): |
|
return self.name |
|
|
|
@property |
|
def config(self): |
|
config = self.meta |
|
config.update({'TYPE': self.type}) |
|
return config |
|
|
|
def in_defaults(self): |
|
return self.type in self.TYPE_DEFAULTS |
|
|
|
def is_valid(self): |
|
if self.in_defaults(): |
|
return True |
|
storage = jms_storage.get_log_storage(self.config) |
|
return storage.ping() |
|
|
|
def can_delete(self): |
|
return not self.in_defaults() |
|
|
|
|
|
class ReplayStorage(CommonModelMixin): |
|
TYPE_CHOICES = const.REPLAY_STORAGE_TYPE_CHOICES |
|
TYPE_SERVER = const.REPLAY_STORAGE_TYPE_SERVER |
|
TYPE_DEFAULTS = dict(const.REPLAY_STORAGE_TYPE_CHOICES_DEFAULT).keys() |
|
|
|
name = models.CharField(max_length=32, verbose_name=_("Name"), unique=True) |
|
type = models.CharField( |
|
max_length=16, choices=TYPE_CHOICES, verbose_name=_('Type'), |
|
default=TYPE_SERVER |
|
) |
|
meta = EncryptJsonDictTextField(default={}) |
|
comment = models.TextField( |
|
max_length=128, default='', blank=True, verbose_name=_('Comment') |
|
) |
|
|
|
def __str__(self): |
|
return self.name |
|
|
|
def convert_type(self): |
|
s3_type_list = [const.REPLAY_STORAGE_TYPE_CEPH] |
|
tp = self.type |
|
if tp in s3_type_list: |
|
tp = const.REPLAY_STORAGE_TYPE_S3 |
|
return tp |
|
|
|
def get_extra_config(self): |
|
extra_config = {'TYPE': self.convert_type()} |
|
if self.type == const.REPLAY_STORAGE_TYPE_SWIFT: |
|
extra_config.update({'signer': 'S3SignerType'}) |
|
return extra_config |
|
|
|
@property |
|
def config(self): |
|
config = self.meta |
|
extra_config = self.get_extra_config() |
|
config.update(extra_config) |
|
return config |
|
|
|
def in_defaults(self): |
|
return self.type in self.TYPE_DEFAULTS |
|
|
|
def is_valid(self): |
|
if self.in_defaults(): |
|
return True |
|
storage = jms_storage.get_object_storage(self.config) |
|
target = 'tests.py' |
|
src = os.path.join(settings.BASE_DIR, 'common', target) |
|
return storage.is_valid(src, target) |
|
|
|
def can_delete(self): |
|
return not self.in_defaults()
|
|
|