Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: project perm rules with OR op #941

Merged
merged 1 commit into from
May 24, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 52 additions & 28 deletions bcs-app/backend/components/iam/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
# specific language governing permissions and limitations under the License.
#
from typing import Dict, Optional

from django.conf import settings

try:
Expand All @@ -26,11 +28,6 @@


class IAMClient(Client):
def _list_policies(self, data):
path = f"/api/v1/systems/{self._app_code}/policies"
ok, message, data = self._call_iam_api(http_get, path, data)
return ok, message, data

def list_policies(self, data):
policies = []
page, page_size = 1, 100
Expand Down Expand Up @@ -76,16 +73,17 @@ def grant_resource_creator_actions(self, bk_token, bk_username, data):

return True, "success"

def _list_policies(self, data):
path = f"/api/v1/systems/{self._app_code}/policies"
ok, message, data = self._call_iam_api(http_get, path, data)
return ok, message, data


class BCSIAM(IAM):
def __init__(self, app_code, app_secret, bk_iam_host, bk_paas_host):
self._client = IAMClient(app_code, app_secret, bk_iam_host, bk_paas_host)

def make_dict_filter(self, request, key_mapping=None):
"""
仅支持{'op': 'in', 'field': 'project.id', 'value': [1, 2, 3]}
或者{'op': 'eq', 'field': 'project.id', 'value': 1}
"""
def do_policy_query(self, request) -> Optional[Dict]:
dellkeji marked this conversation as resolved.
Show resolved Hide resolved
# 1. validate
if not isinstance(request, Request):
raise AuthInvalidRequest("request should be instance of iam.auth.models.Request")
Expand All @@ -99,22 +97,7 @@ def make_dict_filter(self, request, key_mapping=None):
if not policies:
return None

op = policies["op"]
if op not in [OP.IN, OP.EQ, OP.ANY]:
raise AuthInvalidRequest("make_dict_filter only support OP.IN or OP.EQ or OP.ANY")

value = policies["value"]
if op == OP.EQ:
value = [
value,
]

field = policies["field"]
if key_mapping:
k = key_mapping.get(field) or field
return {k: value, "op": op}

return {field: value, "op": op}
return policies

def _match_resource_id(self, expression, resource_type_id, resource_id):
if expression["op"] in [OP.AND, OP.OR]:
Expand Down Expand Up @@ -265,10 +248,14 @@ def can_edit(self, username, project_id, raise_exception=False):
action_id = self.actions.EDIT.value
return self._allowed_do_project_inst(username, action_id, project_id, raise_exception)

def make_view_perm_filter(self, username):
def make_view_perm_filter(self, username: str) -> Dict:
action_id = self.actions.VIEW.value
request = self._make_request_with_resources(username, action_id)
return self.iam.make_dict_filter(request, {"project.id": "project_id_list"})
policies = self.iam.do_policy_query(request)
if not policies:
return {}

return self._make_dict_filter(policies)

def op_is_any(self, filter):
if not filter:
Expand Down Expand Up @@ -302,3 +289,40 @@ def query_authorized_users(self, project_id, action_id):

def grant_related_action_perms(self, username, project_id, project_name):
return self.iam.grant_resource_creator_action(username, self.resource_type_id, project_id, project_name)

def _make_dict_filter(self, policies: Dict) -> Dict:
"""
基于策略规则, 生成 project_id 过滤器。

:params policies: 权限中心返回的策略规则,如 {'op': OP.IN, 'value': [2, 1], 'field': 'project.id'}
:returns : project_id 过滤器, 如 {'project_id_list': [1, 2], 'op': OP.IN}
"""
op = policies["op"]
dellkeji marked this conversation as resolved.
Show resolved Hide resolved
if op not in [OP.IN, OP.EQ, OP.ANY, OP.OR, OP.AND]:
raise AuthInvalidRequest(f"make_dict_filter does not support op:{op}")

field, key = "project.id", "project_id_list"

if op == OP.EQ:
return {key: [policies["value"]], "op": OP.IN}

if op in [OP.IN, OP.ANY]:
return {key: policies["value"] or [], "op": op}

# 如果 op 是 OP.OR 或 OP.AND,只处理一级,不考虑嵌套的情况
value_list = []
for policy in policies["content"]:
if policy["field"] != field:
continue

op = policy["op"]
if op == OP.ANY:
return {key: policy["value"] or [], "op": op}

value = policy["value"]
if op == OP.IN:
value_list.extend(value)
elif op == OP.EQ:
value_list.append(value)

return {key: list(set(value_list)), "op": OP.IN}
6 changes: 6 additions & 0 deletions bcs-app/backend/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,12 @@ def get_logging_config(log_level, rds_hander_settings=None, log_path="app.log"):
"handlers": ["console", "logstash_redis", "file"],
"level": "DEBUG",
},
# 配置iam logger
'iam': {
'handlers': ['file'],
'level': os.getenv('IAM_LOG_LEVEL', 'ERROR'),
'propagate': False,
},
"sentry_logger": {"handlers": ["sentry"], "level": "ERROR"},
},
}
Expand Down
55 changes: 55 additions & 0 deletions bcs-app/backend/tests/components/test_permissions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# -*- coding: utf-8 -*-
#
# Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community Edition) available.
# Copyright (C) 2017-2019 THL A29 Limited, a Tencent company. All rights reserved.
# Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://opensource.org/licenses/MIT
#
# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
# specific language governing permissions and limitations under the License.
#
import pytest
from iam import OP

from backend.components.iam.permissions import ProjectPermission

test_dict_filter_data = [
({'op': OP.IN, 'value': [2, 1], 'field': 'project.id'}, {'project_id_list': [1, 2], 'op': OP.IN}),
({'op': OP.EQ, 'value': 1, 'field': 'project.id'}, {'project_id_list': [1], 'op': OP.IN}),
({'op': OP.ANY, 'value': [], 'field': 'project.id'}, {'project_id_list': [], 'op': OP.ANY}),
(
{
'op': OP.OR,
'content': [
{'op': OP.IN, 'field': 'project.id', 'value': [2, 1, 5]},
{'op': OP.ANY, 'field': 'project.id', 'value': []},
{'op': OP.EQ, 'field': 'project.id', 'value': 3},
{'op': OP.IN, 'field': 'project.id', 'value': [4]},
],
},
{'project_id_list': [], 'op': OP.ANY},
),
(
{
'op': OP.OR,
'content': [
{'op': OP.IN, 'field': 'project.id', 'value': [2, 1, 5]},
{'op': OP.EQ, 'field': 'project.id', 'value': 3},
{'op': OP.IN, 'field': 'fake_project.id', 'value': [4, 6]},
],
},
{'project_id_list': [1, 2, 3, 5], 'op': OP.IN},
),
]


class TestProjectPermission:
@pytest.mark.parametrize('policies, expected_dict_filter', test_dict_filter_data)
def test_make_dict_filter(self, policies, expected_dict_filter):
project_perm = ProjectPermission()
dict_filter = project_perm._make_dict_filter(policies)
assert dict_filter['project_id_list'].sort() == expected_dict_filter['project_id_list'].sort()
assert dict_filter['op'] == expected_dict_filter['op']