From a5aa35870666e6adaf47b24c341b0342ccc95250 Mon Sep 17 00:00:00 2001 From: ImMin5 Date: Thu, 20 Jun 2024 22:30:43 +0900 Subject: [PATCH] feat: add step for exclude data field based on datasource's permissions info Signed-off-by: ImMin5 --- .../cost_analysis/manager/cost_manager.py | 34 +++-- .../cost_analysis/model/cost/__init__.py | 0 .../cost_analysis/model/cost/response.py | 34 +++++ .../cost_analysis/service/cost_service.py | 119 +++++++++++++++++- 4 files changed, 174 insertions(+), 13 deletions(-) create mode 100644 src/spaceone/cost_analysis/model/cost/__init__.py create mode 100644 src/spaceone/cost_analysis/model/cost/response.py diff --git a/src/spaceone/cost_analysis/manager/cost_manager.py b/src/spaceone/cost_analysis/manager/cost_manager.py index e7d9b769..3fc25ca5 100644 --- a/src/spaceone/cost_analysis/manager/cost_manager.py +++ b/src/spaceone/cost_analysis/manager/cost_manager.py @@ -119,6 +119,7 @@ def filter_costs(self, **conditions): return self.cost_model.filter(**conditions) def list_costs(self, query: dict, domain_id: str, data_source_id: str): + query = self.change_filter_v_workspace_id(query, domain_id, data_source_id) query = self._change_filter_project_group_id(query, domain_id) return self.cost_model.query(**query) @@ -204,6 +205,7 @@ def analyze_yearly_costs_with_cache( def analyze_costs_by_granularity( self, query: dict, domain_id: str, data_source_id: str ): + self._check_group_by(query) self._check_date_range(query) granularity = query["granularity"] @@ -497,15 +499,6 @@ def change_filter_v_workspace_id( query["filter"] = change_filter return query - @staticmethod - def _get_data_source_id_from_filter(query: dict) -> str: - for condition in query.get("filter", []): - key = condition.get("k", condition.get("key")) - value = condition.get("v", condition.get("value")) - - if key == "data_source_id": - return value - def _change_response_workspace_group_by( self, response: dict, query: dict, domain_id: str, data_source_id: str ) -> dict: @@ -550,3 +543,26 @@ def _get_workspace_id_from_v_workspace_id( workspace_id = ds_account_vos[0].workspace_id return workspace_id + + @staticmethod + def _get_data_source_id_from_filter(query: dict) -> str: + for condition in query.get("filter", []): + key = condition.get("k", condition.get("key")) + value = condition.get("v", condition.get("value")) + + if key == "data_source_id": + return value + + @staticmethod + def _check_group_by(query: dict) -> None: + group_by = query.get("group_by", []) + for group_by_field in group_by: + if group_by_field.split(".")[0] == "data": + raise ERROR_INVALID_PARAMETER( + key=group_by_field, reason=f"Data field is not allowed to group by." + ) + elif group_by_field in ["cost", "usage_quantity"]: + raise ERROR_INVALID_PARAMETER( + key=group_by_field, + reason=f"{group_by_field} are not allowed to group by.", + ) diff --git a/src/spaceone/cost_analysis/model/cost/__init__.py b/src/spaceone/cost_analysis/model/cost/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/spaceone/cost_analysis/model/cost/response.py b/src/spaceone/cost_analysis/model/cost/response.py new file mode 100644 index 00000000..1e361a6c --- /dev/null +++ b/src/spaceone/cost_analysis/model/cost/response.py @@ -0,0 +1,34 @@ +from typing import Union, List +from pydantic import BaseModel + +__all__ = ["CostResponse", "CostsResponse"] + + +class CostResponse(BaseModel): + cost_id: Union[str, None] = None + cost: Union[float, None] = None + usage_quantity: Union[float, None] = None + usage_unit: Union[str, None] = None + provider: Union[str, None] = None + region_code: Union[str, None] = None + region_key: Union[str, None] = None + product: Union[str, None] = None + usage_type: Union[str, None] = None + resource: Union[str, None] = None + tags: Union[dict, None] = None + additional_info: Union[dict, None] = None + data: Union[dict, None] = None + account_id: Union[str, None] = None + service_account_id: Union[str, None] = None + project_id: Union[str, None] = None + data_source_id: Union[str, None] = None + workspace_id: Union[str, None] = None + domain_id: Union[str, None] = None + billed_year: Union[str, None] = None + billed_month: Union[str, None] = None + billed_date: Union[str, None] = None + + +class CostsResponse(BaseModel): + results: List[CostResponse] + total_count: int diff --git a/src/spaceone/cost_analysis/service/cost_service.py b/src/spaceone/cost_analysis/service/cost_service.py index 879bcb5c..17b63acd 100644 --- a/src/spaceone/cost_analysis/service/cost_service.py +++ b/src/spaceone/cost_analysis/service/cost_service.py @@ -1,10 +1,20 @@ import logging +from typing import Union from spaceone.core.service import * from spaceone.core import utils +from spaceone.cost_analysis.model.cost.response import CostsResponse + +from spaceone.cost_analysis.manager.data_source_account_manager import ( + DataSourceAccountManager, +) + from spaceone.cost_analysis.error import * +from spaceone.cost_analysis.manager import DataSourceManager from spaceone.cost_analysis.manager.cost_manager import CostManager from spaceone.cost_analysis.manager.identity_manager import IdentityManager +from spaceone.cost_analysis.model import DataSource +from spaceone.cost_analysis.model.cost.response import CostResponse from spaceone.cost_analysis.model.cost_model import Cost _LOGGER = logging.getLogger(__name__) @@ -20,6 +30,7 @@ class CostService(BaseService): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.cost_mgr: CostManager = self.locator.get_manager("CostManager") + self.data_source_mgr = DataSourceManager() @transaction(permission="cost-analysis:Cost.write", role_types=["WORKSPACE_OWNER"]) @check_required( @@ -56,6 +67,8 @@ def create(self, params): identity_mgr: IdentityManager = self.locator.get_manager("IdentityManager") identity_mgr.get_project(params["project_id"], params["domain_id"]) + # todo : only local type datasource can create + cost_vo: Cost = self.cost_mgr.create_cost(params) self.cost_mgr.remove_stat_cache(params["domain_id"], params["data_source_id"]) @@ -98,7 +111,8 @@ def delete(self, params): role_types=["DOMAIN_ADMIN", "WORKSPACE_OWNER", "WORKSPACE_MEMBER"], ) @check_required(["cost_id", "domain_id"]) - def get(self, params): + @convert_model + def get(self, params: dict) -> Union[CostResponse, dict]: """Get cost Args: @@ -118,7 +132,35 @@ def get(self, params): workspace_id = params.get("workspace_id") domain_id = params["domain_id"] - return self.cost_mgr.get_cost(cost_id, domain_id, workspace_id, user_projects) + if workspace_id: + cost_vo: Cost = self.cost_mgr.get_cost(cost_id, domain_id, user_projects) + + v_workspace_ids = self._get_v_workspace_ids_related_with_workspace_id( + domain_id, workspace_id + ) + if ( + cost_vo.workspace_id not in v_workspace_ids + and cost_vo.workspace_id != workspace_id + ): + raise ERROR_PERMISSION_DENIED() + else: + cost_vo: Cost = self.cost_mgr.get_cost( + cost_id, domain_id, workspace_id, user_projects + ) + + cost_info = cost_vo.to_dict() + + # Check fields permissions + if self.transaction.get_meta("authorization.role_type") != "DOMAIN_ADMIN": + data_source_id = cost_vo.data_source_id + data_source_vo = self.data_source_mgr.get_data_source( + data_source_id, domain_id + ) + cost_info = self._remove_deny_fields_with_data_source_vo( + cost_info, data_source_vo + ) + + return CostResponse(**cost_info) @transaction( permission="cost-analysis:Cost.read", @@ -146,7 +188,8 @@ def get(self, params): ) @append_keyword_filter(["cost_id"]) @set_query_page_limit(1000) - def list(self, params): + @convert_model + def list(self, params: dict) -> Union[CostsResponse, dict]: """List costs Args: @@ -174,7 +217,28 @@ def list(self, params): query = params.get("query", {}) domain_id = params["domain_id"] data_source_id = params["data_source_id"] - return self.cost_mgr.list_costs(query, domain_id, data_source_id) + + cost_vos, total_count = self.cost_mgr.list_costs( + query, domain_id, data_source_id + ) + + # Check data fields permissions + if self.transaction.get_meta("authorization.role_type") != "DOMAIN_ADMIN": + data_source_vo = self.data_source_mgr.get_data_source( + data_source_id, domain_id + ) + cost_reports_info = [] + for cost_vo in cost_vos: + cost_info = cost_vo.to_dict() + + cost_info = self._remove_deny_fields_with_data_source_vo( + cost_info, data_source_vo + ) + cost_reports_info.append(cost_info) + else: + cost_reports_info = [cost_vo.to_dict() for cost_vo in cost_vos] + + return CostsResponse(results=cost_reports_info, total_count=total_count) @transaction( permission="cost-analysis:Cost.read", @@ -215,6 +279,12 @@ def analyze(self, params): data_source_id = params["data_source_id"] query = params.get("query", {}) + if self.transaction.get_meta("authorization.role_type") != "DOMAIN_ADMIN": + data_source_vo = self.data_source_mgr.get_data_source( + data_source_id, domain_id + ) + self._check_fields_with_data_source_permissions(query, data_source_vo) + return self.cost_mgr.analyze_costs_by_granularity( query, domain_id, data_source_id ) @@ -335,3 +405,44 @@ def _page_results(response, page): response["results"] = results return response + + @staticmethod + def _get_v_workspace_ids_related_with_workspace_id( + domain_id: str, workspace_id: str + ) -> list: + v_workspace_ids = [] + data_source_account_mgr = DataSourceAccountManager() + data_source_account_vos = data_source_account_mgr.filter_data_source_accounts( + domain_id=domain_id, + workspace_id=workspace_id, + ) + + v_workspace_ids.extend(data_source_account_vos.values_list("v_workspace_id")) + return v_workspace_ids + + @staticmethod + def _remove_deny_fields_with_data_source_vo( + cost_info: dict, data_source_vo: DataSource + ): + permissions = data_source_vo.permissions or {} + if permissions: + deny = permissions.get("deny", []) + for deny_field in deny: + if utils.get_dict_value(cost_info, deny_field): + utils.change_dict_value(cost_info, deny_field, None) + return cost_info + + @staticmethod + def _check_fields_with_data_source_permissions( + query: dict, data_source_vo: DataSource + ): + permissions = data_source_vo.permissions or {} + deny = permissions.get("deny", []) + + fields = query.get("fields", {}) + + for field_key in fields.keys(): + field_info = fields[field_key] + if _field_info_key := field_info.get("key"): + if _field_info_key in deny: + raise ERROR_PERMISSION_DENIED()