diff --git a/apps/assets/models/cmd_filter.py b/apps/assets/models/cmd_filter.py index e26393fff..be8945c55 100644 --- a/apps/assets/models/cmd_filter.py +++ b/apps/assets/models/cmd_filter.py @@ -180,9 +180,10 @@ class CommandFilterRule(OrgModelMixin): @classmethod def get_queryset( - cls, user_id=None, user_group_id=None, system_user_id=None, + cls, user_id=None, user_group_id=None, account=None, asset_id=None, org_id=None ): + from perms.models.const import SpecialAccount user_groups = [] user = get_object_or_none(User, pk=user_id) if user: @@ -191,7 +192,7 @@ class CommandFilterRule(OrgModelMixin): if user_group: org_id = user_group.org_id user_groups.append(user_group) - account = get_object_or_none(Account, pk=system_user_id) + asset = get_object_or_none(Asset, pk=asset_id) q = Q() if user: @@ -200,7 +201,8 @@ class CommandFilterRule(OrgModelMixin): q |= Q(user_groups__in=set(user_groups)) if account: org_id = account.org_id - q |= Q(accounts=account) + q |= Q(accounts__contains=list(account)) |\ + Q(accounts__contains=SpecialAccount.ALL.value) if asset: org_id = asset.org_id q |= Q(assets=asset) diff --git a/apps/authentication/api/connection_token.py b/apps/authentication/api/connection_token.py index f85b1ee54..a67fc92bd 100644 --- a/apps/authentication/api/connection_token.py +++ b/apps/authentication/api/connection_token.py @@ -1,6 +1,7 @@ import abc import os import json +import time import base64 import urllib.parse from django.http import HttpResponse @@ -41,17 +42,25 @@ class ConnectionTokenMixin: def get_request_resources(self, serializer): user = self.get_request_resource_user(serializer) asset = serializer.validated_data.get('asset') - application = serializer.validated_data.get('application') - system_user = serializer.validated_data.get('system_user') - return user, asset, application, system_user + account = serializer.validated_data.get('account') + return user, asset, account @staticmethod - def check_user_has_resource_permission(user, asset, application, system_user): - from perms.utils.asset import has_asset_system_permission + def check_user_has_resource_permission(user, asset, account): + from perms.utils.account import PermAccountUtil + if not asset or not user: + error = '' + raise PermissionDenied(error) - if asset and not has_asset_system_permission(user, asset, system_user): - error = f'User not has this asset and system user permission: ' \ - f'user={user.id} system_user={system_user.id} asset={asset.id}' + actions, expire_at = PermAccountUtil().validate_permission( + user, asset, account_username=account + ) + if not actions: + error = '' + raise PermissionDenied(error) + + if expire_at < time.time(): + error = '' raise PermissionDenied(error) def get_smart_endpoint(self, protocol, asset=None, application=None): @@ -69,13 +78,12 @@ class ConnectionTokenMixin: return true_value if is_true(os.getenv(env_key, env_default)) else false_value def get_client_protocol_data(self, token: ConnectionToken): - from assets.models import SystemUser - protocol = token.system_user.protocol + protocol = token.protocol username = token.user.username rdp_config = ssh_token = '' - if protocol == SystemUser.Protocol.rdp: + if protocol == 'rdp': filename, rdp_config = self.get_rdp_file_info(token) - elif protocol == SystemUser.Protocol.ssh: + elif protocol == 'ssh': filename, ssh_token = self.get_ssh_token(token) else: raise ValueError('Protocol not support: {}'.format(protocol)) @@ -134,15 +142,12 @@ class ConnectionTokenMixin: rdp_options['screen mode id:i'] = '2' if full_screen else '1' # 设置 RDP Server 地址 - endpoint = self.get_smart_endpoint( - protocol='rdp', asset=token.asset, application=token.application - ) + endpoint = self.get_smart_endpoint(protocol='rdp', asset=token.asset) rdp_options['full address:s'] = f'{endpoint.host}:{endpoint.rdp_port}' # 设置用户名 rdp_options['username:s'] = '{}|{}'.format(token.user.username, str(token.id)) - if token.system_user.ad_domain: - rdp_options['domain:s'] = token.system_user.ad_domain + # rdp_options['domain:s'] = token.account_ad_domain # 设置宽高 height = self.request.query_params.get('height') @@ -158,13 +163,12 @@ class ConnectionTokenMixin: if token.asset: name = token.asset.name - elif token.application and token.application.category_remote_app: - app = '||jmservisor' - name = token.application.name - rdp_options['remoteapplicationmode:i'] = '1' - rdp_options['alternate shell:s'] = app - rdp_options['remoteapplicationprogram:s'] = app - rdp_options['remoteapplicationname:s'] = name + # remote-app + # app = '||jmservisor' + # rdp_options['remoteapplicationmode:i'] = '1' + # rdp_options['alternate shell:s'] = app + # rdp_options['remoteapplicationprogram:s'] = app + # rdp_options['remoteapplicationname:s'] = name else: name = '*' prefix_name = f'{token.user.username}-{name}' @@ -188,16 +192,12 @@ class ConnectionTokenMixin: def get_ssh_token(self, token: ConnectionToken): if token.asset: name = token.asset.name - elif token.application: - name = token.application.name else: name = '*' prefix_name = f'{token.user.username}-{name}' filename = self.get_connect_filename(prefix_name) - endpoint = self.get_smart_endpoint( - protocol='ssh', asset=token.asset, application=token.application - ) + endpoint = self.get_smart_endpoint(protocol='ssh', asset=token.asset) data = { 'ip': endpoint.host, 'port': str(endpoint.ssh_port), @@ -251,8 +251,8 @@ class ConnectionTokenViewSet(ConnectionTokenMixin, RootOrgViewMixin, JMSModelVie return token def perform_create(self, serializer): - user, asset, application, system_user = self.get_request_resources(serializer) - self.check_user_has_resource_permission(user, asset, application, system_user) + user, asset, account = self.get_request_resources(serializer) + self.check_user_has_resource_permission(user, asset, account) return super(ConnectionTokenViewSet, self).perform_create(serializer) @action(methods=['POST'], detail=False, url_path='secret-info/detail') @@ -264,7 +264,6 @@ class ConnectionTokenViewSet(ConnectionTokenMixin, RootOrgViewMixin, JMSModelVie token_id = request.data.get('token') or '' token = get_object_or_404(ConnectionToken, pk=token_id) self.check_token_valid(token) - token.load_system_user_auth() serializer = self.get_serializer(instance=token) return Response(serializer.data, status=status.HTTP_200_OK) diff --git a/apps/authentication/models.py b/apps/authentication/models.py deleted file mode 100644 index 765f38ef9..000000000 --- a/apps/authentication/models.py +++ /dev/null @@ -1,248 +0,0 @@ -import uuid -from datetime import datetime, timedelta -from django.utils import timezone -from django.utils.translation import ugettext_lazy as _ -from django.conf import settings -from rest_framework.authtoken.models import Token -from orgs.mixins.models import OrgModelMixin - -from django.db import models -from common.utils import lazyproperty -from common.utils.timezone import as_current_tz -from common.db.models import BaseCreateUpdateModel, JMSBaseModel - - -class AccessKey(models.Model): - id = models.UUIDField(verbose_name='AccessKeyID', primary_key=True, - default=uuid.uuid4, editable=False) - secret = models.UUIDField(verbose_name='AccessKeySecret', - default=uuid.uuid4, editable=False) - user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name='User', - on_delete=models.CASCADE, related_name='access_keys') - is_active = models.BooleanField(default=True, verbose_name=_('Active')) - date_created = models.DateTimeField(auto_now_add=True) - - def get_id(self): - return str(self.id) - - def get_secret(self): - return str(self.secret) - - def get_full_value(self): - return '{}:{}'.format(self.id, self.secret) - - def __str__(self): - return str(self.id) - - class Meta: - verbose_name = _("Access key") - - -class PrivateToken(Token): - """Inherit from auth token, otherwise migration is boring""" - - class Meta: - verbose_name = _('Private Token') - - -class SSOToken(BaseCreateUpdateModel): - """ - 类似腾讯企业邮的 [单点登录](https://exmail.qq.com/qy_mng_logic/doc#10036) - 出于安全考虑,这里的 `token` 使用一次随即过期。但我们保留每一个生成过的 `token`。 - """ - authkey = models.UUIDField(primary_key=True, default=uuid.uuid4, verbose_name=_('Token')) - expired = models.BooleanField(default=False, verbose_name=_('Expired')) - user = models.ForeignKey('users.User', on_delete=models.CASCADE, verbose_name=_('User'), db_constraint=False) - - class Meta: - verbose_name = _('SSO token') - - -def date_expired_default(): - return timezone.now() + timedelta(seconds=settings.CONNECTION_TOKEN_EXPIRATION) - - -class ConnectionToken(OrgModelMixin, JMSBaseModel): - secret = models.CharField(max_length=64, default='', verbose_name=_("Secret")) - date_expired = models.DateTimeField( - default=date_expired_default, verbose_name=_("Date expired") - ) - - user = models.ForeignKey( - 'users.User', on_delete=models.SET_NULL, verbose_name=_('User'), - related_name='connection_tokens', null=True, blank=True - ) - user_display = models.CharField(max_length=128, default='', verbose_name=_("User display")) - asset = models.ForeignKey( - 'assets.Asset', on_delete=models.SET_NULL, verbose_name=_('Asset'), - related_name='connection_tokens', null=True, blank=True - ) - asset_display = models.CharField(max_length=128, default='', verbose_name=_("Asset display")) - account = models.CharField(max_length=128, default='', verbose_name=_("Account")) - - class Meta: - ordering = ('-date_expired',) - verbose_name = _('Connection token') - permissions = [ - ('view_connectiontokensecret', _('Can view connection token secret')) - ] - - @classmethod - def get_default_date_expired(cls): - return date_expired_default() - - @property - def is_expired(self): - return self.date_expired < timezone.now() - - @property - def expire_time(self): - interval = self.date_expired - timezone.now() - seconds = interval.total_seconds() - if seconds < 0: - seconds = 0 - return int(seconds) - - def expire(self): - self.date_expired = timezone.now() - self.save() - - @property - def is_valid(self): - return not self.is_expired - - def is_type(self, tp): - return self.type == tp - - def renewal(self): - """ 续期 Token,将来支持用户自定义创建 token 后,续期策略要修改 """ - self.date_expired = self.get_default_date_expired() - self.save() - - actions = expired_at = None # actions 和 expired_at 在 check_valid() 中赋值 - - def check_valid(self): - from perms.utils.permission import validate_permission as asset_validate_permission - - if self.is_expired: - is_valid = False - error = _('Connection token expired at: {}').format(as_current_tz(self.date_expired)) - return is_valid, error - - if not self.user: - is_valid = False - error = _('User not exists') - return is_valid, error - if not self.user.is_valid: - is_valid = False - error = _('User invalid, disabled or expired') - return is_valid, error - - if not self.account: - is_valid = False - error = _('Account not exists') - return is_valid, error - - if not self.asset: - is_valid = False - error = _('Asset not exists') - return is_valid, error - - if not self.asset.is_active: - is_valid = False - error = _('Asset inactive') - return is_valid, error - - has_perm, actions, expired_at = asset_validate_permission( - self.user, self.asset, self.account - ) - if not has_perm: - is_valid = False - error = _('User has no permission to access asset or permission expired') - return is_valid, error - self.actions = actions - self.expired_at = expired_at - return True, '' - - @lazyproperty - def domain(self): - if self.asset: - return self.asset.domain - if not self.application: - return - if self.application.category_remote_app: - asset = self.application.get_remote_app_asset() - domain = asset.domain if asset else None - else: - domain = self.application.domain - return domain - - @lazyproperty - def gateway(self): - from assets.models import Domain - if not self.domain: - return - self.domain: Domain - return self.domain.random_gateway() - - @lazyproperty - def remote_app(self): - if not self.application: - return {} - if not self.application.category_remote_app: - return {} - return self.application.get_rdp_remote_app_setting() - - @lazyproperty - def asset_or_remote_app_asset(self): - if self.asset: - return self.asset - if self.application and self.application.category_remote_app: - return self.application.get_remote_app_asset() - - @lazyproperty - def cmd_filter_rules(self): - from assets.models import CommandFilterRule - kwargs = { - 'user_id': self.user.id, - 'system_user_id': self.system_user.id, - } - if self.asset: - kwargs['asset_id'] = self.asset.id - elif self.application: - kwargs['application_id'] = self.application_id - rules = CommandFilterRule.get_queryset(**kwargs) - return rules - - def load_system_user_auth(self): - if self.asset: - self.system_user.load_asset_more_auth(self.asset.id, self.user.username, self.user.id) - elif self.application: - self.system_user.load_app_more_auth(self.application.id, self.user.username, self.user.id) - - -class TempToken(JMSBaseModel): - username = models.CharField(max_length=128, verbose_name=_("Username")) - secret = models.CharField(max_length=64, verbose_name=_("Secret")) - verified = models.BooleanField(default=False, verbose_name=_("Verified")) - date_verified = models.DateTimeField(null=True, verbose_name=_("Date verified")) - date_expired = models.DateTimeField(verbose_name=_("Date expired")) - - class Meta: - verbose_name = _("Temporary token") - - @property - def user(self): - from users.models import User - return User.objects.filter(username=self.username).first() - - @property - def is_valid(self): - not_expired = self.date_expired and self.date_expired > timezone.now() - return not self.verified and not_expired - - -class SuperConnectionToken(ConnectionToken): - class Meta: - proxy = True - verbose_name = _("Super connection token") diff --git a/apps/authentication/models/__init__.py b/apps/authentication/models/__init__.py new file mode 100644 index 000000000..e889e03b1 --- /dev/null +++ b/apps/authentication/models/__init__.py @@ -0,0 +1,5 @@ +from .access_key import * +from .connection_token import * +from .private_token import * +from .sso_token import * +from .temp_token import * diff --git a/apps/authentication/models/access_key.py b/apps/authentication/models/access_key.py new file mode 100644 index 000000000..67aa6b812 --- /dev/null +++ b/apps/authentication/models/access_key.py @@ -0,0 +1,31 @@ +import uuid +from django.utils.translation import ugettext_lazy as _ +from django.conf import settings + +from django.db import models + + +class AccessKey(models.Model): + id = models.UUIDField(verbose_name='AccessKeyID', primary_key=True, + default=uuid.uuid4, editable=False) + secret = models.UUIDField(verbose_name='AccessKeySecret', + default=uuid.uuid4, editable=False) + user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name='User', + on_delete=models.CASCADE, related_name='access_keys') + is_active = models.BooleanField(default=True, verbose_name=_('Active')) + date_created = models.DateTimeField(auto_now_add=True) + + def get_id(self): + return str(self.id) + + def get_secret(self): + return str(self.secret) + + def get_full_value(self): + return '{}:{}'.format(self.id, self.secret) + + def __str__(self): + return str(self.id) + + class Meta: + verbose_name = _("Access key") diff --git a/apps/authentication/models/connection_token.py b/apps/authentication/models/connection_token.py new file mode 100644 index 000000000..c76d6e0f4 --- /dev/null +++ b/apps/authentication/models/connection_token.py @@ -0,0 +1,140 @@ +import time +from datetime import timedelta +from django.utils import timezone +from django.utils.translation import ugettext_lazy as _ +from django.conf import settings +from orgs.mixins.models import OrgModelMixin + +from django.db import models +from common.utils import lazyproperty +from common.utils.timezone import as_current_tz +from common.db.models import JMSBaseModel + + +def date_expired_default(): + return timezone.now() + timedelta(seconds=settings.CONNECTION_TOKEN_EXPIRATION) + + +class ConnectionToken(OrgModelMixin, JMSBaseModel): + user = models.ForeignKey( + 'users.User', on_delete=models.SET_NULL, null=True, blank=True, + related_name='connection_tokens', verbose_name=_('User') + ) + asset = models.ForeignKey( + 'assets.Asset', on_delete=models.SET_NULL, null=True, blank=True, + related_name='connection_tokens', verbose_name=_('Asset'), + ) + user_display = models.CharField(max_length=128, default='', verbose_name=_("User display")) + asset_display = models.CharField(max_length=128, default='', verbose_name=_("Asset display")) + protocol = '' + account = models.CharField(max_length=128, default='', verbose_name=_("Account")) + secret = models.CharField(max_length=64, default='', verbose_name=_("Secret")) + date_expired = models.DateTimeField(default=date_expired_default, verbose_name=_("Date expired")) + + class Meta: + ordering = ('-date_expired',) + verbose_name = _('Connection token') + permissions = [ + ('view_connectiontokensecret', _('Can view connection token secret')) + ] + + @property + def is_expired(self): + return self.date_expired < timezone.now() + + @property + def expire_time(self): + interval = self.date_expired - timezone.now() + seconds = interval.total_seconds() + if seconds < 0: + seconds = 0 + return int(seconds) + + @property + def is_valid(self): + return not self.is_expired + + @classmethod + def get_default_date_expired(cls): + return date_expired_default() + + def expire(self): + self.date_expired = timezone.now() + self.save() + + def renewal(self): + """ 续期 Token,将来支持用户自定义创建 token 后,续期策略要修改 """ + self.date_expired = self.get_default_date_expired() + self.save() + + # actions 和 expired_at 在 check_valid() 中赋值 + actions = expire_at = None + + def check_valid(self): + from perms.utils.account import PermAccountUtil + if self.is_expired: + is_valid = False + error = _('Connection token expired at: {}').format(as_current_tz(self.date_expired)) + return is_valid, error + if not self.user: + is_valid = False + error = _('User not exists') + return is_valid, error + if not self.user.is_valid: + is_valid = False + error = _('User invalid, disabled or expired') + return is_valid, error + if not self.asset: + is_valid = False + error = _('Asset not exists') + return is_valid, error + if not self.asset.is_active: + is_valid = False + error = _('Asset inactive') + return is_valid, error + if not self.account: + is_valid = False + error = _('Account not exists') + return is_valid, error + + actions, expire_at = PermAccountUtil().validate_permission( + self.user, self.asset, self.account + ) + if not actions or expire_at < time.time(): + is_valid = False + error = _('User has no permission to access asset or permission expired') + return is_valid, error + self.actions = actions + self.expire_at = expire_at + return True, '' + + @lazyproperty + def domain(self): + domain = self.asset.domain if self.asset else None + return domain + + @lazyproperty + def gateway(self): + from assets.models import Domain + if not self.domain: + return + self.domain: Domain + return self.domain.random_gateway() + + @lazyproperty + def cmd_filter_rules(self): + from assets.models import CommandFilterRule + kwargs = { + 'user_id': self.user.id, + 'account': self.account, + } + if self.asset: + kwargs['asset_id'] = self.asset.id + rules = CommandFilterRule.get_queryset(**kwargs) + return rules + + +class SuperConnectionToken(ConnectionToken): + class Meta: + proxy = True + verbose_name = _("Super connection token") diff --git a/apps/authentication/models/private_token.py b/apps/authentication/models/private_token.py new file mode 100644 index 000000000..8d83d1e0a --- /dev/null +++ b/apps/authentication/models/private_token.py @@ -0,0 +1,9 @@ +from django.utils.translation import ugettext_lazy as _ +from rest_framework.authtoken.models import Token + + +class PrivateToken(Token): + """Inherit from auth token, otherwise migration is boring""" + + class Meta: + verbose_name = _('Private Token') diff --git a/apps/authentication/models/sso_token.py b/apps/authentication/models/sso_token.py new file mode 100644 index 000000000..fb4c68827 --- /dev/null +++ b/apps/authentication/models/sso_token.py @@ -0,0 +1,18 @@ +import uuid +from django.utils.translation import ugettext_lazy as _ + +from django.db import models +from common.db.models import BaseCreateUpdateModel + + +class SSOToken(BaseCreateUpdateModel): + """ + 类似腾讯企业邮的 [单点登录](https://exmail.qq.com/qy_mng_logic/doc#10036) + 出于安全考虑,这里的 `token` 使用一次随即过期。但我们保留每一个生成过的 `token`。 + """ + authkey = models.UUIDField(primary_key=True, default=uuid.uuid4, verbose_name=_('Token')) + expired = models.BooleanField(default=False, verbose_name=_('Expired')) + user = models.ForeignKey('users.User', on_delete=models.CASCADE, verbose_name=_('User'), db_constraint=False) + + class Meta: + verbose_name = _('SSO token') diff --git a/apps/authentication/models/temp_token.py b/apps/authentication/models/temp_token.py new file mode 100644 index 000000000..d76a30a42 --- /dev/null +++ b/apps/authentication/models/temp_token.py @@ -0,0 +1,26 @@ +from django.utils import timezone +from django.utils.translation import ugettext_lazy as _ + +from django.db import models +from common.db.models import JMSBaseModel + + +class TempToken(JMSBaseModel): + username = models.CharField(max_length=128, verbose_name=_("Username")) + secret = models.CharField(max_length=64, verbose_name=_("Secret")) + verified = models.BooleanField(default=False, verbose_name=_("Verified")) + date_verified = models.DateTimeField(null=True, verbose_name=_("Date verified")) + date_expired = models.DateTimeField(verbose_name=_("Date expired")) + + class Meta: + verbose_name = _("Temporary token") + + @property + def user(self): + from users.models import User + return User.objects.filter(username=self.username).first() + + @property + def is_valid(self): + not_expired = self.date_expired and self.date_expired > timezone.now() + return not self.verified and not_expired diff --git a/apps/authentication/serializers/connection_token.py b/apps/authentication/serializers/connection_token.py index 58973ba70..1093da1f9 100644 --- a/apps/authentication/serializers/connection_token.py +++ b/apps/authentication/serializers/connection_token.py @@ -165,6 +165,6 @@ class ConnectionTokenSecretSerializer(OrgResourceModelSerializerMixin): class Meta: model = ConnectionToken fields = [ - 'id', 'secret', 'type', 'user', 'asset', 'application', 'system_user', - 'remote_app', 'cmd_filter_rules', 'domain', 'gateway', 'actions', 'expired_at', + 'id', 'secret', 'type', 'user', 'asset', 'account', + 'cmd_filter_rules', 'domain', 'gateway', 'actions', 'expired_at', ] diff --git a/apps/orgs/models.py b/apps/orgs/models.py index 5e78d2757..73dc3c6ad 100644 --- a/apps/orgs/models.py +++ b/apps/orgs/models.py @@ -140,7 +140,10 @@ class Organization(OrgRoleMixin, models.Model): @classmethod def default(cls): defaults = dict(id=cls.DEFAULT_ID, name=cls.DEFAULT_NAME) - obj, created = cls.objects.get_or_create(defaults=defaults, id=cls.DEFAULT_ID, builtin=True) + obj, created = cls.objects.get_or_create(defaults=defaults, id=cls.DEFAULT_ID) + if not obj.builtin: + obj.builtin = True + obj.save() return obj @classmethod diff --git a/apps/perms/api/user_permission/common.py b/apps/perms/api/user_permission/common.py index ebcbcf3e6..927ec7443 100644 --- a/apps/perms/api/user_permission/common.py +++ b/apps/perms/api/user_permission/common.py @@ -1,34 +1,20 @@ # -*- coding: utf-8 -*- # -import uuid -import time -from collections import defaultdict - from django.shortcuts import get_object_or_404 -from django.utils.decorators import method_decorator -from rest_framework.views import APIView, Response -from rest_framework import status from rest_framework.generics import ( - ListAPIView, get_object_or_404, RetrieveAPIView -) - -from orgs.utils import tmp_to_root_org -from perms.utils.permission import ( - get_asset_system_user_ids_with_actions_by_user, validate_permission + ListAPIView, get_object_or_404 ) from common.permissions import IsValidUser from common.utils import get_logger, lazyproperty from perms.hands import User, Asset, Account from perms import serializers -from perms.models import AssetPermission, Action +from perms.models import Action from perms.utils import PermAccountUtil logger = get_logger(__name__) __all__ = [ - 'ValidateUserAssetPermissionApi', - 'GetUserAssetPermissionActionsApi', 'UserGrantedAssetAccountsApi', 'MyGrantedAssetAccountsApi', 'UserGrantedAssetSpecialAccountsApi', @@ -36,70 +22,6 @@ __all__ = [ ] -@method_decorator(tmp_to_root_org(), name='get') -class GetUserAssetPermissionActionsApi(RetrieveAPIView): - serializer_class = serializers.ActionsSerializer - rbac_perms = { - 'retrieve': 'perms.view_userassets', - 'GET': 'perms.view_userassets', - } - - def get_user(self): - user_id = self.request.query_params.get('user_id', '') - user = get_object_or_404(User, id=user_id) - return user - - def get_object(self): - asset_id = self.request.query_params.get('asset_id', '') - account = self.request.query_params.get('account', '') - - try: - asset_id = uuid.UUID(asset_id) - except ValueError: - return Response({'msg': False}, status=403) - - asset = get_object_or_404(Asset, id=asset_id) - - system_users_actions = get_asset_system_user_ids_with_actions_by_user(self.get_user(), asset) - # actions = system_users_actions.get(system_user.id) - actions = system_users_actions.get(account) - return {"actions": actions} - - -@method_decorator(tmp_to_root_org(), name='get') -class ValidateUserAssetPermissionApi(APIView): - rbac_perms = { - 'GET': 'perms.view_userassets' - } - - def get(self, request, *args, **kwargs): - user_id = self.request.query_params.get('user_id', '') - asset_id = request.query_params.get('asset_id', '') - account = request.query_params.get('account', '') - action_name = request.query_params.get('action_name', '') - - data = { - 'has_permission': False, - 'expire_at': int(time.time()), - 'actions': [] - } - - if not all((user_id, asset_id, account, action_name)): - return Response(data) - - user = User.objects.get(id=user_id) - asset = Asset.objects.valid().get(id=asset_id) - - has_perm, actions, expire_at = validate_permission(user, asset, account, action_name) - status_code = status.HTTP_200_OK if has_perm else status.HTTP_403_FORBIDDEN - data = { - 'has_permission': has_perm, - 'actions': actions, - 'expire_at': int(expire_at) - } - return Response(data, status=status_code) - - class UserGrantedAssetAccountsApi(ListAPIView): serializer_class = serializers.AccountsGrantedSerializer rbac_perms = { diff --git a/apps/perms/urls/asset_permission.py b/apps/perms/urls/asset_permission.py index cc8e10f36..a97727550 100644 --- a/apps/perms/urls/asset_permission.py +++ b/apps/perms/urls/asset_permission.py @@ -84,12 +84,6 @@ permission_urlpatterns = [ # 授权规则中授权的资产 path('/assets/all/', api.AssetPermissionAllAssetListApi.as_view(), name='asset-permission-all-assets'), path('/users/all/', api.AssetPermissionAllUserListApi.as_view(), name='asset-permission-all-users'), - - # 验证用户是否有某个资产和系统用户的权限 - # Todo: v3 先不动, 可能会修改连接资产时的逻辑, 直接获取认证信息,获取不到就时没有权限,就不需要校验了 - path('user/validate/', api.ValidateUserAssetPermissionApi.as_view(), name='validate-user-asset-permission'), - path('user/actions/', api.GetUserAssetPermissionActionsApi.as_view(), name='get-user-asset-permission-actions'), - ] asset_permission_urlpatterns = [ diff --git a/apps/perms/utils/account.py b/apps/perms/utils/account.py index a5fdadc6b..34b839fab 100644 --- a/apps/perms/utils/account.py +++ b/apps/perms/utils/account.py @@ -1,3 +1,4 @@ +import time from collections import defaultdict from assets.models import Account from .permission import AssetPermissionUtil @@ -8,25 +9,29 @@ __all__ = ['PermAccountUtil'] class PermAccountUtil(AssetPermissionUtil): """ 资产授权账号相关的工具 """ - def get_perm_accounts_for_user_asset(self, user, asset, with_actions=False): - """ 获取授权给用户某个资产的账号 """ - perms = self.get_permissions_for_user_asset(user, asset) - accounts = self.get_perm_accounts_for_permissions(perms, with_actions=with_actions) - return accounts - def get_perm_accounts_for_user(self, user, with_actions=False): """ 获取授权给用户的所有账号 """ perms = self.get_permissions_for_user(user) accounts = self.get_perm_accounts_for_permissions(perms, with_actions=with_actions) return accounts + def get_perm_accounts_for_user_asset(self, user, asset, with_actions=False, with_perms=False): + """ 获取授权给用户某个资产的账号 """ + perms = self.get_permissions_for_user_asset(user, asset) + accounts = self.get_perm_accounts_for_permissions(perms, with_actions=with_actions) + if with_perms: + return perms, accounts + return accounts + def get_perm_accounts_for_user_group_asset(self, user_group, asset, with_actions=False): + """ 获取授权给用户组某个资产的账号 """ perms = self.get_permissions_for_user_group_asset(user_group, asset) accounts = self.get_perm_accounts_for_permissions(perms, with_actions=with_actions) return accounts @staticmethod def get_perm_accounts_for_permissions(permissions, with_actions=False): + """ 获取授权规则包含的账号 """ aid_actions_map = defaultdict(int) for perm in permissions: account_ids = perm.get_all_accounts(flat=True) @@ -40,3 +45,14 @@ class PermAccountUtil(AssetPermissionUtil): account.actions = aid_actions_map.get(str(account.id)) return accounts + def validate_permission(self, user, asset, account_username): + """ 校验用户有某个资产下某个账号名的权限 """ + perms, accounts = self.get_perm_accounts_for_user_asset( + user, asset, with_actions=True, with_perms=True + ) + perm = perms.first() + # Todo: 后面可能需要加上 protocol 进行过滤, 因为同名的账号协议是不一样可能会存在多个 + account = accounts.filter(username=account_username).first() + actions = account.actions if account else [] + expire_at = perm.date_expired if perm else time.time() + return actions, expire_at diff --git a/apps/perms/utils/permission.py b/apps/perms/utils/permission.py index b16c69fa0..e7d88e06d 100644 --- a/apps/perms/utils/permission.py +++ b/apps/perms/utils/permission.py @@ -14,21 +14,6 @@ logger = get_logger(__file__) class AssetPermissionUtil(object): """ 资产授权相关的方法工具 """ - def get_permissions_for_user_asset(self, user, asset): - """ 获取同时包含用户、资产的授权规则 """ - user_perm_ids = self.get_permissions_for_user(user, flat=True) - asset_perm_ids = self.get_permissions_for_asset(asset, flat=True) - perm_ids = set(user_perm_ids) & set(asset_perm_ids) - perms = AssetPermission.objects.filter(id__in=perm_ids) - return perms - - def get_permissions_for_user_group_asset(self, user_group, asset): - user_perm_ids = self.get_permissions_for_user_groups([user_group], flat=True) - asset_perm_ids = self.get_permissions_for_asset(asset, flat=True) - perm_ids = set(user_perm_ids) & set(asset_perm_ids) - perms = AssetPermission.objects.filter(id__in=perm_ids) - return perms - def get_permissions_for_user(self, user, with_group=True, flat=False): """ 获取用户的授权规则 """ perm_ids = set() @@ -43,22 +28,21 @@ class AssetPermissionUtil(object): perm_ids.update(group_perm_ids) if flat: return perm_ids - perms = AssetPermission.objects.filter(id__in=perm_ids) + perms = self.get_permissions(ids=perm_ids) return perms - @staticmethod - def get_permissions_for_user_groups(user_groups, flat=False): + def get_permissions_for_user_groups(self, user_groups, flat=False): """ 获取用户组的授权规则 """ if isinstance(user_groups, list): group_ids = [g.id for g in user_groups] else: group_ids = user_groups.values_list('id', flat=True).distinct() - group_perm_ids = AssetPermission.user_groups.through.objects\ - .filter(usergroup_id__in=group_ids)\ + group_perm_ids = AssetPermission.user_groups.through.objects \ + .filter(usergroup_id__in=group_ids) \ .values_list('assetpermission_id', flat=True).distinct() if flat: return group_perm_ids - perms = AssetPermission.objects.filter(id__in=group_perm_ids) + perms = self.get_permissions(ids=group_perm_ids) return perms def get_permissions_for_asset(self, asset, with_node=True, flat=False): @@ -73,11 +57,10 @@ class AssetPermissionUtil(object): perm_ids.update(node_perm_ids) if flat: return perm_ids - perms = AssetPermission.objects.filter(id__in=perm_ids) + perms = self.get_permissions(ids=perm_ids) return perms - @staticmethod - def get_permissions_for_nodes(nodes, with_ancestor=False, flat=False): + def get_permissions_for_nodes(self, nodes, with_ancestor=False, flat=False): """ 获取节点的授权规则 """ if with_ancestor: node_ids = set() @@ -87,93 +70,29 @@ class AssetPermissionUtil(object): node_ids.update(_node_ids) else: node_ids = nodes.values_list('id', flat=True).distinct() - node_perm_ids = AssetPermission.nodes.through.objects.filter(node_id__in=node_ids) \ + perm_ids = AssetPermission.nodes.through.objects.filter(node_id__in=node_ids) \ .values_list('assetpermission_id', flat=True).distinct() if flat: - return node_perm_ids - perms = AssetPermission.objects.filter(id__in=node_perm_ids) + return perm_ids + perms = self.get_permissions(ids=perm_ids) return perms + def get_permissions_for_user_asset(self, user, asset): + """ 获取同时包含用户、资产的授权规则 """ + user_perm_ids = self.get_permissions_for_user(user, flat=True) + asset_perm_ids = self.get_permissions_for_asset(asset, flat=True) + perm_ids = set(user_perm_ids) & set(asset_perm_ids) + perms = self.get_permissions(ids=perm_ids) + return perms -# TODO: 下面的方法放到类中进行实现 - - -def validate_permission(user, asset, account, action='connect'): - asset_perm_ids = get_user_all_asset_perm_ids(user) - - asset_perm_ids_from_asset = AssetPermission.assets.through.objects.filter( - assetpermission_id__in=asset_perm_ids, - asset_id=asset.id - ).values_list('assetpermission_id', flat=True) - - nodes = asset.get_nodes() - node_keys = set() - for node in nodes: - ancestor_keys = node.get_ancestor_keys(with_self=True) - node_keys.update(ancestor_keys) - node_ids = set(Node.objects.filter(key__in=node_keys).values_list('id', flat=True)) - - asset_perm_ids_from_node = AssetPermission.nodes.through.objects.filter( - assetpermission_id__in=asset_perm_ids, - node_id__in=node_ids - ).values_list('assetpermission_id', flat=True) - - asset_perm_ids = {*asset_perm_ids_from_asset, *asset_perm_ids_from_node} - - asset_perms = AssetPermission.objects\ - .filter(id__in=asset_perm_ids, accounts__contains=account)\ - .order_by('-date_expired') - - if asset_perms: - actions = set() - actions_values = asset_perms.values_list('actions', flat=True) - for value in actions_values: - _actions = Action.value_to_choices(value) - actions.update(_actions) - asset_perm: AssetPermission = asset_perms.first() - actions = list(actions) - expire_at = asset_perm.date_expired.timestamp() - else: - actions = [] - expire_at = time.time() - - # TODO: 组件改造API完成后统一通过actions判断has_perm - has_perm = action in actions - return has_perm, actions, expire_at - - -def get_asset_system_user_ids_with_actions(asset_perm_ids, asset: Asset): - nodes = asset.get_nodes() - node_keys = set() - for node in nodes: - ancestor_keys = node.get_ancestor_keys(with_self=True) - node_keys.update(ancestor_keys) - - queryset = AssetPermission.objects.filter(id__in=asset_perm_ids)\ - .filter(Q(assets=asset) | Q(nodes__key__in=node_keys)) - - asset_protocols = asset.protocols_as_dict.keys() - values = queryset.filter( - system_users__protocol__in=asset_protocols - ).distinct().values_list('system_users', 'actions') - system_users_actions = defaultdict(int) - - for system_user_id, actions in values: - if None in (system_user_id, actions): - continue - system_users_actions[system_user_id] |= actions - return system_users_actions - - -def get_asset_system_user_ids_with_actions_by_user(user: User, asset: Asset): - asset_perm_ids = get_user_all_asset_perm_ids(user) - return get_asset_system_user_ids_with_actions(asset_perm_ids, asset) - - -def has_asset_system_permission(user: User, asset: Asset, account: str): - systemuser_actions_mapper = get_asset_system_user_ids_with_actions_by_user(user, asset) - actions = systemuser_actions_mapper.get(account, 0) - if actions: - return True - return False + def get_permissions_for_user_group_asset(self, user_group, asset): + user_perm_ids = self.get_permissions_for_user_groups([user_group], flat=True) + asset_perm_ids = self.get_permissions_for_asset(asset, flat=True) + perm_ids = set(user_perm_ids) & set(asset_perm_ids) + perms = self.get_permissions(ids=perm_ids) + return perms + @staticmethod + def get_permissions(ids): + perms = AssetPermission.objects.filter(id__in=ids).order_by('-date_expired') + return perms