Merge remote-tracking branch 'origin/v3' into v3

This commit is contained in:
Aaron3S 2022-11-16 15:05:38 +08:00
commit e7dde616c0
10 changed files with 127 additions and 140 deletions

View File

@ -1,65 +1,77 @@
FROM python:3.8-slim as stage-build
ARG TARGETARCH
ARG VERSION
ENV VERSION=$VERSION
WORKDIR /opt/jumpserver
ADD . .
RUN cd utils && bash -ixeu build.sh
FROM python:3.8-slim FROM python:3.8-slim
ARG TARGETARCH
MAINTAINER JumpServer Team <ibuler@qq.com> MAINTAINER JumpServer Team <ibuler@qq.com>
ARG BUILD_DEPENDENCIES=" \ ARG BUILD_DEPENDENCIES=" \
g++ \ g++ \
make \ make \
pkg-config" pkg-config"
ARG DEPENDENCIES=" \ ARG DEPENDENCIES=" \
default-libmysqlclient-dev \ default-libmysqlclient-dev \
freetds-dev \ freetds-dev \
libpq-dev \ libpq-dev \
libffi-dev \ libffi-dev \
libldap2-dev \ libjpeg-dev \
libsasl2-dev \ libldap2-dev \
libxml2-dev \ libsasl2-dev \
libxmlsec1-dev \ libxml2-dev \
libxmlsec1-openssl \ libxmlsec1-dev \
libaio-dev \ libxmlsec1-openssl \
openssh-client \ libaio-dev \
sshpass" openssh-client \
sshpass"
ARG TOOLS=" \ ARG TOOLS=" \
curl \ ca-certificates \
default-mysql-client \ curl \
iproute2 \ default-mysql-client \
iputils-ping \ iputils-ping \
locales \ locales \
procps \ procps \
redis-tools \ redis-tools \
telnet \ telnet \
vim \ vim \
unzip \ unzip \
wget" wget"
RUN sed -i 's@http://.*.debian.org@http://mirrors.ustc.edu.cn@g' /etc/apt/sources.list \ ARG APT_MIRROR=http://mirrors.ustc.edu.cn
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core \
sed -i "s@http://.*.debian.org@${APT_MIRROR}@g" /etc/apt/sources.list \
&& rm -f /etc/apt/apt.conf.d/docker-clean \
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& apt-get update \ && apt-get update \
&& apt-get -y install --no-install-recommends ${BUILD_DEPENDENCIES} \ && apt-get -y install --no-install-recommends ${BUILD_DEPENDENCIES} \
&& apt-get -y install --no-install-recommends ${DEPENDENCIES} \ && apt-get -y install --no-install-recommends ${DEPENDENCIES} \
&& apt-get -y install --no-install-recommends ${TOOLS} \ && apt-get -y install --no-install-recommends ${TOOLS} \
&& localedef -c -f UTF-8 -i zh_CN zh_CN.UTF-8 \
&& cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& mkdir -p /root/.ssh/ \ && mkdir -p /root/.ssh/ \
&& echo "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null" > /root/.ssh/config \ && echo "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null" > /root/.ssh/config \
&& sed -i "s@# alias l@alias l@g" ~/.bashrc \ && sed -i "s@# alias l@alias l@g" ~/.bashrc \
&& echo "set mouse-=a" > ~/.vimrc \ && echo "set mouse-=a" > ~/.vimrc \
&& echo "no" | dpkg-reconfigure dash \ && echo "no" | dpkg-reconfigure dash \
&& echo "zh_CN.UTF-8" | dpkg-reconfigure locales \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
ARG TARGETARCH ARG DOWNLOAD_URL=https://download.jumpserver.org
ARG ORACLE_LIB_MAJOR=19
ARG ORACLE_LIB_MINOR=10
ENV ORACLE_FILE="instantclient-basiclite-linux.${TARGETARCH:-amd64}-${ORACLE_LIB_MAJOR}.${ORACLE_LIB_MINOR}.0.0.0dbru.zip"
RUN mkdir -p /opt/oracle/ \ RUN mkdir -p /opt/oracle/ \
&& cd /opt/oracle/ \ && cd /opt/oracle/ \
&& wget https://download.jumpserver.org/files/oracle/${ORACLE_FILE} \ && wget ${DOWNLOAD_URL}/public/instantclient-basiclite-linux.${TARGETARCH}-19.10.0.0.0.zip \
&& unzip instantclient-basiclite-linux.${TARGETARCH-amd64}-19.10.0.0.0dbru.zip \ && unzip instantclient-basiclite-linux.${TARGETARCH}-19.10.0.0.0.zip \
&& mv instantclient_${ORACLE_LIB_MAJOR}_${ORACLE_LIB_MINOR} instantclient \ && sh -c "echo /opt/oracle/instantclient_19_10 > /etc/ld.so.conf.d/oracle-instantclient.conf" \
&& echo "/opt/oracle/instantclient" > /etc/ld.so.conf.d/oracle-instantclient.conf \
&& ldconfig \ && ldconfig \
&& rm -f ${ORACLE_FILE} && rm -f instantclient-basiclite-linux.${TARGETARCH}-19.10.0.0.0.zip
WORKDIR /tmp/build WORKDIR /tmp/build
COPY ./requirements ./requirements COPY ./requirements ./requirements
@ -68,21 +80,18 @@ ARG PIP_MIRROR=https://pypi.douban.com/simple
ENV PIP_MIRROR=$PIP_MIRROR ENV PIP_MIRROR=$PIP_MIRROR
ARG PIP_JMS_MIRROR=https://pypi.douban.com/simple ARG PIP_JMS_MIRROR=https://pypi.douban.com/simple
ENV PIP_JMS_MIRROR=$PIP_JMS_MIRROR ENV PIP_JMS_MIRROR=$PIP_JMS_MIRROR
# 因为以 jms 或者 jumpserver 开头的 mirror 上可能没有
RUN pip install --upgrade pip==20.2.4 setuptools==49.6.0 wheel==0.34.2 -i ${PIP_MIRROR} \
&& pip install --no-cache-dir $(grep -E 'jms|jumpserver' requirements/requirements.txt) -i ${PIP_JMS_MIRROR} \
&& pip install --no-cache-dir -r requirements/requirements.txt -i ${PIP_MIRROR} \
&& rm -rf ~/.cache/pip
ARG VERSION RUN --mount=type=cache,target=/root/.cache/pip \
ENV VERSION=$VERSION set -ex \
&& pip config set global.index-url ${PIP_MIRROR} \
&& pip install --upgrade pip \
&& pip install --upgrade setuptools wheel \
&& pip install $(grep -E 'jms|jumpserver' requirements/requirements.txt) -i ${PIP_JMS_MIRROR} \
&& pip install -r requirements/requirements.txt
ADD . . COPY --from=stage-build /opt/jumpserver/release/jumpserver /opt/jumpserver
RUN cd utils \ RUN echo > /opt/jumpserver/config.yml \
&& bash -ixeu build.sh \ && rm -rf /tmp/build
&& mv ../release/jumpserver /opt/jumpserver \
&& rm -rf /tmp/build \
&& echo > /opt/jumpserver/config.yml
WORKDIR /opt/jumpserver WORKDIR /opt/jumpserver
VOLUME /opt/jumpserver/data VOLUME /opt/jumpserver/data

