mirror of https://github.com/jumpserver/jumpserver
feat: 账号密钥用vault储存 (#10830)
* feat: 账号密钥用vault储存 * perf: 优化 Vault * perf: 重构 Vault Backend 设计架构 (未完成) * perf: 重构 Vault Backend 设计架构 (未完成2) * perf: 重构 Vault Backend 设计架构 (未完成3) * perf: 重构 Vault Backend 设计架构 (未完成4) * perf: 重构 Vault Backend 设计架构 (未完成5) * perf: 重构 Vault Backend 设计架构 (已完成) * perf: 重构 Vault Backend 设计架构 (已完成) * perf: 重构 Vault Backend 设计架构 (已完成) * perf: 小优化 * perf: 优化 --------- Co-authored-by: feng <1304903146@qq.com> Co-authored-by: Bai <baijiangjie@gmail.com> Co-authored-by: feng626 <57284900+feng626@users.noreply.github.com>pull/11133/head
parent
7776158279
commit
3b615719fe
|
@ -26,6 +26,7 @@ class AccountViewSet(OrgBulkModelViewSet):
|
||||||
filterset_class = AccountFilterSet
|
filterset_class = AccountFilterSet
|
||||||
serializer_classes = {
|
serializer_classes = {
|
||||||
'default': serializers.AccountSerializer,
|
'default': serializers.AccountSerializer,
|
||||||
|
'retrieve': serializers.AccountDetailSerializer,
|
||||||
}
|
}
|
||||||
rbac_perms = {
|
rbac_perms = {
|
||||||
'partial_update': ['accounts.change_account'],
|
'partial_update': ['accounts.change_account'],
|
||||||
|
@ -133,11 +134,13 @@ class AccountHistoriesSecretAPI(ExtraFilterFieldsMixin, RecordViewLogMixin, List
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
account = self.get_object()
|
account = self.get_object()
|
||||||
histories = account.history.all()
|
histories = account.history.all()
|
||||||
last_history = account.history.first()
|
latest_history = account.history.first()
|
||||||
if not last_history:
|
if not latest_history:
|
||||||
|
return histories
|
||||||
|
if account.secret != latest_history.secret:
|
||||||
|
return histories
|
||||||
|
if account.secret_type != latest_history.secret_type:
|
||||||
|
return histories
|
||||||
|
histories = histories.exclude(history_id=latest_history.history_id)
|
||||||
return histories
|
return histories
|
||||||
|
|
||||||
if account.secret == last_history.secret \
|
|
||||||
and account.secret_type == last_history.secret_type:
|
|
||||||
histories = histories.exclude(history_id=last_history.history_id)
|
|
||||||
return histories
|
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
import os
|
||||||
|
from django.utils.functional import LazyObject
|
||||||
|
from importlib import import_module
|
||||||
|
|
||||||
|
from common.utils import get_logger
|
||||||
|
from ..const import VaultTypeChoices
|
||||||
|
|
||||||
|
__all__ = ['vault_client', 'get_vault_client']
|
||||||
|
|
||||||
|
|
||||||
|
logger = get_logger(__file__)
|
||||||
|
|
||||||
|
|
||||||
|
def get_vault_client(raise_exception=False, **kwargs):
|
||||||
|
try:
|
||||||
|
tp = kwargs.get('VAULT_TYPE')
|
||||||
|
module_path = f'apps.accounts.backends.{tp}.main'
|
||||||
|
client = import_module(module_path).Vault(**kwargs)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f'Init vault client failed: {e}')
|
||||||
|
if raise_exception:
|
||||||
|
raise
|
||||||
|
tp = VaultTypeChoices.local
|
||||||
|
module_path = f'apps.accounts.backends.{tp}.main'
|
||||||
|
kwargs['VAULT_TYPE'] = tp
|
||||||
|
client = import_module(module_path).Vault(**kwargs)
|
||||||
|
return client
|
||||||
|
|
||||||
|
|
||||||
|
class VaultClient(LazyObject):
|
||||||
|
|
||||||
|
def _setup(self):
|
||||||
|
from jumpserver import settings as js_settings
|
||||||
|
from django.conf import settings
|
||||||
|
vault_config_names = [k for k in js_settings.__dict__.keys() if k.startswith('VAULT_')]
|
||||||
|
vault_configs = {name: getattr(settings, name, None) for name in vault_config_names}
|
||||||
|
self._wrapped = get_vault_client(**vault_configs)
|
||||||
|
|
||||||
|
|
||||||
|
""" 为了安全, 页面修改配置, 重启服务后才会重新初始化 vault_client """
|
||||||
|
vault_client = VaultClient()
|
|
@ -0,0 +1,77 @@
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
|
||||||
|
from django.forms.models import model_to_dict
|
||||||
|
|
||||||
|
__all__ = ['BaseVault']
|
||||||
|
|
||||||
|
|
||||||
|
class BaseVault(ABC):
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.type = kwargs.get('VAULT_TYPE')
|
||||||
|
|
||||||
|
def is_type(self, tp):
|
||||||
|
return self.type == tp
|
||||||
|
|
||||||
|
def get(self, instance):
|
||||||
|
""" 返回 secret 值 """
|
||||||
|
return self._get(instance)
|
||||||
|
|
||||||
|
def create(self, instance):
|
||||||
|
if not instance.secret_has_save_to_vault:
|
||||||
|
self._create(instance)
|
||||||
|
self._clean_db_secret(instance)
|
||||||
|
self.save_metadata(instance)
|
||||||
|
|
||||||
|
if instance.is_sync_metadata:
|
||||||
|
self.save_metadata(instance)
|
||||||
|
|
||||||
|
def update(self, instance):
|
||||||
|
if not instance.secret_has_save_to_vault:
|
||||||
|
self._update(instance)
|
||||||
|
self._clean_db_secret(instance)
|
||||||
|
self.save_metadata(instance)
|
||||||
|
|
||||||
|
if instance.is_sync_metadata:
|
||||||
|
self.save_metadata(instance)
|
||||||
|
|
||||||
|
def delete(self, instance):
|
||||||
|
self._delete(instance)
|
||||||
|
|
||||||
|
def save_metadata(self, instance):
|
||||||
|
metadata = model_to_dict(instance, fields=[
|
||||||
|
'name', 'username', 'secret_type',
|
||||||
|
'connectivity', 'su_from', 'privileged'
|
||||||
|
])
|
||||||
|
metadata = {field: str(value) for field, value in metadata.items()}
|
||||||
|
return self._save_metadata(instance, metadata)
|
||||||
|
|
||||||
|
# -------- abstractmethod -------- #
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def _get(self, instance):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def _create(self, instance):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def _update(self, instance):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def _delete(self, instance):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def _clean_db_secret(self, instance):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def _save_metadata(self, instance, metadata):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def is_active(self, *args, **kwargs) -> (bool, str):
|
||||||
|
raise NotImplementedError
|
|
@ -0,0 +1 @@
|
||||||
|
from .main import *
|
|
@ -0,0 +1,84 @@
|
||||||
|
import sys
|
||||||
|
from abc import ABC
|
||||||
|
|
||||||
|
from common.db.utils import Encryptor
|
||||||
|
from common.utils import lazyproperty
|
||||||
|
|
||||||
|
current_module = sys.modules[__name__]
|
||||||
|
|
||||||
|
__all__ = ['build_entry']
|
||||||
|
|
||||||
|
|
||||||
|
class BaseEntry(ABC):
|
||||||
|
|
||||||
|
def __init__(self, instance):
|
||||||
|
self.instance = instance
|
||||||
|
|
||||||
|
@lazyproperty
|
||||||
|
def full_path(self):
|
||||||
|
path_base = self.path_base
|
||||||
|
path_spec = self.path_spec
|
||||||
|
path = f'{path_base}/{path_spec}'
|
||||||
|
return path
|
||||||
|
|
||||||
|
@property
|
||||||
|
def path_base(self):
|
||||||
|
path = f'orgs/{self.instance.org_id}'
|
||||||
|
return path
|
||||||
|
|
||||||
|
@property
|
||||||
|
def path_spec(self):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def to_internal_data(self):
|
||||||
|
secret = getattr(self.instance, '_secret', None)
|
||||||
|
if secret is not None:
|
||||||
|
secret = Encryptor(secret).encrypt()
|
||||||
|
data = {'secret': secret}
|
||||||
|
return data
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def to_external_data(data):
|
||||||
|
secret = data.pop('secret', None)
|
||||||
|
if secret is not None:
|
||||||
|
secret = Encryptor(secret).decrypt()
|
||||||
|
return secret
|
||||||
|
|
||||||
|
|
||||||
|
class AccountEntry(BaseEntry):
|
||||||
|
|
||||||
|
@property
|
||||||
|
def path_spec(self):
|
||||||
|
path = f'assets/{self.instance.asset_id}/accounts/{self.instance.id}'
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
class AccountTemplateEntry(BaseEntry):
|
||||||
|
|
||||||
|
@property
|
||||||
|
def path_spec(self):
|
||||||
|
path = f'account-templates/{self.instance.id}'
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
class HistoricalAccountEntry(BaseEntry):
|
||||||
|
|
||||||
|
@property
|
||||||
|
def path_base(self):
|
||||||
|
account = self.instance.instance
|
||||||
|
path = f'accounts/{account.id}/'
|
||||||
|
return path
|
||||||
|
|
||||||
|
@property
|
||||||
|
def path_spec(self):
|
||||||
|
path = f'histories/{self.instance.history_id}'
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def build_entry(instance) -> BaseEntry:
|
||||||
|
class_name = instance.__class__.__name__
|
||||||
|
entry_class_name = f'{class_name}Entry'
|
||||||
|
entry_class = getattr(current_module, entry_class_name, None)
|
||||||
|
if not entry_class:
|
||||||
|
raise Exception(f'Entry class {entry_class_name} is not found')
|
||||||
|
return entry_class(instance)
|
|
@ -0,0 +1,47 @@
|
||||||
|
from .entries import build_entry
|
||||||
|
from .service import VaultKVClient
|
||||||
|
from ..base import BaseVault
|
||||||
|
|
||||||
|
__all__ = ['Vault']
|
||||||
|
|
||||||
|
|
||||||
|
class Vault(BaseVault):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.client = VaultKVClient(
|
||||||
|
url=kwargs.get('VAULT_HCP_HOST'),
|
||||||
|
token=kwargs.get('VAULT_HCP_TOKEN'),
|
||||||
|
mount_point=kwargs.get('VAULT_HCP_MOUNT_POINT')
|
||||||
|
)
|
||||||
|
|
||||||
|
def is_active(self):
|
||||||
|
return self.client.is_active()
|
||||||
|
|
||||||
|
def _get(self, instance):
|
||||||
|
entry = build_entry(instance)
|
||||||
|
# TODO: get data 是不是层数太多了
|
||||||
|
data = self.client.get(path=entry.full_path).get('data', {})
|
||||||
|
data = entry.to_external_data(data)
|
||||||
|
return data
|
||||||
|
|
||||||
|
def _create(self, instance):
|
||||||
|
entry = build_entry(instance)
|
||||||
|
data = entry.to_internal_data()
|
||||||
|
self.client.create(path=entry.full_path, data=data)
|
||||||
|
|
||||||
|
def _update(self, instance):
|
||||||
|
entry = build_entry(instance)
|
||||||
|
data = entry.to_internal_data()
|
||||||
|
self.client.patch(path=entry.full_path, data=data)
|
||||||
|
|
||||||
|
def _delete(self, instance):
|
||||||
|
entry = build_entry(instance)
|
||||||
|
self.client.delete(path=entry.full_path)
|
||||||
|
|
||||||
|
def _clean_db_secret(self, instance):
|
||||||
|
instance.is_sync_metadata = False
|
||||||
|
instance.mark_secret_save_to_vault()
|
||||||
|
|
||||||
|
def _save_metadata(self, instance, metadata):
|
||||||
|
entry = build_entry(instance)
|
||||||
|
self.client.update_metadata(path=entry.full_path, metadata=metadata)
|
|
@ -0,0 +1,103 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
import hvac
|
||||||
|
from hvac import exceptions
|
||||||
|
from requests.exceptions import ConnectionError
|
||||||
|
|
||||||
|
from common.utils import get_logger
|
||||||
|
|
||||||
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
__all__ = ['VaultKVClient']
|
||||||
|
|
||||||
|
|
||||||
|
class VaultKVClient(object):
|
||||||
|
max_versions = 20
|
||||||
|
|
||||||
|
def __init__(self, url, token, mount_point):
|
||||||
|
assert isinstance(self.max_versions, int) and self.max_versions >= 3, (
|
||||||
|
'max_versions must to be an integer that is greater than or equal to 3'
|
||||||
|
)
|
||||||
|
self.client = hvac.Client(url=url, token=token)
|
||||||
|
self.mount_point = mount_point
|
||||||
|
self.enable_secrets_engine_if_need()
|
||||||
|
|
||||||
|
def is_active(self):
|
||||||
|
try:
|
||||||
|
if not self.client.sys.is_initialized():
|
||||||
|
return False, 'Vault is not initialized'
|
||||||
|
if self.client.sys.is_sealed():
|
||||||
|
return False, 'Vault is sealed'
|
||||||
|
if not self.client.is_authenticated():
|
||||||
|
return False, 'Vault is not authenticated'
|
||||||
|
except ConnectionError as e:
|
||||||
|
logger.error(str(e))
|
||||||
|
return False, f'Vault is not reachable: {e}'
|
||||||
|
else:
|
||||||
|
return True, ''
|
||||||
|
|
||||||
|
def enable_secrets_engine_if_need(self):
|
||||||
|
secrets_engines = self.client.sys.list_mounted_secrets_engines()
|
||||||
|
mount_points = secrets_engines.keys()
|
||||||
|
if f'{self.mount_point}/' in mount_points:
|
||||||
|
return
|
||||||
|
self.client.sys.enable_secrets_engine(
|
||||||
|
backend_type='kv',
|
||||||
|
path=self.mount_point,
|
||||||
|
options={'version': 2} # TODO: version 是否从配置中读取?
|
||||||
|
)
|
||||||
|
self.client.secrets.kv.v2.configure(
|
||||||
|
max_versions=self.max_versions,
|
||||||
|
mount_point=self.mount_point
|
||||||
|
)
|
||||||
|
|
||||||
|
def get(self, path, version=None):
|
||||||
|
try:
|
||||||
|
response = self.client.secrets.kv.v2.read_secret_version(
|
||||||
|
path=path,
|
||||||
|
version=version,
|
||||||
|
mount_point=self.mount_point
|
||||||
|
)
|
||||||
|
except exceptions.InvalidPath as e:
|
||||||
|
logger.error('Get secret error: {}'.format(e))
|
||||||
|
return {}
|
||||||
|
data = response.get('data', {})
|
||||||
|
return data
|
||||||
|
|
||||||
|
def create(self, path, data: dict):
|
||||||
|
self._update_or_create(path=path, data=data)
|
||||||
|
|
||||||
|
def update(self, path, data: dict):
|
||||||
|
""" 未更新的数据会被删除 """
|
||||||
|
self._update_or_create(path=path, data=data)
|
||||||
|
|
||||||
|
def patch(self, path, data: dict):
|
||||||
|
""" 未更新的数据不会被删除 """
|
||||||
|
self.client.secrets.kv.v2.patch(
|
||||||
|
path=path,
|
||||||
|
secret=data,
|
||||||
|
mount_point=self.mount_point
|
||||||
|
)
|
||||||
|
|
||||||
|
def delete(self, path):
|
||||||
|
self.client.secrets.kv.v2.delete_metadata_and_all_versions(
|
||||||
|
path=path,
|
||||||
|
mount_point=self.mount_point,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _update_or_create(self, path, data: dict):
|
||||||
|
self.client.secrets.kv.v2.create_or_update_secret(
|
||||||
|
path=path,
|
||||||
|
secret=data,
|
||||||
|
mount_point=self.mount_point
|
||||||
|
)
|
||||||
|
|
||||||
|
def update_metadata(self, path, metadata: dict):
|
||||||
|
try:
|
||||||
|
self.client.secrets.kv.v2.update_metadata(
|
||||||
|
path=path,
|
||||||
|
mount_point=self.mount_point,
|
||||||
|
custom_metadata=metadata
|
||||||
|
)
|
||||||
|
except exceptions.InvalidPath as e:
|
||||||
|
logger.error('Update metadata error: {}'.format(e))
|
|
@ -0,0 +1 @@
|
||||||
|
from .main import *
|
|
@ -0,0 +1,36 @@
|
||||||
|
from common.utils import get_logger
|
||||||
|
from ..base import BaseVault
|
||||||
|
|
||||||
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
__all__ = ['Vault']
|
||||||
|
|
||||||
|
|
||||||
|
class Vault(BaseVault):
|
||||||
|
|
||||||
|
def is_active(self):
|
||||||
|
return True, ''
|
||||||
|
|
||||||
|
def _get(self, instance):
|
||||||
|
secret = getattr(instance, '_secret', None)
|
||||||
|
return secret
|
||||||
|
|
||||||
|
def _create(self, instance):
|
||||||
|
""" Ignore """
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _update(self, instance):
|
||||||
|
""" Ignore """
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _delete(self, instance):
|
||||||
|
""" Ignore """
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _save_metadata(self, instance, metadata):
|
||||||
|
""" Ignore """
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _clean_db_secret(self, instance):
|
||||||
|
""" Ignore *重要* 不能删除本地 secret """
|
||||||
|
pass
|
|
@ -1,2 +1,3 @@
|
||||||
from .account import *
|
from .account import *
|
||||||
from .automation import *
|
from .automation import *
|
||||||
|
from .vault import *
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
from django.db import models
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
__all__ = ['VaultTypeChoices']
|
||||||
|
|
||||||
|
|
||||||
|
class VaultTypeChoices(models.TextChoices):
|
||||||
|
local = 'local', _('Local Vault')
|
||||||
|
hcp = 'hcp', _('HCP Vault')
|
|
@ -0,0 +1,28 @@
|
||||||
|
# Generated by Django 3.2.19 on 2023-06-21 06:56
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('accounts', '0011_auto_20230506_1443'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='account',
|
||||||
|
old_name='secret',
|
||||||
|
new_name='_secret',
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='accounttemplate',
|
||||||
|
old_name='secret',
|
||||||
|
new_name='_secret',
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='historicalaccount',
|
||||||
|
old_name='secret',
|
||||||
|
new_name='_secret',
|
||||||
|
),
|
||||||
|
]
|
|
@ -7,9 +7,10 @@ from simple_history.models import HistoricalRecords
|
||||||
from assets.models.base import AbsConnectivity
|
from assets.models.base import AbsConnectivity
|
||||||
from common.utils import lazyproperty
|
from common.utils import lazyproperty
|
||||||
from .base import BaseAccount
|
from .base import BaseAccount
|
||||||
|
from .mixins import VaultModelMixin
|
||||||
from ..const import AliasAccount, Source
|
from ..const import AliasAccount, Source
|
||||||
|
|
||||||
__all__ = ['Account', 'AccountTemplate']
|
__all__ = ['Account', 'AccountTemplate', 'AccountHistoricalRecords']
|
||||||
|
|
||||||
|
|
||||||
class AccountHistoricalRecords(HistoricalRecords):
|
class AccountHistoricalRecords(HistoricalRecords):
|
||||||
|
@ -32,7 +33,7 @@ class AccountHistoricalRecords(HistoricalRecords):
|
||||||
diff = attrs - history_attrs
|
diff = attrs - history_attrs
|
||||||
if not diff:
|
if not diff:
|
||||||
return
|
return
|
||||||
super().post_save(instance, created, using=using, **kwargs)
|
return super().post_save(instance, created, using=using, **kwargs)
|
||||||
|
|
||||||
def create_history_model(self, model, inherited):
|
def create_history_model(self, model, inherited):
|
||||||
if self.included_fields and not self.excluded_fields:
|
if self.included_fields and not self.excluded_fields:
|
||||||
|
@ -53,7 +54,7 @@ class Account(AbsConnectivity, BaseAccount):
|
||||||
on_delete=models.SET_NULL, verbose_name=_("Su from")
|
on_delete=models.SET_NULL, verbose_name=_("Su from")
|
||||||
)
|
)
|
||||||
version = models.IntegerField(default=0, verbose_name=_('Version'))
|
version = models.IntegerField(default=0, verbose_name=_('Version'))
|
||||||
history = AccountHistoricalRecords(included_fields=['id', 'secret', 'secret_type', 'version'])
|
history = AccountHistoricalRecords(included_fields=['id', '_secret', 'secret_type', 'version'])
|
||||||
source = models.CharField(max_length=30, default=Source.LOCAL, verbose_name=_('Source'))
|
source = models.CharField(max_length=30, default=Source.LOCAL, verbose_name=_('Source'))
|
||||||
source_id = models.CharField(max_length=128, null=True, blank=True, verbose_name=_('Source ID'))
|
source_id = models.CharField(max_length=128, null=True, blank=True, verbose_name=_('Source ID'))
|
||||||
|
|
||||||
|
@ -198,3 +199,21 @@ class AccountTemplate(BaseAccount):
|
||||||
return
|
return
|
||||||
self.bulk_update_accounts(accounts, {'secret': self.secret})
|
self.bulk_update_accounts(accounts, {'secret': self.secret})
|
||||||
self.bulk_create_history_accounts(accounts, user_id)
|
self.bulk_create_history_accounts(accounts, user_id)
|
||||||
|
|
||||||
|
|
||||||
|
def replace_history_model_with_mixin():
|
||||||
|
"""
|
||||||
|
替换历史模型中的父类为指定的Mixin类。
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
model (class): 历史模型类,例如 Account.history.model
|
||||||
|
mixin_class (class): 要替换为的Mixin类
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None
|
||||||
|
"""
|
||||||
|
model = Account.history.model
|
||||||
|
model.__bases__ = (VaultModelMixin,) + model.__bases__
|
||||||
|
|
||||||
|
|
||||||
|
replace_history_model_with_mixin()
|
||||||
|
|
|
@ -9,33 +9,32 @@ from django.db import models
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from accounts.const import SecretType
|
from accounts.const import SecretType
|
||||||
from common.db import fields
|
|
||||||
from common.utils import (
|
from common.utils import (
|
||||||
ssh_key_string_to_obj, ssh_key_gen, get_logger,
|
ssh_key_string_to_obj, ssh_key_gen, get_logger,
|
||||||
random_string, lazyproperty, parse_ssh_public_key_str, is_openssh_format_key
|
random_string, lazyproperty, parse_ssh_public_key_str, is_openssh_format_key
|
||||||
)
|
)
|
||||||
|
from accounts.models.mixins import VaultModelMixin, VaultManagerMixin, VaultQuerySetMixin
|
||||||
from orgs.mixins.models import JMSOrgBaseModel, OrgManager
|
from orgs.mixins.models import JMSOrgBaseModel, OrgManager
|
||||||
|
|
||||||
logger = get_logger(__file__)
|
logger = get_logger(__file__)
|
||||||
|
|
||||||
|
|
||||||
class BaseAccountQuerySet(models.QuerySet):
|
class BaseAccountQuerySet(VaultQuerySetMixin, models.QuerySet):
|
||||||
def active(self):
|
def active(self):
|
||||||
return self.filter(is_active=True)
|
return self.filter(is_active=True)
|
||||||
|
|
||||||
|
|
||||||
class BaseAccountManager(OrgManager):
|
class BaseAccountManager(VaultManagerMixin, OrgManager):
|
||||||
def active(self):
|
def active(self):
|
||||||
return self.get_queryset().active()
|
return self.get_queryset().active()
|
||||||
|
|
||||||
|
|
||||||
class BaseAccount(JMSOrgBaseModel):
|
class BaseAccount(VaultModelMixin, JMSOrgBaseModel):
|
||||||
name = models.CharField(max_length=128, verbose_name=_("Name"))
|
name = models.CharField(max_length=128, verbose_name=_("Name"))
|
||||||
username = models.CharField(max_length=128, blank=True, verbose_name=_('Username'), db_index=True)
|
username = models.CharField(max_length=128, blank=True, verbose_name=_('Username'), db_index=True)
|
||||||
secret_type = models.CharField(
|
secret_type = models.CharField(
|
||||||
max_length=16, choices=SecretType.choices, default=SecretType.PASSWORD, verbose_name=_('Secret type')
|
max_length=16, choices=SecretType.choices, default=SecretType.PASSWORD, verbose_name=_('Secret type')
|
||||||
)
|
)
|
||||||
secret = fields.EncryptTextField(blank=True, null=True, verbose_name=_('Secret'))
|
|
||||||
privileged = models.BooleanField(verbose_name=_("Privileged"), default=False)
|
privileged = models.BooleanField(verbose_name=_("Privileged"), default=False)
|
||||||
is_active = models.BooleanField(default=True, verbose_name=_("Is active"))
|
is_active = models.BooleanField(default=True, verbose_name=_("Is active"))
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
from .vault import *
|
|
@ -0,0 +1,88 @@
|
||||||
|
from django.db import models
|
||||||
|
from django.db.models.signals import post_save
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
from common.db import fields
|
||||||
|
|
||||||
|
__all__ = ['VaultQuerySetMixin', 'VaultManagerMixin', 'VaultModelMixin']
|
||||||
|
|
||||||
|
|
||||||
|
class VaultQuerySetMixin(models.QuerySet):
|
||||||
|
|
||||||
|
def update(self, **kwargs):
|
||||||
|
"""
|
||||||
|
1. 替换 secret 为 _secret
|
||||||
|
2. 触发 post_save 信号
|
||||||
|
"""
|
||||||
|
if 'secret' in kwargs:
|
||||||
|
kwargs.update({
|
||||||
|
'_secret': kwargs.pop('secret')
|
||||||
|
})
|
||||||
|
rows = super().update(**kwargs)
|
||||||
|
|
||||||
|
# 为了获取更新后的对象所以单独查询一次
|
||||||
|
ids = self.values_list('id', flat=True)
|
||||||
|
objs = self.model.objects.filter(id__in=ids)
|
||||||
|
for obj in objs:
|
||||||
|
post_save.send(obj.__class__, instance=obj, created=False)
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
class VaultManagerMixin(models.Manager):
|
||||||
|
""" 触发 bulk_create 和 bulk_update 操作下的 post_save 信号 """
|
||||||
|
|
||||||
|
def bulk_create(self, objs, batch_size=None, ignore_conflicts=False):
|
||||||
|
objs = super().bulk_create(objs, batch_size=batch_size, ignore_conflicts=ignore_conflicts)
|
||||||
|
for obj in objs:
|
||||||
|
post_save.send(obj.__class__, instance=obj, created=True)
|
||||||
|
return objs
|
||||||
|
|
||||||
|
def bulk_update(self, objs, batch_size=None, ignore_conflicts=False):
|
||||||
|
objs = super().bulk_update(objs, batch_size=batch_size, ignore_conflicts=ignore_conflicts)
|
||||||
|
for obj in objs:
|
||||||
|
post_save.send(obj.__class__, instance=obj, created=False)
|
||||||
|
return objs
|
||||||
|
|
||||||
|
|
||||||
|
class VaultModelMixin(models.Model):
|
||||||
|
_secret = fields.EncryptTextField(blank=True, null=True, verbose_name=_('Secret'))
|
||||||
|
is_sync_metadata = True
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
# 缓存 secret 值, lazy-property 不能用
|
||||||
|
__secret = False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def secret(self):
|
||||||
|
if self.__secret is False:
|
||||||
|
from accounts.backends import vault_client
|
||||||
|
secret = vault_client.get(self)
|
||||||
|
if not secret and not self.secret_has_save_to_vault:
|
||||||
|
# vault_client 获取不到, 并且 secret 没有保存到 vault, 就从 self._secret 获取
|
||||||
|
secret = self._secret
|
||||||
|
self.__secret = secret
|
||||||
|
return self.__secret
|
||||||
|
|
||||||
|
@secret.setter
|
||||||
|
def secret(self, value):
|
||||||
|
"""
|
||||||
|
保存的时候通过 post_save 信号监听进行处理,
|
||||||
|
先保存到 db, 再保存到 vault 同时删除本地 db _secret 值
|
||||||
|
"""
|
||||||
|
self._secret = value
|
||||||
|
|
||||||
|
_secret_save_to_vault_mark = '# Secret-has-been-saved-to-vault #'
|
||||||
|
|
||||||
|
def mark_secret_save_to_vault(self):
|
||||||
|
self._secret = self._secret_save_to_vault_mark
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def secret_has_save_to_vault(self):
|
||||||
|
return self._secret == self._secret_save_to_vault_mark
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
""" 通过 post_save signal 处理 _secret 数据 """
|
||||||
|
return super().save(*args, **kwargs)
|
|
@ -198,7 +198,6 @@ class AccountAssetSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
class AccountSerializer(AccountCreateUpdateSerializerMixin, BaseAccountSerializer):
|
class AccountSerializer(AccountCreateUpdateSerializerMixin, BaseAccountSerializer):
|
||||||
asset = AccountAssetSerializer(label=_('Asset'))
|
asset = AccountAssetSerializer(label=_('Asset'))
|
||||||
has_secret = serializers.BooleanField(label=_("Has secret"), read_only=True)
|
|
||||||
source = LabeledChoiceField(
|
source = LabeledChoiceField(
|
||||||
choices=Source.choices, label=_("Source"), required=False,
|
choices=Source.choices, label=_("Source"), required=False,
|
||||||
allow_null=True, default=Source.LOCAL
|
allow_null=True, default=Source.LOCAL
|
||||||
|
@ -233,6 +232,15 @@ class AccountSerializer(AccountCreateUpdateSerializerMixin, BaseAccountSerialize
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
|
class AccountDetailSerializer(AccountSerializer):
|
||||||
|
has_secret = serializers.BooleanField(label=_("Has secret"), read_only=True)
|
||||||
|
|
||||||
|
class Meta(AccountSerializer.Meta):
|
||||||
|
model = Account
|
||||||
|
fields = AccountSerializer.Meta.fields + ['has_secret']
|
||||||
|
read_only_fields = AccountSerializer.Meta.read_only_fields + ['has_secret']
|
||||||
|
|
||||||
|
|
||||||
class AssetAccountBulkSerializerResultSerializer(serializers.Serializer):
|
class AssetAccountBulkSerializerResultSerializer(serializers.Serializer):
|
||||||
asset = serializers.CharField(read_only=True, label=_('Asset'))
|
asset = serializers.CharField(read_only=True, label=_('Asset'))
|
||||||
state = serializers.CharField(read_only=True, label=_('State'))
|
state = serializers.CharField(read_only=True, label=_('State'))
|
||||||
|
|
|
@ -61,20 +61,18 @@ class AuthValidateMixin(serializers.Serializer):
|
||||||
|
|
||||||
|
|
||||||
class BaseAccountSerializer(AuthValidateMixin, BulkOrgResourceModelSerializer):
|
class BaseAccountSerializer(AuthValidateMixin, BulkOrgResourceModelSerializer):
|
||||||
has_secret = serializers.BooleanField(label=_("Has secret"), read_only=True)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = BaseAccount
|
model = BaseAccount
|
||||||
fields_mini = ['id', 'name', 'username']
|
fields_mini = ['id', 'name', 'username']
|
||||||
fields_small = fields_mini + [
|
fields_small = fields_mini + [
|
||||||
'secret_type', 'secret', 'has_secret', 'passphrase',
|
'secret_type', 'secret', 'passphrase',
|
||||||
'privileged', 'is_active', 'spec_info',
|
'privileged', 'is_active', 'spec_info',
|
||||||
]
|
]
|
||||||
fields_other = ['created_by', 'date_created', 'date_updated', 'comment']
|
fields_other = ['created_by', 'date_created', 'date_updated', 'comment']
|
||||||
fields = fields_small + fields_other
|
fields = fields_small + fields_other
|
||||||
read_only_fields = [
|
read_only_fields = [
|
||||||
'has_secret', 'spec_info',
|
'spec_info', 'date_verified', 'created_by', 'date_created',
|
||||||
'date_verified', 'created_by', 'date_created',
|
|
||||||
]
|
]
|
||||||
extra_kwargs = {
|
extra_kwargs = {
|
||||||
'spec_info': {'label': _('Spec info')},
|
'spec_info': {'label': _('Spec info')},
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
from django.db.models.signals import pre_save
|
from django.db.models.signals import pre_save, post_save, post_delete
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
|
|
||||||
|
from accounts.backends import vault_client
|
||||||
from common.utils import get_logger
|
from common.utils import get_logger
|
||||||
from .models import Account
|
from .models import Account, AccountTemplate
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
@ -13,3 +14,23 @@ def on_account_pre_save(sender, instance, **kwargs):
|
||||||
instance.version = 1
|
instance.version = 1
|
||||||
else:
|
else:
|
||||||
instance.version = instance.history.count()
|
instance.version = instance.history.count()
|
||||||
|
|
||||||
|
|
||||||
|
class VaultSignalHandler(object):
|
||||||
|
""" 处理 Vault 相关的信号 """
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def save_to_vault(sender, instance, created, **kwargs):
|
||||||
|
if created:
|
||||||
|
vault_client.create(instance)
|
||||||
|
else:
|
||||||
|
vault_client.update(instance)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def delete_to_vault(sender, instance, **kwargs):
|
||||||
|
vault_client.delete(instance)
|
||||||
|
|
||||||
|
|
||||||
|
for model in (Account, AccountTemplate, Account.history.model):
|
||||||
|
post_save.connect(VaultSignalHandler.save_to_vault, sender=model)
|
||||||
|
post_delete.connect(VaultSignalHandler.delete_to_vault, sender=model)
|
||||||
|
|
|
@ -0,0 +1,56 @@
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
from celery import shared_task
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from accounts.backends import vault_client
|
||||||
|
from accounts.models import Account, AccountTemplate
|
||||||
|
from common.utils import get_logger
|
||||||
|
from orgs.utils import tmp_to_root_org
|
||||||
|
from ..const import VaultTypeChoices
|
||||||
|
|
||||||
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task(verbose_name=_('Sync secret to vault'))
|
||||||
|
def sync_secret_to_vault():
|
||||||
|
if vault_client.is_type(VaultTypeChoices.local):
|
||||||
|
# 这里不能判断 settings.VAULT_TYPE, 必须判断当前 vault_client 的类型
|
||||||
|
print('\033[35m>>> 当前 Vault 类型为本地数据库, 不需要同步')
|
||||||
|
return
|
||||||
|
|
||||||
|
print('\033[33m>>> 开始同步密钥数据到 Vault ({})'.format(datetime.datetime.now()))
|
||||||
|
with tmp_to_root_org():
|
||||||
|
to_sync_models = [Account, AccountTemplate, Account.history.model]
|
||||||
|
for model in to_sync_models:
|
||||||
|
print(f'\033[33m>>> 开始同步: {model.__module__}')
|
||||||
|
succeeded = []
|
||||||
|
failed = []
|
||||||
|
skipped = []
|
||||||
|
instances = model.objects.all()
|
||||||
|
for instance in instances:
|
||||||
|
instance_desc = f'[{instance}]'
|
||||||
|
if instance.secret_has_save_to_vault:
|
||||||
|
print(f'\033[32m- 跳过同步: {instance_desc}, 原因: [已同步]')
|
||||||
|
skipped.append(instance)
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
vault_client.create(instance)
|
||||||
|
except Exception as e:
|
||||||
|
failed.append(instance)
|
||||||
|
print(f'\033[31m- 同步失败: {instance_desc}, 原因: [{e}]')
|
||||||
|
else:
|
||||||
|
succeeded.append(instance)
|
||||||
|
print(f'\033[32m- 同步成功: {instance_desc}')
|
||||||
|
|
||||||
|
total = len(succeeded) + len(failed) + len(skipped)
|
||||||
|
print(
|
||||||
|
f'\033[33m>>> 同步完成: {model.__module__}, '
|
||||||
|
f'共计: {total}, '
|
||||||
|
f'成功: {len(succeeded)}, '
|
||||||
|
f'失败: {len(failed)}, '
|
||||||
|
f'跳过: {len(skipped)}'
|
||||||
|
)
|
||||||
|
|
||||||
|
print('\033[33m>>> 全部同步完成 ({})'.format(datetime.datetime.now()))
|
||||||
|
print('\033[0m')
|
|
@ -1,9 +1,7 @@
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from accounts.const import (
|
from accounts.const import SecretType, DEFAULT_PASSWORD_RULES
|
||||||
SecretType, DEFAULT_PASSWORD_RULES
|
|
||||||
)
|
|
||||||
from common.utils import ssh_key_gen, random_string
|
from common.utils import ssh_key_gen, random_string
|
||||||
from common.utils import validate_ssh_private_key, parse_ssh_private_key_str
|
from common.utils import validate_ssh_private_key, parse_ssh_private_key_str
|
||||||
|
|
||||||
|
|
|
@ -11,12 +11,12 @@ from django.core.exceptions import ValidationError
|
||||||
from django.core.validators import MinValueValidator, MaxValueValidator
|
from django.core.validators import MinValueValidator, MaxValueValidator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import Q, Manager, QuerySet
|
from django.db.models import Q, Manager, QuerySet
|
||||||
from django.utils.encoding import force_str
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from rest_framework.utils.encoders import JSONEncoder
|
from rest_framework.utils.encoders import JSONEncoder
|
||||||
|
|
||||||
from common.local import add_encrypted_field_set
|
from common.local import add_encrypted_field_set
|
||||||
from common.utils import signer, crypto, contains_ip
|
from common.utils import contains_ip
|
||||||
|
from .utils import Encryptor
|
||||||
from .validators import PortRangeValidator
|
from .validators import PortRangeValidator
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
@ -139,20 +139,11 @@ class EncryptMixin:
|
||||||
EncryptMixin要放在最前面
|
EncryptMixin要放在最前面
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def decrypt_from_signer(self, value):
|
|
||||||
return signer.unsign(value) or ""
|
|
||||||
|
|
||||||
def from_db_value(self, value, expression, connection, context=None):
|
def from_db_value(self, value, expression, connection, context=None):
|
||||||
if value is None:
|
if value is None:
|
||||||
return value
|
return value
|
||||||
|
|
||||||
value = force_str(value)
|
plain_value = Encryptor(value).decrypt()
|
||||||
plain_value = crypto.decrypt(value)
|
|
||||||
|
|
||||||
# 如果没有解开,使用原来的signer解密
|
|
||||||
if not plain_value:
|
|
||||||
plain_value = self.decrypt_from_signer(value)
|
|
||||||
|
|
||||||
# 可能和Json mix,所以要先解密,再json
|
# 可能和Json mix,所以要先解密,再json
|
||||||
sp = super()
|
sp = super()
|
||||||
if hasattr(sp, "from_db_value"):
|
if hasattr(sp, "from_db_value"):
|
||||||
|
@ -167,9 +158,9 @@ class EncryptMixin:
|
||||||
sp = super()
|
sp = super()
|
||||||
if hasattr(sp, "get_prep_value"):
|
if hasattr(sp, "get_prep_value"):
|
||||||
value = sp.get_prep_value(value)
|
value = sp.get_prep_value(value)
|
||||||
value = force_str(value)
|
|
||||||
# 替换新的加密方式
|
# 替换新的加密方式
|
||||||
return crypto.encrypt(value)
|
return Encryptor(value).encrypt()
|
||||||
|
|
||||||
|
|
||||||
class EncryptTextField(EncryptMixin, models.TextField):
|
class EncryptTextField(EncryptMixin, models.TextField):
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
|
|
||||||
from django.db import connections
|
from django.db import connections
|
||||||
|
from django.utils.encoding import force_text
|
||||||
|
|
||||||
from common.utils import get_logger
|
from common.utils import get_logger, signer, crypto
|
||||||
|
|
||||||
logger = get_logger(__file__)
|
logger = get_logger(__file__)
|
||||||
|
|
||||||
|
@ -55,3 +56,19 @@ def safe_db_connection():
|
||||||
close_old_connections()
|
close_old_connections()
|
||||||
yield
|
yield
|
||||||
close_old_connections()
|
close_old_connections()
|
||||||
|
|
||||||
|
|
||||||
|
class Encryptor:
|
||||||
|
def __init__(self, value):
|
||||||
|
self.value = force_text(value)
|
||||||
|
|
||||||
|
def decrypt(self):
|
||||||
|
plain_value = crypto.decrypt(self.value)
|
||||||
|
|
||||||
|
# 如果没有解开,使用原来的signer解密
|
||||||
|
if not plain_value:
|
||||||
|
plain_value = signer.unsign(self.value) or ""
|
||||||
|
return plain_value
|
||||||
|
|
||||||
|
def encrypt(self):
|
||||||
|
return crypto.encrypt(self.value)
|
||||||
|
|
|
@ -67,12 +67,18 @@ class EventLoopThread(threading.Thread):
|
||||||
_loop_thread = EventLoopThread()
|
_loop_thread = EventLoopThread()
|
||||||
_loop_thread.setDaemon(True)
|
_loop_thread.setDaemon(True)
|
||||||
_loop_thread.start()
|
_loop_thread.start()
|
||||||
executor = ThreadPoolExecutor(max_workers=10,
|
executor = ThreadPoolExecutor(
|
||||||
thread_name_prefix='debouncer')
|
max_workers=10,
|
||||||
|
thread_name_prefix='debouncer'
|
||||||
|
)
|
||||||
_loop_debouncer_func_task_cache = {}
|
_loop_debouncer_func_task_cache = {}
|
||||||
_loop_debouncer_func_args_cache = {}
|
_loop_debouncer_func_args_cache = {}
|
||||||
|
|
||||||
|
|
||||||
|
def get_loop():
|
||||||
|
return _loop_thread.get_loop()
|
||||||
|
|
||||||
|
|
||||||
def cancel_or_remove_debouncer_task(cache_key):
|
def cancel_or_remove_debouncer_task(cache_key):
|
||||||
task = _loop_debouncer_func_task_cache.get(cache_key, None)
|
task = _loop_debouncer_func_task_cache.get(cache_key, None)
|
||||||
if not task:
|
if not task:
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import time
|
|
||||||
import hmac
|
|
||||||
import base64
|
import base64
|
||||||
|
import hmac
|
||||||
|
import time
|
||||||
|
|
||||||
from common.utils import get_logger
|
|
||||||
from common.sdk.im.utils import digest, as_request
|
|
||||||
from common.sdk.im.mixin import BaseRequest
|
from common.sdk.im.mixin import BaseRequest
|
||||||
|
from common.sdk.im.utils import digest, as_request
|
||||||
|
from common.utils import get_logger
|
||||||
from users.utils import construct_user_email
|
from users.utils import construct_user_email
|
||||||
|
|
||||||
logger = get_logger(__file__)
|
logger = get_logger(__file__)
|
||||||
|
|
|
@ -251,6 +251,12 @@ class Config(dict):
|
||||||
# 临时密码
|
# 临时密码
|
||||||
'AUTH_TEMP_TOKEN': False,
|
'AUTH_TEMP_TOKEN': False,
|
||||||
|
|
||||||
|
# Vault
|
||||||
|
'VAULT_TYPE': 'local',
|
||||||
|
'VAULT_HCP_HOST': '',
|
||||||
|
'VAULT_HCP_TOKEN': '',
|
||||||
|
'VAULT_HCP_MOUNT_POINT': 'jumpserver',
|
||||||
|
|
||||||
# Auth LDAP settings
|
# Auth LDAP settings
|
||||||
'AUTH_LDAP': False,
|
'AUTH_LDAP': False,
|
||||||
'AUTH_LDAP_SERVER_URI': 'ldap://localhost:389',
|
'AUTH_LDAP_SERVER_URI': 'ldap://localhost:389',
|
||||||
|
|
|
@ -174,6 +174,12 @@ AUTH_OAUTH2_LOGOUT_URL_NAME = "authentication:oauth2:logout"
|
||||||
# 临时 token
|
# 临时 token
|
||||||
AUTH_TEMP_TOKEN = CONFIG.AUTH_TEMP_TOKEN
|
AUTH_TEMP_TOKEN = CONFIG.AUTH_TEMP_TOKEN
|
||||||
|
|
||||||
|
# Vault
|
||||||
|
VAULT_TYPE = CONFIG.VAULT_TYPE
|
||||||
|
VAULT_HCP_HOST = CONFIG.VAULT_HCP_HOST
|
||||||
|
VAULT_HCP_TOKEN = CONFIG.VAULT_HCP_TOKEN
|
||||||
|
VAULT_HCP_MOUNT_POINT = CONFIG.VAULT_HCP_MOUNT_POINT
|
||||||
|
|
||||||
# Other setting
|
# Other setting
|
||||||
# 这个是 User Login Private Token
|
# 这个是 User Login Private Token
|
||||||
TOKEN_EXPIRATION = CONFIG.TOKEN_EXPIRATION
|
TOKEN_EXPIRATION = CONFIG.TOKEN_EXPIRATION
|
||||||
|
|
|
@ -119,3 +119,4 @@ class OrgModelMixin(models.Model):
|
||||||
class JMSOrgBaseModel(JMSBaseModel, OrgModelMixin):
|
class JMSOrgBaseModel(JMSBaseModel, OrgModelMixin):
|
||||||
class Meta:
|
class Meta:
|
||||||
abstract = True
|
abstract = True
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
#
|
#
|
||||||
import uuid
|
import uuid
|
||||||
from inspect import signature
|
|
||||||
from functools import wraps
|
|
||||||
from werkzeug.local import LocalProxy
|
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
|
from functools import wraps
|
||||||
|
from inspect import signature
|
||||||
|
|
||||||
|
from werkzeug.local import LocalProxy
|
||||||
|
|
||||||
from common.local import thread_local
|
from common.local import thread_local
|
||||||
from .models import Organization
|
from .models import Organization
|
||||||
|
@ -133,6 +134,7 @@ def org_aware_func(org_arg_name):
|
||||||
:param org_arg_name: 函数中包含org_id的对象是哪个参数
|
:param org_arg_name: 函数中包含org_id的对象是哪个参数
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def decorate(func):
|
def decorate(func):
|
||||||
@wraps(func)
|
@wraps(func)
|
||||||
def wrapper(*args, **kwargs):
|
def wrapper(*args, **kwargs):
|
||||||
|
@ -149,7 +151,9 @@ def org_aware_func(org_arg_name):
|
||||||
with tmp_to_org(org_aware_resource.org_id):
|
with tmp_to_org(org_aware_resource.org_id):
|
||||||
# print("Current org id: {}".format(org_aware_resource.org_id))
|
# print("Current org id: {}".format(org_aware_resource.org_id))
|
||||||
return func(*args, **kwargs)
|
return func(*args, **kwargs)
|
||||||
|
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
return decorate
|
return decorate
|
||||||
|
|
||||||
|
|
||||||
|
@ -162,4 +166,5 @@ def ensure_in_real_or_default_org(func):
|
||||||
if not current_org or current_org.is_root():
|
if not current_org or current_org.is_root():
|
||||||
raise ValueError('You must in a real or default org!')
|
raise ValueError('You must in a real or default org!')
|
||||||
return func(*args, **kwargs)
|
return func(*args, **kwargs)
|
||||||
|
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
from .settings import *
|
|
||||||
from .ldap import *
|
|
||||||
from .wecom import *
|
|
||||||
from .dingtalk import *
|
from .dingtalk import *
|
||||||
from .feishu import *
|
|
||||||
from .public import *
|
|
||||||
from .email import *
|
from .email import *
|
||||||
|
from .feishu import *
|
||||||
|
from .ldap import *
|
||||||
|
from .public import *
|
||||||
|
from .settings import *
|
||||||
from .sms import *
|
from .sms import *
|
||||||
|
from .vault import *
|
||||||
|
from .wecom import *
|
||||||
|
|
|
@ -50,6 +50,7 @@ class SettingsApi(generics.RetrieveUpdateAPIView):
|
||||||
'huawei': serializers.HuaweiSMSSettingSerializer,
|
'huawei': serializers.HuaweiSMSSettingSerializer,
|
||||||
'cmpp2': serializers.CMPP2SMSSettingSerializer,
|
'cmpp2': serializers.CMPP2SMSSettingSerializer,
|
||||||
'custom': serializers.CustomSMSSettingSerializer,
|
'custom': serializers.CustomSMSSettingSerializer,
|
||||||
|
'vault': serializers.VaultSettingSerializer,
|
||||||
}
|
}
|
||||||
|
|
||||||
rbac_category_permissions = {
|
rbac_category_permissions = {
|
||||||
|
@ -75,6 +76,7 @@ class SettingsApi(generics.RetrieveUpdateAPIView):
|
||||||
'sms': 'settings.change_sms',
|
'sms': 'settings.change_sms',
|
||||||
'alibaba': 'settings.change_sms',
|
'alibaba': 'settings.change_sms',
|
||||||
'tencent': 'settings.change_sms',
|
'tencent': 'settings.change_sms',
|
||||||
|
'vault': 'settings.change_vault',
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
|
|
|
@ -0,0 +1,60 @@
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from django.conf import settings
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.generics import GenericAPIView
|
||||||
|
from rest_framework.views import Response, APIView
|
||||||
|
|
||||||
|
from accounts.tasks.vault import sync_secret_to_vault
|
||||||
|
from accounts.backends import get_vault_client
|
||||||
|
from settings.models import Setting
|
||||||
|
from .. import serializers
|
||||||
|
|
||||||
|
|
||||||
|
class VaultTestingAPI(GenericAPIView):
|
||||||
|
serializer_class = serializers.VaultSettingSerializer
|
||||||
|
rbac_perms = {
|
||||||
|
'POST': 'settings.change_vault'
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_config(self, request):
|
||||||
|
serializer = self.serializer_class(data=request.data)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
data = serializer.validated_data
|
||||||
|
for k, v in data.items():
|
||||||
|
if v:
|
||||||
|
continue
|
||||||
|
# 页面没有传递值, 从 settings 中获取
|
||||||
|
data[k] = getattr(settings, k, None)
|
||||||
|
return data
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
config = self.get_config(request)
|
||||||
|
try:
|
||||||
|
client = get_vault_client(raise_exception=True, **config)
|
||||||
|
ok, error = client.is_active()
|
||||||
|
except Exception as e:
|
||||||
|
ok, error = False, str(e)
|
||||||
|
|
||||||
|
if ok:
|
||||||
|
_status, msg = status.HTTP_200_OK, _('Test success')
|
||||||
|
else:
|
||||||
|
_status, msg = status.HTTP_400_BAD_REQUEST, error
|
||||||
|
|
||||||
|
return Response(status=_status, data={'msg': msg})
|
||||||
|
|
||||||
|
|
||||||
|
class VaultSyncDataAPI(APIView):
|
||||||
|
perm_model = Setting
|
||||||
|
rbac_perms = {
|
||||||
|
'POST': 'settings.change_vault'
|
||||||
|
}
|
||||||
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
task = self._run_task()
|
||||||
|
return Response({'task': task.id}, status=status.HTTP_201_CREATED)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _run_task():
|
||||||
|
task = sync_secret_to_vault.delay()
|
||||||
|
return task
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
# Generated by Django 3.2.19 on 2023-06-30 10:37
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('settings', '0007_migrate_ldap_sync_org_ids'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='setting',
|
||||||
|
options={'permissions': [('change_email', 'Can change email setting'), ('change_auth', 'Can change auth setting'), ('change_vault', 'Can change vault setting'), ('change_systemmsgsubscription', 'Can change system msg sub setting'), ('change_sms', 'Can change sms setting'), ('change_security', 'Can change security setting'), ('change_clean', 'Can change clean setting'), ('change_interface', 'Can change interface setting'), ('change_license', 'Can change license setting'), ('change_terminal', 'Can change terminal setting'), ('change_other', 'Can change other setting')], 'verbose_name': 'System setting'},
|
||||||
|
),
|
||||||
|
]
|
|
@ -159,6 +159,7 @@ class Setting(models.Model):
|
||||||
permissions = [
|
permissions = [
|
||||||
('change_email', _('Can change email setting')),
|
('change_email', _('Can change email setting')),
|
||||||
('change_auth', _('Can change auth setting')),
|
('change_auth', _('Can change auth setting')),
|
||||||
|
('change_vault', _('Can change vault setting')),
|
||||||
('change_systemmsgsubscription', _('Can change system msg sub setting')),
|
('change_systemmsgsubscription', _('Can change system msg sub setting')),
|
||||||
('change_sms', _('Can change sms setting')),
|
('change_sms', _('Can change sms setting')),
|
||||||
('change_security', _('Can change security setting')),
|
('change_security', _('Can change security setting')),
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
#
|
#
|
||||||
|
|
||||||
from .basic import *
|
|
||||||
from .auth import *
|
from .auth import *
|
||||||
from .email import *
|
from .basic import *
|
||||||
from .public import *
|
|
||||||
from .settings import *
|
|
||||||
from .security import *
|
|
||||||
from .terminal import *
|
|
||||||
from .cleaning import *
|
from .cleaning import *
|
||||||
|
from .email import *
|
||||||
from .other import *
|
from .other import *
|
||||||
|
from .public import *
|
||||||
|
from .security import *
|
||||||
|
from .settings import *
|
||||||
|
from .terminal import *
|
||||||
|
from .vault import *
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from accounts.const import VaultTypeChoices
|
||||||
|
from common.serializers.fields import EncryptedField
|
||||||
|
|
||||||
|
__all__ = ['VaultSettingSerializer']
|
||||||
|
|
||||||
|
|
||||||
|
class VaultSettingSerializer(serializers.Serializer):
|
||||||
|
VAULT_TYPE = serializers.ChoiceField(
|
||||||
|
default=VaultTypeChoices.local, choices=VaultTypeChoices.choices,
|
||||||
|
required=False, label=_('Type')
|
||||||
|
)
|
||||||
|
VAULT_HCP_HOST = serializers.CharField(
|
||||||
|
max_length=256, allow_blank=True, required=False, label=_('Host')
|
||||||
|
)
|
||||||
|
VAULT_HCP_TOKEN = EncryptedField(
|
||||||
|
max_length=256, allow_blank=True, required=False, label=_('Token')
|
||||||
|
)
|
||||||
|
VAULT_HCP_MOUNT_POINT = serializers.CharField(
|
||||||
|
max_length=256, allow_blank=True, required=False, label=_('Mount Point')
|
||||||
|
)
|
|
@ -18,6 +18,8 @@ urlpatterns = [
|
||||||
path('feishu/testing/', api.FeiShuTestingAPI.as_view(), name='feishu-testing'),
|
path('feishu/testing/', api.FeiShuTestingAPI.as_view(), name='feishu-testing'),
|
||||||
path('sms/<str:backend>/testing/', api.SMSTestingAPI.as_view(), name='sms-testing'),
|
path('sms/<str:backend>/testing/', api.SMSTestingAPI.as_view(), name='sms-testing'),
|
||||||
path('sms/backend/', api.SMSBackendAPI.as_view(), name='sms-backend'),
|
path('sms/backend/', api.SMSBackendAPI.as_view(), name='sms-backend'),
|
||||||
|
path('vault/testing/', api.VaultTestingAPI.as_view(), name='vault-testing'),
|
||||||
|
path('vault/sync/', api.VaultSyncDataAPI.as_view(), name='vault-sync'),
|
||||||
|
|
||||||
path('setting/', api.SettingsApi.as_view(), name='settings-setting'),
|
path('setting/', api.SettingsApi.as_view(), name='settings-setting'),
|
||||||
path('logo/', api.SettingsLogoApi.as_view(), name='settings-logo'),
|
path('logo/', api.SettingsLogoApi.as_view(), name='settings-logo'),
|
||||||
|
|
|
@ -133,3 +133,5 @@ ipython==8.14.0
|
||||||
ForgeryPy3==0.3.1
|
ForgeryPy3==0.3.1
|
||||||
django-debug-toolbar==4.1.0
|
django-debug-toolbar==4.1.0
|
||||||
Pympler==1.0.1
|
Pympler==1.0.1
|
||||||
|
hvac==1.1.1
|
||||||
|
pyhcl==0.4.4
|
||||||
|
|
Loading…
Reference in New Issue