From d9a11900a938e90ab9fd1ab8b9a74c0ef3aa2e55 Mon Sep 17 00:00:00 2001 From: Aaron3S Date: Wed, 22 Feb 2023 15:35:59 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E4=BD=9C=E4=B8=9A?= =?UTF-8?q?=E4=B8=AD=E5=BF=83=E8=B5=84=E4=BA=A7=E5=92=8C=E7=94=A8=E4=BA=8E?= =?UTF-8?q?=E6=B2=A1=E6=9C=89=E8=BF=87=E6=BB=A4=E6=8E=88=E6=9D=83=E8=A7=84?= =?UTF-8?q?=E5=88=99=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/ops/ansible/inventory.py | 5 ++- apps/ops/models/job.py | 69 ++++++++++++++++++++++++++++++++++- apps/ops/serializers/job.py | 10 ++++- 3 files changed, 80 insertions(+), 4 deletions(-) diff --git a/apps/ops/ansible/inventory.py b/apps/ops/ansible/inventory.py index de273cfd7..ba3be6bde 100644 --- a/apps/ops/ansible/inventory.py +++ b/apps/ops/ansible/inventory.py @@ -139,8 +139,11 @@ class JMSInventory: self.make_ssh_account_vars(host, asset, account, automation, protocols, platform, gateway) return host + def get_asset_accounts(self, asset): + return list(asset.accounts.filter(is_active=True)) + def select_account(self, asset): - accounts = list(asset.accounts.filter(is_active=True)) + accounts = self.get_asset_accounts(asset) if not accounts: return None account_selected = None diff --git a/apps/ops/models/job.py b/apps/ops/models/job.py index d17dcd442..911ca2c97 100644 --- a/apps/ops/models/job.py +++ b/apps/ops/models/job.py @@ -2,26 +2,91 @@ import json import logging import os import uuid +from collections import defaultdict from celery import current_task from django.conf import settings from django.db import models +from django.db.models import Q from django.utils import timezone from django.utils.translation import gettext_lazy as _ -__all__ = ["Job", "JobExecution"] +__all__ = ["Job", "JobExecution", "JMSPermedInventory"] from simple_history.models import HistoricalRecords +from accounts.models import Account from acls.models import CommandFilterACL +from assets.models import Asset from ops.ansible import JMSInventory, AdHocRunner, PlaybookRunner from ops.mixin import PeriodTaskModelMixin from ops.variables import * from ops.const import Types, Modules, RunasPolicies, JobStatus from orgs.mixins.models import JMSOrgBaseModel +from perms.models import AssetPermission +from perms.utils import UserPermAssetUtil from terminal.notifications import CommandExecutionAlert +def get_parent_keys(key, include_self=True): + keys = [] + split_keys = key.split(':') + for i in range(len(split_keys)): + keys.append(':'.join(split_keys[:i + 1])) + if not include_self: + keys.pop() + return keys + + +class JMSPermedInventory(JMSInventory): + def __init__(self, assets, account_policy='privileged_first', + account_prefer='root,Administrator', host_callback=None, exclude_localhost=False, user=None): + super().__init__(assets, account_policy, account_prefer, host_callback, exclude_localhost) + self.user = user + self.assets_accounts_mapper = self.get_assets_accounts_mapper() + + def get_asset_accounts(self, asset): + return self.assets_accounts_mapper.get(asset.id, []) + + def get_permed_assets(self): + permed_assets = UserPermAssetUtil(self.user).get_all_assets() + return set(self.assets) & (set(permed_assets)) + + def get_assets_accounts_mapper(self): + mapper = defaultdict(set) + asset_ids = self.assets.values_list('id', flat=True) + asset_node_keys = Asset.nodes.through.objects. \ + filter(asset_id__in=asset_ids).values_list('asset_id', 'node__key') + + node_asset_map = defaultdict(set) + for asset_id, node_key in asset_node_keys: + all_keys = get_parent_keys(node_key) + for key in all_keys: + node_asset_map[key].add(asset_id) + + perms = AssetPermission.objects \ + .filter(is_active=True) \ + .filter(Q(users=self.user) | Q(user_groups__in=self.user.groups.all())) \ + .filter(Q(assets__in=asset_ids) | Q(nodes__key__in=[i[1] for i in asset_node_keys if len(i) == 2])) \ + .values_list('assets', 'nodes__key', 'accounts') + + asset_permed_accounts_mapper = defaultdict(set) + for asset_id, node_id, accounts in perms: + if asset_id: + asset_permed_accounts_mapper[asset_id].update(accounts) + if node_id and asset_id in node_asset_map[node_id]: + asset_permed_accounts_mapper[asset_id].update(accounts) + + accounts = Account.objects.filter(asset__in=asset_ids) + for account in accounts: + if account.asset_id not in asset_permed_accounts_mapper: + continue + permed_usernames = asset_permed_accounts_mapper[account.asset_id] + if "@ALL" in permed_usernames or account.username in permed_usernames: + mapper[account.asset_id].add(account) + return mapper + + class Job(JMSOrgBaseModel, PeriodTaskModelMixin): name = models.CharField(max_length=128, null=True, verbose_name=_('Name')) @@ -90,7 +155,7 @@ class Job(JMSOrgBaseModel, PeriodTaskModelMixin): @property def inventory(self): - return JMSInventory(self.assets.all(), self.runas_policy, self.runas) + return JMSPermedInventory(self.assets.all(), self.runas_policy, self.runas, user=self.creator) @property def material(self): diff --git a/apps/ops/serializers/job.py b/apps/ops/serializers/job.py index 792a23acd..cbf7b8936 100644 --- a/apps/ops/serializers/job.py +++ b/apps/ops/serializers/job.py @@ -3,7 +3,7 @@ import uuid from django.utils.translation import gettext_lazy as _ from rest_framework import serializers -from assets.models import Node +from assets.models import Node, Asset from perms.utils.user_perm import UserPermAssetUtil from common.serializers.fields import ReadableHiddenField from ops.mixin import PeriodTaskSerializerMixin @@ -17,6 +17,8 @@ class JobSerializer(BulkOrgResourceModelSerializer, PeriodTaskSerializerMixin): nodes = serializers.ListField(required=False, child=serializers.CharField()) date_last_run = serializers.DateTimeField(label=_('Date last run'), read_only=True) name = serializers.CharField(label=_('Name'), max_length=128, allow_blank=True, required=False) + assets = serializers.PrimaryKeyRelatedField(label=_('Assets'), queryset=Asset.objects.all(), many=True, + required=False) def to_internal_value(self, data): instant = data.get('instant', False) @@ -30,6 +32,12 @@ class JobSerializer(BulkOrgResourceModelSerializer, PeriodTaskSerializerMixin): user = request.user if request else None return user + def validate_assets(self, assets): + permed_assets = UserPermAssetUtil(self.get_request_user()).get_all_assets() + if not set(assets).issubset(set(permed_assets)): + raise serializers.ValidationError(_('Assets not in user perm')) + return assets + def create(self, validated_data): assets = validated_data.__getitem__('assets') node_ids = validated_data.pop('nodes', None) From 34386bd6fbf469a38a86c9b6475644bcbd624826 Mon Sep 17 00:00:00 2001 From: Aaron3S Date: Wed, 22 Feb 2023 18:06:46 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20=E4=BF=AE=E6=94=B9=E7=94=9F=E6=88=90?= =?UTF-8?q?=20inventory=20=E9=94=99=E8=AF=AF=E4=BB=A3=E7=A0=81=EF=BC=8C?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E8=BF=90=E8=A1=8C=E4=BD=9C=E4=B8=9A=E6=9D=83?= =?UTF-8?q?=E9=99=90=E6=A0=A1=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/ops/models/job.py | 35 +++++++++++++++++++++++------------ apps/ops/serializers/job.py | 8 +------- 2 files changed, 24 insertions(+), 19 deletions(-) diff --git a/apps/ops/models/job.py b/apps/ops/models/job.py index 911ca2c97..51a8db528 100644 --- a/apps/ops/models/job.py +++ b/apps/ops/models/job.py @@ -48,15 +48,12 @@ class JMSPermedInventory(JMSInventory): def get_asset_accounts(self, asset): return self.assets_accounts_mapper.get(asset.id, []) - def get_permed_assets(self): - permed_assets = UserPermAssetUtil(self.user).get_all_assets() - return set(self.assets) & (set(permed_assets)) - def get_assets_accounts_mapper(self): mapper = defaultdict(set) asset_ids = self.assets.values_list('id', flat=True) - asset_node_keys = Asset.nodes.through.objects. \ - filter(asset_id__in=asset_ids).values_list('asset_id', 'node__key') + asset_node_keys = Asset.nodes.through.objects \ + .filter(asset_id__in=asset_ids) \ + .values_list('asset_id', 'node__key') node_asset_map = defaultdict(set) for asset_id, node_key in asset_node_keys: @@ -64,18 +61,20 @@ class JMSPermedInventory(JMSInventory): for key in all_keys: node_asset_map[key].add(asset_id) + groups = self.user.groups.all() perms = AssetPermission.objects \ + .filter(date_expired__gte=timezone.now()) \ .filter(is_active=True) \ - .filter(Q(users=self.user) | Q(user_groups__in=self.user.groups.all())) \ - .filter(Q(assets__in=asset_ids) | Q(nodes__key__in=[i[1] for i in asset_node_keys if len(i) == 2])) \ + .filter(Q(users=self.user) | Q(user_groups__in=groups)) \ + .filter(Q(assets__in=asset_ids) | Q(nodes__key__in=node_asset_map.keys())) \ .values_list('assets', 'nodes__key', 'accounts') asset_permed_accounts_mapper = defaultdict(set) - for asset_id, node_id, accounts in perms: - if asset_id: - asset_permed_accounts_mapper[asset_id].update(accounts) - if node_id and asset_id in node_asset_map[node_id]: + for asset_id, node_key, accounts in perms: + if asset_id in asset_ids: asset_permed_accounts_mapper[asset_id].update(accounts) + for my_asset in node_asset_map[node_key]: + asset_permed_accounts_mapper[my_asset].update(accounts) accounts = Account.objects.filter(asset__in=asset_ids) for account in accounts: @@ -404,7 +403,19 @@ class JobExecution(JMSOrgBaseModel): 'dangerous keyword \'{}\'\033[0m'.format(line['line'], line['file'], line['keyword'])) raise Exception("Playbook contains dangerous keywords") + def check_assets_perms(self): + all_permed_assets = UserPermAssetUtil(self.creator).get_all_assets() + has_permed_assets = set(self.current_job.assets.all()) & set(all_permed_assets) + + for asset in self.current_job.assets.all(): + if asset not in has_permed_assets: + print("\033[31mAsset {}({}) has no access permission\033[0m".format(asset.name, asset.address)) + + if self.current_job.assets.count() != len(has_permed_assets): + raise Exception("You do not have access rights to some assets") + def before_start(self): + self.check_assets_perms() if self.current_job.type == 'playbook': self.check_danger_keywords() if self.current_job.type == 'adhoc': diff --git a/apps/ops/serializers/job.py b/apps/ops/serializers/job.py index cbf7b8936..5e714976d 100644 --- a/apps/ops/serializers/job.py +++ b/apps/ops/serializers/job.py @@ -17,7 +17,7 @@ class JobSerializer(BulkOrgResourceModelSerializer, PeriodTaskSerializerMixin): nodes = serializers.ListField(required=False, child=serializers.CharField()) date_last_run = serializers.DateTimeField(label=_('Date last run'), read_only=True) name = serializers.CharField(label=_('Name'), max_length=128, allow_blank=True, required=False) - assets = serializers.PrimaryKeyRelatedField(label=_('Assets'), queryset=Asset.objects.all(), many=True, + assets = serializers.PrimaryKeyRelatedField(label=_('Assets'), queryset=Asset.objects, many=True, required=False) def to_internal_value(self, data): @@ -32,12 +32,6 @@ class JobSerializer(BulkOrgResourceModelSerializer, PeriodTaskSerializerMixin): user = request.user if request else None return user - def validate_assets(self, assets): - permed_assets = UserPermAssetUtil(self.get_request_user()).get_all_assets() - if not set(assets).issubset(set(permed_assets)): - raise serializers.ValidationError(_('Assets not in user perm')) - return assets - def create(self, validated_data): assets = validated_data.__getitem__('assets') node_ids = validated_data.pop('nodes', None)