View File

@ -19,7 +19,6 @@ __all__ = [
'UserGroupGrantedAssetsApi', 'UserGroupGrantedNodesApi', 'UserGroupGrantedAssetsApi', 'UserGroupGrantedNodesApi',
'UserGroupGrantedNodeAssetsApi', 'UserGroupGrantedNodeAssetsApi',
'UserGroupGrantedNodeChildrenAsTreeApi', 'UserGroupGrantedNodeChildrenAsTreeApi',
'UserGroupGrantedAssetAccountsApi',
] ]
@ -191,17 +190,3 @@ class UserGroupGrantedNodeChildrenAsTreeApi(SerializeToTreeNodeMixin, ListAPIVie
nodes = self.get_nodes() nodes = self.get_nodes()
nodes = self.serialize_nodes(nodes) nodes = self.serialize_nodes(nodes)
return Response(data=nodes) return Response(data=nodes)
class UserGroupGrantedAssetAccountsApi(uapi.UserGrantedAssetAccountsApi):
@lazyproperty
def user_group(self):
group_id = self.kwargs.get('pk')
return UserGroup.objects.get(id=group_id)
def get_queryset(self):
accounts = PermAccountUtil().get_perm_accounts_for_user_group_asset(
self.user_group, self.asset, with_actions=True
)
return accounts

View File

@ -1,7 +1,7 @@
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from rest_framework.generics import ListAPIView, get_object_or_404 from rest_framework.generics import ListAPIView, get_object_or_404
from common.utils import get_logger from common.utils import get_logger, lazyproperty
from perms import serializers from perms import serializers
from perms.hands import Asset from perms.hands import Asset
from perms.utils import PermAccountUtil from perms.utils import PermAccountUtil
@ -10,14 +10,14 @@ from .mixin import SelfOrPKUserMixin
logger = get_logger(__name__) logger = get_logger(__name__)
__all__ = [ __all__ = [
'UserGrantedAssetAccountsApi', 'UserPermedAssetAccountsApi',
] ]
class UserGrantedAssetAccountsApi(SelfOrPKUserMixin, ListAPIView): class UserPermedAssetAccountsApi(SelfOrPKUserMixin, ListAPIView):
serializer_class = serializers.AccountsGrantedSerializer serializer_class = serializers.AccountsPermedSerializer
@property @lazyproperty
def asset(self): def asset(self):
asset_id = self.kwargs.get('asset_id') asset_id = self.kwargs.get('asset_id')
kwargs = {'id': asset_id, 'is_active': True} kwargs = {'id': asset_id, 'is_active': True}

View File

@ -2,8 +2,11 @@
# #
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from rest_framework.request import Request from rest_framework.request import Request
from django.utils.translation import ugettext_lazy as _
from common.http import is_true from common.http import is_true
from common.utils import is_uuid
from common.exceptions import JMSObjectDoesNotExist
from common.mixins.api import RoleAdminMixin, RoleUserMixin from common.mixins.api import RoleAdminMixin, RoleUserMixin
from perms.utils.user_permission import UserGrantedTreeRefreshController from perms.utils.user_permission import UserGrantedTreeRefreshController
from rbac.permissions import RBACPermission from rbac.permissions import RBACPermission
@ -43,6 +46,12 @@ class SelfOrPKUserMixin:
request: Request request: Request
permission_classes = (RBACPermission,) permission_classes = (RBACPermission,)
def get_rbac_perms(self):
if self.request_user_is_self():
return self.self_rbac_perms
else:
return self.admin_rbac_perms
@property @property
def self_rbac_perms(self): def self_rbac_perms(self):
return ( return (
@ -61,18 +70,15 @@ class SelfOrPKUserMixin:
('GET', 'perms.view_userassets'), ('GET', 'perms.view_userassets'),
) )
def get_rbac_perms(self):
if self.request_user_is_self():
return self.self_rbac_perms
else:
return self.admin_rbac_perms
def request_user_is_self(self):
return self.kwargs.get('user') in ['my', 'self']
@property @property
def user(self): def user(self):
if self.request_user_is_self(): if self.request_user_is_self():
return self.request.user user = self.request.user
elif is_uuid(self.kwargs.get('user')):
user = get_object_or_404(User, pk=self.kwargs.get('user'))
else: else:
return get_object_or_404(User, pk=self.kwargs.get('user')) raise JMSObjectDoesNotExist(object_name=_('User'))
return user
def request_user_is_self(self):
return self.kwargs.get('user') in ['my', 'self']

View File

@ -6,15 +6,15 @@ from django.utils.translation import ugettext_lazy as _
from common.db.fields import BitChoices from common.db.fields import BitChoices
from common.utils.integer import bit from common.utils.integer import bit
__all__ = ["SpecialAccount", "ActionChoices"] __all__ = ["ActionChoices"]
class ActionChoices(BitChoices): class ActionChoices(BitChoices):
connect = bit(1), _("Connect") connect = bit(0), _("Connect")
upload = bit(2), _("Upload") upload = bit(1), _("Upload")
download = bit(3), _("Download") download = bit(2), _("Download")
copy = bit(4), _("Copy") copy = bit(3), _("Copy")
paste = bit(5), _("Paste") paste = bit(4), _("Paste")
@classmethod @classmethod
def is_tree(cls): def is_tree(cls):
@ -23,6 +23,7 @@ class ActionChoices(BitChoices):
@classmethod @classmethod
def branches(cls): def branches(cls):
return ( return (
cls.connect,
(_("Transfer"), [cls.upload, cls.download]), (_("Transfer"), [cls.upload, cls.download]),
(_("Clipboard"), [cls.copy, cls.paste]), (_("Clipboard"), [cls.copy, cls.paste]),
) )
@ -31,7 +32,3 @@ class ActionChoices(BitChoices):
def has_perm(cls, action_name, total): def has_perm(cls, action_name, total):
action_value = getattr(cls, action_name) action_value = getattr(cls, action_name)
return action_value & total == action_value return action_value & total == action_value
class SpecialAccount(models.TextChoices):
ALL = "@ALL", "All"

View File

@ -11,7 +11,7 @@ from common.db.models import UnionQuerySet
from common.utils import date_expired_default from common.utils import date_expired_default
from orgs.mixins.models import OrgManager from orgs.mixins.models import OrgManager
from orgs.mixins.models import OrgModelMixin from orgs.mixins.models import OrgModelMixin
from perms.const import ActionChoices, SpecialAccount from perms.const import ActionChoices
__all__ = ['AssetPermission', 'ActionChoices'] __all__ = ['AssetPermission', 'ActionChoices']
@ -37,7 +37,7 @@ class AssetPermissionQuerySet(models.QuerySet):
def filter_by_accounts(self, accounts): def filter_by_accounts(self, accounts):
q = Q(accounts__contains=list(accounts)) | \ q = Q(accounts__contains=list(accounts)) | \
Q(accounts__contains=SpecialAccount.ALL.value) Q(accounts__contains=Account.AliasAccount.ALL.value)
return self.filter(q) return self.filter(q)
@ -127,7 +127,7 @@ class AssetPermission(OrgModelMixin):
""" """
asset_ids = self.get_all_assets(flat=True) asset_ids = self.get_all_assets(flat=True)
q = Q(asset_id__in=asset_ids) q = Q(asset_id__in=asset_ids)
if SpecialAccount.ALL in self.accounts: if Account.AliasAccount.ALL in self.accounts:
q &= Q(username__in=self.accounts) q &= Q(username__in=self.accounts)
accounts = Account.objects.filter(q).order_by('asset__name', 'name', 'username') accounts = Account.objects.filter(q).order_by('asset__name', 'name', 'username')
if not flat: if not flat:

View File

@ -11,7 +11,7 @@ from perms.serializers.permission import ActionChoicesField
__all__ = [ __all__ = [
'NodeGrantedSerializer', 'AssetGrantedSerializer', 'NodeGrantedSerializer', 'AssetGrantedSerializer',
'ActionsSerializer', 'AccountsGrantedSerializer' 'ActionsSerializer', 'AccountsPermedSerializer'
] ]
@ -48,7 +48,7 @@ class ActionsSerializer(serializers.Serializer):
actions = ActionChoicesField(read_only=True) actions = ActionChoicesField(read_only=True)
class AccountsGrantedSerializer(serializers.ModelSerializer): class AccountsPermedSerializer(serializers.ModelSerializer):
actions = ActionChoicesField(read_only=True) actions = ActionChoicesField(read_only=True)
class Meta: class Meta:

View File

@ -55,8 +55,9 @@ user_permission_urlpatterns = [
name='my-ungrouped-assets'), name='my-ungrouped-assets'),
# 获取授权给用户某个资产的所有账号 # 获取授权给用户某个资产的所有账号
path('<str:user>/assets/<uuid:asset_id>/accounts/', api.UserGrantedAssetAccountsApi.as_view(), # user params: ['my', 'self'] or user.id
name='user-asset-accounts'), path('<str:user>/assets/<uuid:asset_id>/accounts/', api.UserPermedAssetAccountsApi.as_view(),
name='user-permed-asset-accounts'),
] ]
user_group_permission_urlpatterns = [ user_group_permission_urlpatterns = [
@ -68,10 +69,6 @@ user_group_permission_urlpatterns = [
name='user-group-nodes-children-as-tree'), name='user-group-nodes-children-as-tree'),
path('<uuid:pk>/nodes/<uuid:node_id>/assets/', api.UserGroupGrantedNodeAssetsApi.as_view(), path('<uuid:pk>/nodes/<uuid:node_id>/assets/', api.UserGroupGrantedNodeAssetsApi.as_view(),
name='user-group-node-assets'), name='user-group-node-assets'),
# 获取所有和资产-用户组关联的账号列表
path('<uuid:pk>/assets/<uuid:asset_id>/accounts/', api.UserGroupGrantedAssetAccountsApi.as_view(),
name='user-group-asset-accounts'),
] ]
user_permission_urlpatterns = [ user_permission_urlpatterns = [

View File

@ -9,8 +9,28 @@ __all__ = ['PermAccountUtil']
class PermAccountUtil(AssetPermissionUtil): class PermAccountUtil(AssetPermissionUtil):
""" 资产授权账号相关的工具 """ """ 资产授权账号相关的工具 """
def validate_permission(self, user, asset, account_username):
""" 校验用户有某个资产下某个账号名的权限
:param user: User
:param asset: Asset
:param account_username: 可能是 @USER @INPUT 字符串
"""
permed_accounts = self.get_permed_accounts_for_user(user, asset)
accounts_mapper = {account.username: account for account in permed_accounts}
account = accounts_mapper.get(account_username)
actions, date_expired = (account.actions, account.date_expired) if account else (False, None)
return actions, date_expired
def get_permed_accounts_for_user(self, user, asset):
""" 获取授权给用户某个资产的账号 """
perms = self.get_permissions_for_user_asset(user, asset)
permed_accounts = self.get_permed_accounts_from_perms(perms, user, asset)
return permed_accounts
@staticmethod @staticmethod
def get_permed_accounts_from_perms(perms, user, asset): def get_permed_accounts_from_perms(perms, user, asset):
# alias: is a collection of account usernames and special accounts [@ALL, @INPUT, @USER]
alias_action_bit_mapper = defaultdict(int) alias_action_bit_mapper = defaultdict(int)
alias_expired_mapper = defaultdict(list) alias_expired_mapper = defaultdict(list)
@ -21,23 +41,26 @@ class PermAccountUtil(AssetPermissionUtil):
asset_accounts = asset.accounts.all() asset_accounts = asset.accounts.all()
username_account_mapper = {account.username: account for account in asset_accounts} username_account_mapper = {account.username: account for account in asset_accounts}
cleaned_accounts_action_bit = defaultdict(int) cleaned_accounts_action_bit = defaultdict(int)
cleaned_accounts_expired = defaultdict(list) cleaned_accounts_expired = defaultdict(list)
# @ALL 账号先处理,后面的每个最多映射一个账号 # @ALL 账号先处理,后面的每个最多映射一个账号
all_action_bit = alias_action_bit_mapper.pop('@ALL', None) all_action_bit = alias_action_bit_mapper.pop(Account.AliasAccount.ALL, None)
if all_action_bit: if all_action_bit:
for account in asset_accounts: for account in asset_accounts:
cleaned_accounts_action_bit[account] |= all_action_bit cleaned_accounts_action_bit[account] |= all_action_bit
cleaned_accounts_expired[account].extend(alias_expired_mapper['@ALL']) cleaned_accounts_expired[account].extend(
alias_expired_mapper[Account.AliasAccount.ALL]
)
for alias, action_bit in alias_action_bit_mapper.items(): for alias, action_bit in alias_action_bit_mapper.items():
if alias == '@USER': if alias == Account.AliasAccount.USER:
if user.username in username_account_mapper: if user.username in username_account_mapper:
account = username_account_mapper[user.username] account = username_account_mapper[user.username]
else: else:
account = Account.get_user_account(user.username) account = Account.get_user_account(user.username)
elif alias == '@INPUT': elif alias == Account.AliasAccount.INPUT:
account = Account.get_manual_account() account = Account.get_manual_account()
elif alias in username_account_mapper: elif alias in username_account_mapper:
account = username_account_mapper[alias] account = username_account_mapper[alias]
@ -54,35 +77,3 @@ class PermAccountUtil(AssetPermissionUtil):
account.date_expired = max(cleaned_accounts_expired[account]) account.date_expired = max(cleaned_accounts_expired[account])
accounts.append(account) accounts.append(account)
return accounts return accounts
def get_permed_accounts_for_user(self, user, asset):
""" 获取授权给用户某个资产的账号 """
perms = self.get_permissions_for_user_asset(user, asset)
permed_accounts = self.get_permed_accounts_from_perms(perms, user, asset)
return permed_accounts
@staticmethod
def get_accounts_for_permission(perm, with_actions=False):
""" 获取授权规则包含的账号 """
aid_actions_map = defaultdict(int)
# 这里不行,速度太慢, 别情有很多查询
account_ids = perm.get_all_accounts(flat=True)
actions = perm.actions
for aid in account_ids:
aid_actions_map[str(aid)] |= actions
account_ids = list(aid_actions_map.keys())
accounts = Account.objects.filter(id__in=account_ids)
return accounts
def validate_permission(self, user, asset, account_username):
""" 校验用户有某个资产下某个账号名的权限
:param account_username: 可能是 @USER @INPUT
"""
permed_accounts = self.get_permed_accounts_for_user(user, asset)
accounts_mapper = {account.username: account for account in permed_accounts}
account = accounts_mapper.get(account_username)
if not account:
return False, None
else:
return account.actions, account.date_expired

View File

@ -11,6 +11,9 @@ action="${1-start}"
service="${2-all}" service="${2-all}"
trap cleanup EXIT trap cleanup EXIT
rm -f /opt/jumpserver/tmp/*.pid
if [[ "$action" == "bash" || "$action" == "sh" ]];then if [[ "$action" == "bash" || "$action" == "sh" ]];then
bash bash
elif [[ "$action" == "sleep" ]];then elif [[ "$action" == "sleep" ]];then
@ -19,4 +22,3 @@ elif [[ "$action" == "sleep" ]];then
else else
python jms "${action}" "${service}" python jms "${action}" "${service}"
fi fi