diff --git a/aliyun/log/__init__.py b/aliyun/log/__init__.py index 52471f6..9544eba 100755 --- a/aliyun/log/__init__.py +++ b/aliyun/log/__init__.py @@ -16,6 +16,7 @@ from .logitem import LogItem from .consumer_group_request import * from .external_store_config import * +from .resource_policy import ResourcePolicyResourceType # response class from .consumer_group_response import * @@ -55,6 +56,7 @@ from .multimodal_config_response import GetLogStoreMultimodalConfigurationResponse, \ PutLogStoreMultimodalConfigurationResponse from .object_response import PutObjectResponse, GetObjectResponse +from .resource_policy_response import PutResourcePolicyResponse, GetResourcePolicyResponse, DeleteResourcePolicyResponse from .store_view import StoreView, StoreViewStore from .store_view_response import CreateStoreViewResponse, UpdateStoreViewResponse, DeleteStoreViewResponse, ListStoreViewsResponse, GetStoreViewResponse diff --git a/aliyun/log/__init__.pyi b/aliyun/log/__init__.pyi index 8a86130..da65871 100644 --- a/aliyun/log/__init__.pyi +++ b/aliyun/log/__init__.pyi @@ -15,6 +15,7 @@ from .version import __version__ as __version__ from .logitem import LogItem as LogItem from .consumer_group_request import CreateConsumerGroupRequest as CreateConsumerGroupRequest, ConsumerGroupGetCheckPointRequest as ConsumerGroupGetCheckPointRequest, ConsumerGroupHeartBeatRequest as ConsumerGroupHeartBeatRequest, ConsumerGroupUpdateCheckPointRequest as ConsumerGroupUpdateCheckPointRequest from .external_store_config import ExternalStoreConfig as ExternalStoreConfig, ExternalStoreConfigBase as ExternalStoreConfigBase, ExternalStoreCsvConfig as ExternalStoreCsvConfig, ExternalStoreOssConfig as ExternalStoreOssConfig +from .resource_policy import ResourcePolicyResourceType as ResourcePolicyResourceType from .consumer_group_response import ConsumerGroupEntity as ConsumerGroupEntity, ConsumerGroupCheckPointResponse as ConsumerGroupCheckPointResponse, ConsumerGroupHeartBeatResponse as ConsumerGroupHeartBeatResponse, ConsumerGroupUpdateCheckPointResponse as ConsumerGroupUpdateCheckPointResponse, CreateConsumerGroupResponse as CreateConsumerGroupResponse, DeleteConsumerGroupResponse as DeleteConsumerGroupResponse, ListConsumerGroupResponse as ListConsumerGroupResponse, UpdateConsumerGroupResponse as UpdateConsumerGroupResponse from .cursor_response import GetCursorResponse as GetCursorResponse @@ -54,6 +55,7 @@ from .metering_mode_response import GetLogStoreMeteringModeResponse as GetLogSto from .multimodal_config_response import GetLogStoreMultimodalConfigurationResponse as GetLogStoreMultimodalConfigurationResponse, \ PutLogStoreMultimodalConfigurationResponse as PutLogStoreMultimodalConfigurationResponse from .object_response import PutObjectResponse as PutObjectResponse, GetObjectResponse as GetObjectResponse +from .resource_policy_response import PutResourcePolicyResponse as PutResourcePolicyResponse, GetResourcePolicyResponse as GetResourcePolicyResponse, DeleteResourcePolicyResponse as DeleteResourcePolicyResponse from .store_view import StoreView as StoreView, StoreViewStore as StoreViewStore from .store_view_response import CreateStoreViewResponse as CreateStoreViewResponse, UpdateStoreViewResponse as UpdateStoreViewResponse, DeleteStoreViewResponse as DeleteStoreViewResponse, ListStoreViewsResponse as ListStoreViewsResponse, GetStoreViewResponse as GetStoreViewResponse diff --git a/aliyun/log/logclient.py b/aliyun/log/logclient.py index f78c0c3..ca5c421 100644 --- a/aliyun/log/logclient.py +++ b/aliyun/log/logclient.py @@ -53,8 +53,10 @@ from .putlogsresponse import PutLogsResponse from .shard_response import * from .shipper_response import * -from .resource_response import * -from .resource_params import * +from .resource_response import * +from .resource_params import * +from .resource_policy import ResourcePolicyResourceType +from .resource_policy_response import * from .tag_response import GetResourceTagsResponse from .topostore_response import * from .topostore_params import * @@ -3298,6 +3300,116 @@ def delete_project(self, project_name): (resp, header) = self._send("DELETE", project_name, None, resource, params, headers) return DeleteProjectResponse(header, resp) + @staticmethod + def _validate_resource_policy_target(project_name, resource_type): + if not project_name: + raise LogException("InvalidParameter", "project_name must not be empty") + if resource_type not in ( + ResourcePolicyResourceType.PROJECT, + ResourcePolicyResourceType.LOGSTORE): + raise LogException( + "InvalidParameter", + "resource_type must be project or logstore" + ) + + def put_resource_policy(self, project_name, resource_type, policy_document, + resource_name=None, dry_run=False): + """Create or update a resource policy for a project or logstore. + + :type project_name: string + :param project_name: the project name + + :type resource_type: string + :param resource_type: ``project`` or ``logstore`` + + :type policy_document: string + :param policy_document: the JSON policy document + + :type resource_name: string + :param resource_name: optional resource name; required by the service for logstore policies + + :type dry_run: bool + :param dry_run: validate the policy without persisting it + + :return: PutResourcePolicyResponse + + :raise: LogException + """ + self._validate_resource_policy_target(project_name, resource_type) + if not policy_document: + raise LogException("InvalidParameter", "policy_document must not be empty") + + body = { + "resourceType": resource_type, + "policyDocument": policy_document, + "dryRun": bool(dry_run), + } + if resource_name: + body["resourceName"] = resource_name + + body_str = six.b(json.dumps(body)) + headers = { + "Content-Type": "application/json", + "x-log-bodyrawsize": str(len(body_str)), + } + (resp, header) = self._send( + "PUT", project_name, body_str, "/resource-policies", {}, headers + ) + return PutResourcePolicyResponse(header, resp) + + def get_resource_policy(self, project_name, resource_type, resource_name=None): + """Get a resource policy for a project or logstore. + + :type project_name: string + :param project_name: the project name + + :type resource_type: string + :param resource_type: ``project`` or ``logstore`` + + :type resource_name: string + :param resource_name: optional resource name; required by the service for logstore policies + + :return: GetResourcePolicyResponse + + :raise: LogException + """ + self._validate_resource_policy_target(project_name, resource_type) + params = {"resourceType": resource_type} + if resource_name: + params["resourceName"] = resource_name + + (resp, header) = self._send( + "GET", project_name, None, "/resource-policies", params, + {"Content-Type": "application/json"} + ) + return GetResourcePolicyResponse(resp, header) + + def delete_resource_policy(self, project_name, resource_type, resource_name=None): + """Delete a resource policy for a project or logstore. + + :type project_name: string + :param project_name: the project name + + :type resource_type: string + :param resource_type: ``project`` or ``logstore`` + + :type resource_name: string + :param resource_name: optional resource name; required by the service for logstore policies + + :return: DeleteResourcePolicyResponse + + :raise: LogException + """ + self._validate_resource_policy_target(project_name, resource_type) + params = {"resourceType": resource_type} + if resource_name: + params["resourceName"] = resource_name + + (resp, header) = self._send( + "DELETE", project_name, None, "/resource-policies", params, {} + ) + return DeleteResourcePolicyResponse(header, resp) + def change_resource_group(self, resource_id, resource_group_id, resource_type="PROJECT"): """ Update the resource group of project diff --git a/aliyun/log/logclient.pyi b/aliyun/log/logclient.pyi index fa0e78a..51f758c 100644 --- a/aliyun/log/logclient.pyi +++ b/aliyun/log/logclient.pyi @@ -55,6 +55,7 @@ from .putlogsresponse import PutLogsResponse from .rebuild_index_response import CreateRebuildIndexResponse, GetRebuildIndexResponse from .resource_params import Resource, ResourceRecord from .resource_response import CreateRecordResponse, CreateResourceResponse, DeleteRecordResponse, DeleteResourceResponse, GetRecordResponse, GetResourceResponse, ListRecordResponse, ListResourcesResponse, UpdateRecordResponse, UpdateResourceResponse, UpsertRecordResponse +from .resource_policy_response import DeleteResourcePolicyResponse, GetResourcePolicyResponse, PutResourcePolicyResponse from .scheduled_sql import ScheduledSQL, ScheduledSQLConfiguration from .scheduled_sql_response import CreateScheduledSQLResponse, DeleteScheduledSQLResponse, GetScheduledSQLResponse, GetScheduledSqlJobInstanceResponse, ListScheduledSQLResponse, ListScheduledSqlJobInstancesResponse, ModifyScheduledSqlJobStateResponse, UpdateScheduledSQLResponse from .shard_response import DeleteShardResponse, ListShardResponse @@ -170,6 +171,9 @@ class LogClient(object): def update_project(self, project_name: str, project_des: str) -> UpdateProjectResponse: ... def get_project(self, project_name: str) -> GetProjectResponse: ... def delete_project(self, project_name: str) -> DeleteProjectResponse: ... + def put_resource_policy(self, project_name: str, resource_type: str, policy_document: str, resource_name: Optional[str] = ..., dry_run: bool = ...) -> PutResourcePolicyResponse: ... + def get_resource_policy(self, project_name: str, resource_type: str, resource_name: Optional[str] = ...) -> GetResourcePolicyResponse: ... + def delete_resource_policy(self, project_name: str, resource_type: str, resource_name: Optional[str] = ...) -> DeleteResourcePolicyResponse: ... def change_resource_group(self, resource_id: str, resource_group_id: str, resource_type: str = ...) -> LogResponse: ... def tag_project(self, project_name: str, **tags: Any) -> LogResponse: ... def untag_project(self, project_name: str, *tag_keys: Any) -> LogResponse: ... diff --git a/aliyun/log/resource_policy.py b/aliyun/log/resource_policy.py new file mode 100644 index 0000000..4172708 --- /dev/null +++ b/aliyun/log/resource_policy.py @@ -0,0 +1,12 @@ +#!/usr/bin/env python +# encoding: utf-8 + +# Copyright (C) Alibaba Cloud Computing +# All rights reserved. + + +class ResourcePolicyResourceType(object): + """Resource types supported by the Resource Policy API.""" + + PROJECT = "project" + LOGSTORE = "logstore" diff --git a/aliyun/log/resource_policy.pyi b/aliyun/log/resource_policy.pyi new file mode 100644 index 0000000..54dd726 --- /dev/null +++ b/aliyun/log/resource_policy.pyi @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- + + +class ResourcePolicyResourceType: + PROJECT: str + LOGSTORE: str diff --git a/aliyun/log/resource_policy_response.py b/aliyun/log/resource_policy_response.py new file mode 100644 index 0000000..b9b383c --- /dev/null +++ b/aliyun/log/resource_policy_response.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python +# encoding: utf-8 + +# Copyright (C) Alibaba Cloud Computing +# All rights reserved. + +from .logresponse import LogResponse +from .util import Util + +__all__ = [ + "PutResourcePolicyResponse", + "GetResourcePolicyResponse", + "DeleteResourcePolicyResponse", +] + + +class PutResourcePolicyResponse(LogResponse): + """The response of the put_resource_policy API.""" + + def __init__(self, header, resp=""): + LogResponse.__init__(self, header, resp) + + +class GetResourcePolicyResponse(LogResponse): + """The response of the get_resource_policy API.""" + + def __init__(self, resp, header): + LogResponse.__init__(self, header, resp) + self.resource_type = Util.convert_unicode_to_str(resp["resourceType"]) + self.resource_name = Util.convert_unicode_to_str(resp.get("resourceName", "")) + self.policy_document = Util.convert_unicode_to_str(resp["policyDocument"]) + self.revision = int(resp["revision"]) + self.create_time = int(resp["createTime"]) + self.update_time = int(resp["updateTime"]) + + def get_resource_type(self): + return self.resource_type + + def get_resource_name(self): + return self.resource_name + + def get_policy_document(self): + return self.policy_document + + def get_revision(self): + return self.revision + + def get_create_time(self): + return self.create_time + + def get_update_time(self): + return self.update_time + + def log_print(self): + print("GetResourcePolicyResponse:") + print("headers:", self.get_all_headers()) + print("resource_type:", self.resource_type) + print("resource_name:", self.resource_name) + print("policy_document:", self.policy_document) + print("revision:", self.revision) + print("create_time:", self.create_time) + print("update_time:", self.update_time) + + +class DeleteResourcePolicyResponse(LogResponse): + """The response of the delete_resource_policy API.""" + + def __init__(self, header, resp=""): + LogResponse.__init__(self, header, resp) diff --git a/aliyun/log/resource_policy_response.pyi b/aliyun/log/resource_policy_response.pyi new file mode 100644 index 0000000..5352107 --- /dev/null +++ b/aliyun/log/resource_policy_response.pyi @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +from typing import Any, Dict + +from .logresponse import LogResponse + + +class PutResourcePolicyResponse(LogResponse): + def __init__(self, header: Dict[str, Any], resp: Any = ...) -> None: ... + + +class GetResourcePolicyResponse(LogResponse): + resource_type: str + resource_name: str + policy_document: str + revision: int + create_time: int + update_time: int + def __init__(self, resp: Dict[str, Any], header: Dict[str, Any]) -> None: ... + def get_resource_type(self) -> str: ... + def get_resource_name(self) -> str: ... + def get_policy_document(self) -> str: ... + def get_revision(self) -> int: ... + def get_create_time(self) -> int: ... + def get_update_time(self) -> int: ... + def log_print(self) -> None: ... + + +class DeleteResourcePolicyResponse(LogResponse): + def __init__(self, header: Dict[str, Any], resp: Any = ...) -> None: ... diff --git a/aliyun/log/version.py b/aliyun/log/version.py index d444b64..dff1132 100644 --- a/aliyun/log/version.py +++ b/aliyun/log/version.py @@ -1,4 +1,4 @@ -__version__ = '0.9.49' +__version__ = '0.9.50' import sys OS_VERSION = str(sys.platform) diff --git a/tests/unit/test_resource_policy.py b/tests/unit/test_resource_policy.py new file mode 100644 index 0000000..57f624f --- /dev/null +++ b/tests/unit/test_resource_policy.py @@ -0,0 +1,214 @@ +# encoding: utf-8 + +from __future__ import absolute_import + +import json +import re + +import pytest +import responses + +from aliyun.log import LogException, ResourcePolicyResourceType +from tests._helpers.fakes import make_client, mock_sls_response + + +PROJECT = "resource-policy-project" +LOGSTORE = "resource-policy-logstore" +POLICY = '{"Version":"1","Statement":[]}' + + +@responses.activate +def test_put_resource_policy_sends_project_and_logstore_bodies(): + client = make_client(endpoint="cn-mock.example.com", project=PROJECT) + captured = [] + + def callback(request): + captured.append((json.loads(request.body.decode("utf-8")), request.headers)) + return 200, {"x-log-requestid": "mock-request-id"}, "{}" + + responses.add_callback( + responses.PUT, + re.compile( + r"https?://resource-policy-project\.cn-mock\.example\.com.*?" + r"/resource-policies$" + ), + callback=callback, + ) + + project_response = client.put_resource_policy( + PROJECT, ResourcePolicyResourceType.PROJECT, POLICY + ) + logstore_response = client.put_resource_policy( + PROJECT, + ResourcePolicyResourceType.LOGSTORE, + POLICY, + resource_name=LOGSTORE, + dry_run=True, + ) + + assert captured[0][0] == { + "resourceType": "project", + "policyDocument": POLICY, + "dryRun": False, + } + assert captured[1][0] == { + "resourceType": "logstore", + "resourceName": LOGSTORE, + "policyDocument": POLICY, + "dryRun": True, + } + for body, headers in captured: + assert headers["Content-Type"] == "application/json" + assert int(headers["x-log-bodyrawsize"]) == len( + json.dumps(body).encode("utf-8") + ) + assert project_response.get_request_id() == "mock-request-id" + assert logstore_response.get_request_id() == "mock-request-id" + + +@responses.activate +def test_get_resource_policy_sends_query_and_parses_response(): + client = make_client(endpoint="cn-mock.example.com", project=PROJECT) + mock_sls_response( + responses, + "GET", + re.compile( + r"https?://resource-policy-project\.cn-mock\.example\.com.*?" + r"/resource-policies\?(?:resourceType=logstore" + r"&resourceName=resource-policy-logstore|" + r"resourceName=resource-policy-logstore&resourceType=logstore)$" + ), + body={ + "resourceType": "logstore", + "resourceName": LOGSTORE, + "policyDocument": POLICY, + "revision": 3, + "createTime": 10, + "updateTime": 20, + }, + ) + + response = client.get_resource_policy( + PROJECT, ResourcePolicyResourceType.LOGSTORE, LOGSTORE + ) + + assert response.get_resource_type() == ResourcePolicyResourceType.LOGSTORE + assert response.get_resource_name() == LOGSTORE + assert response.get_policy_document() == POLICY + assert response.get_revision() == 3 + assert response.get_create_time() == 10 + assert response.get_update_time() == 20 + + +@responses.activate +def test_get_project_resource_policy_omits_resource_name_and_uses_response_target(): + client = make_client(endpoint="cn-mock.example.com", project=PROJECT) + mock_sls_response( + responses, + "GET", + re.compile( + r"https?://resource-policy-project\.cn-mock\.example\.com.*?" + r"/resource-policies\?resourceType=project$" + ), + body={ + "resourceType": "logstore", + "resourceName": "unexpected", + "policyDocument": POLICY, + "revision": 1, + "createTime": 2, + "updateTime": 3, + }, + ) + + response = client.get_resource_policy( + PROJECT, ResourcePolicyResourceType.PROJECT + ) + + assert response.get_resource_type() == ResourcePolicyResourceType.LOGSTORE + assert response.get_resource_name() == "unexpected" + + +@responses.activate +def test_delete_resource_policy_sends_target_query(): + client = make_client(endpoint="cn-mock.example.com", project=PROJECT) + mock_sls_response( + responses, + "DELETE", + re.compile( + r"https?://resource-policy-project\.cn-mock\.example\.com.*?" + r"/resource-policies\?resourceType=project$" + ), + ) + + response = client.delete_resource_policy( + PROJECT, ResourcePolicyResourceType.PROJECT + ) + + assert response.get_request_id() == "mock-request-id" + + +def test_put_resource_policy_rejects_empty_document(): + client = make_client(endpoint="cn-mock.example.com", project=PROJECT) + + with pytest.raises(LogException) as excinfo: + client.put_resource_policy( + PROJECT, ResourcePolicyResourceType.PROJECT, "" + ) + + assert excinfo.value.get_error_code() == "InvalidParameter" + + +@pytest.mark.parametrize( + "project,resource_type", + [ + ("", ResourcePolicyResourceType.PROJECT), + (PROJECT, ""), + (PROJECT, "dashboard"), + ], +) +def test_resource_policy_rejects_missing_target(project, resource_type): + client = make_client(endpoint="cn-mock.example.com", project=PROJECT) + + with pytest.raises(LogException) as excinfo: + client.get_resource_policy(project, resource_type) + + assert excinfo.value.get_error_code() == "InvalidParameter" + + +def test_resource_policy_target_validation_is_delegated_to_server(monkeypatch): + client = make_client(endpoint="cn-mock.example.com", project=PROJECT) + calls = [] + + def capture(method, project, body, resource, params, headers, **kwargs): + calls.append((method, body, params)) + if method == "GET": + return ({ + "resourceType": "logstore", + "policyDocument": "{}", + "revision": 1, + "createTime": 2, + "updateTime": 3, + }, {}) + return ({}, {}) + + monkeypatch.setattr(client, "_send", capture) + + client.put_resource_policy( + PROJECT, + ResourcePolicyResourceType.PROJECT, + POLICY, + resource_name=LOGSTORE, + ) + client.get_resource_policy( + PROJECT, ResourcePolicyResourceType.LOGSTORE, resource_name="" + ) + client.delete_resource_policy( + PROJECT, ResourcePolicyResourceType.PROJECT, resource_name=LOGSTORE + ) + + assert json.loads(calls[0][1].decode("utf-8"))["resourceName"] == LOGSTORE + assert calls[1][2] == {"resourceType": "logstore"} + assert calls[2][2] == { + "resourceType": "project", + "resourceName": LOGSTORE, + }