Skip to content

Commit

Permalink
feat: add step for exclude data field based on datasource's permissio…
Browse files Browse the repository at this point in the history
…ns info

Signed-off-by: ImMin5 <[email protected]>
  • Loading branch information
ImMin5 committed Jun 20, 2024
1 parent 51b6a42 commit a5aa358
Show file tree
Hide file tree
Showing 4 changed files with 174 additions and 13 deletions.
34 changes: 25 additions & 9 deletions src/spaceone/cost_analysis/manager/cost_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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"]

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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.",
)
Empty file.
34 changes: 34 additions & 0 deletions src/spaceone/cost_analysis/model/cost/response.py
Original file line number Diff line number Diff line change
@@ -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
119 changes: 115 additions & 4 deletions src/spaceone/cost_analysis/service/cost_service.py
Original file line number Diff line number Diff line change
@@ -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__)
Expand All @@ -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(
Expand Down Expand Up @@ -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"])
Expand Down Expand Up @@ -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:
Expand All @@ -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",
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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
)
Expand Down Expand Up @@ -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()

0 comments on commit a5aa358

Please sign in to comment.