Skip to content
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
71 changes: 68 additions & 3 deletions frontend/service/studio_release_server/deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,71 @@ def _public_artifact_policy(bucket: str) -> dict[str, object]:
}


def _normalized_policy_statement(statement: dict[str, object]) -> dict[str, object]:
"""Normalize TOS scalar/list response variants for semantic comparison."""

normalized = dict(statement)
for field in ("Principal", "Action", "Resource"):
value = normalized.get(field)
values = value if isinstance(value, list) else [value]
normalized[field] = sorted(
values,
key=lambda item: json.dumps(
item,
sort_keys=True,
separators=(",", ":"),
),
)
return normalized


def _policy_statement_matches(
actual: dict[str, object],
expected: dict[str, object],
) -> bool:
return _normalized_policy_statement(actual) == _normalized_policy_statement(
expected
)


def _policy_matches(actual: object, expected: object) -> bool:
"""Compare complete policies while accepting only observed TOS normalization."""

if not isinstance(actual, dict) or not isinstance(expected, dict):
return False
actual_metadata = {
key: value for key, value in actual.items() if key != "Statement"
}
expected_metadata = {
key: value for key, value in expected.items() if key != "Statement"
}
if "Version" not in expected_metadata and actual_metadata.get("Version") == "1.0":
actual_metadata.pop("Version")
if actual_metadata != expected_metadata:
return False

def normalized_statements(policy: dict[str, object]) -> list[dict[str, object]]:
raw = policy.get("Statement", [])
values = [raw] if isinstance(raw, dict) else raw
if not isinstance(values, list) or not all(
isinstance(item, dict) for item in values
):
return []
statements = [_normalized_policy_statement(item) for item in values]
return sorted(
statements,
key=lambda item: json.dumps(
item,
sort_keys=True,
separators=(",", ":"),
),
)

actual_statements = normalized_statements(actual)
expected_statements = normalized_statements(expected)
return bool(actual_statements) and actual_statements == expected_statements


def _is_tos_not_found(error: Exception) -> bool:
return isinstance(error, KeyError) or getattr(error, "status_code", None) == 404

Expand Down Expand Up @@ -486,7 +551,7 @@ def _merge_public_artifact_policy(
for statement in statements
if statement.get("Sid") == _PUBLIC_ARTIFACT_POLICY_SID
]
if len(owned) > 1 or (owned and owned[0] != expected):
if len(owned) > 1 or (owned and not _policy_statement_matches(owned[0], expected)):
raise RuntimeError("Studio public artifact bucket policy has a conflict.")
if owned:
policy["Statement"] = statements
Expand Down Expand Up @@ -545,8 +610,8 @@ def _ensure_public_artifact_bucket(client: Any, provider: CloudProvider) -> str:
raise RuntimeError(
"Studio public artifact bucket policy verification failed."
) from error
verified, _ = _merge_public_artifact_policy(actual, bucket)
if verified != expected:
verified, verification_changed = _merge_public_artifact_policy(actual, bucket)
if verification_changed or not _policy_matches(verified, expected):
raise RuntimeError("Studio public artifact bucket policy verification failed.")
return bucket

Expand Down
9 changes: 9 additions & 0 deletions tests/cli/test_frontend_deploy_iam_vestack.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# 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 importlib
import json
Expand Down
14 changes: 14 additions & 0 deletions tests/cli/test_vestack_studio_deploy.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
# Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# 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.

from types import SimpleNamespace
from unittest.mock import MagicMock

Expand Down
14 changes: 14 additions & 0 deletions tests/cli/test_vestack_studio_image.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
# Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# 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.

from pathlib import Path

from veadk.utils.cloud_provider import (
Expand Down
14 changes: 14 additions & 0 deletions tests/frontend/server/test_agentkit_clients.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
# Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# 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.

from agentkit.sdk.tools.client import AgentkitToolsClient

from frontend.server.agentkit_clients import create_agentkit_client
Expand Down
14 changes: 14 additions & 0 deletions tests/integrations/test_ve_apig_vestack.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
# Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# 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 json
from types import SimpleNamespace
from unittest.mock import MagicMock
Expand Down
6 changes: 6 additions & 0 deletions tests/integrations/test_ve_faas_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# 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.

from types import SimpleNamespace
from unittest.mock import MagicMock
Expand Down
129 changes: 129 additions & 0 deletions tests/test_studio_release_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -2176,6 +2176,135 @@ def put_bucket_policy(self, **kwargs: Any) -> None:
]


def test_public_artifact_bucket_accepts_tos_normalized_policy() -> None:
bucket = "veadk-studio-public"

class _NotFoundError(Exception):
status_code = 404

class _Client:
def __init__(self) -> None:
self.policy: dict[str, object] | None = None
self.policy_puts = 0

def list_buckets(self) -> Any:
return SimpleNamespace(buckets=[SimpleNamespace(name=bucket)])

def get_bucket_tagging(self, **_kwargs: Any) -> Any:
return SimpleNamespace(tag_set=[SimpleNamespace(key="note", value="勿删")])

def get_bucket_policy(self, **_kwargs: Any) -> Any:
if self.policy is None:
raise _NotFoundError
statements = self.policy.get("Statement")
assert isinstance(statements, list) and len(statements) == 1
statement = statements[0]
assert isinstance(statement, dict)
actions = statement.get("Action")
resources = statement.get("Resource")
assert isinstance(actions, list) and len(actions) == 1
assert isinstance(resources, list) and len(resources) == 1
normalized = {
"Version": "1.0",
"Statement": {
**statement,
"Principal": [statement["Principal"]],
"Action": actions[0],
"Resource": resources[0],
},
}
return SimpleNamespace(policy=json.dumps(normalized))

def put_bucket_policy(self, **kwargs: Any) -> None:
self.policy = json.loads(kwargs["policy"])
self.policy_puts += 1

client = _Client()

assert release_deploy._ensure_public_artifact_bucket(client, "volcengine") == bucket
assert release_deploy._ensure_public_artifact_bucket(client, "volcengine") == bucket
assert client.policy_puts == 1


def test_public_artifact_bucket_accepts_normalized_existing_owned_statement() -> None:
bucket = "veadk-studio-public"

class _Client:
def list_buckets(self) -> Any:
return SimpleNamespace(buckets=[SimpleNamespace(name=bucket)])

def get_bucket_tagging(self, **_kwargs: Any) -> Any:
return SimpleNamespace(tag_set=[SimpleNamespace(key="note", value="勿删")])

def get_bucket_policy(self, **_kwargs: Any) -> Any:
return SimpleNamespace(
policy=json.dumps(
{
"Version": "1.0",
"Statement": {
"Sid": "PublicReadStudioRuntimeArtifacts",
"Effect": "Allow",
"Principal": ["*"],
"Action": "tos:GetObject",
"Resource": (
f"trn:tos:::{bucket}/veadk/studio/artifacts/v1/*"
),
},
}
)
)

def put_bucket_policy(self, **_kwargs: Any) -> None:
raise AssertionError("normalized matching policy must not be rewritten")

assert (
release_deploy._ensure_public_artifact_bucket(_Client(), "volcengine") == bucket
)


def test_public_artifact_bucket_rejects_dropped_unrelated_policy() -> None:
bucket = "veadk-studio-public"
unrelated = {
"Sid": "ExistingPrivateAutomation",
"Effect": "Allow",
"Principal": {"Service": "internal"},
"Action": ["tos:PutObject"],
"Resource": [f"trn:tos:::{bucket}/internal/*"],
}

class _Client:
def __init__(self) -> None:
self.policy_puts = 0

def list_buckets(self) -> Any:
return SimpleNamespace(buckets=[SimpleNamespace(name=bucket)])

def get_bucket_tagging(self, **_kwargs: Any) -> Any:
return SimpleNamespace(tag_set=[SimpleNamespace(key="note", value="勿删")])

def get_bucket_policy(self, **_kwargs: Any) -> Any:
statements = [unrelated]
if self.policy_puts:
statements = [
{
"Sid": "PublicReadStudioRuntimeArtifacts",
"Effect": "Allow",
"Principal": ["*"],
"Action": "tos:GetObject",
"Resource": (f"trn:tos:::{bucket}/veadk/studio/artifacts/v1/*"),
}
]
return SimpleNamespace(
policy=json.dumps({"Version": "2012-10-17", "Statement": statements})
)

def put_bucket_policy(self, **_kwargs: Any) -> None:
self.policy_puts += 1

with pytest.raises(RuntimeError, match="policy verification failed"):
release_deploy._ensure_public_artifact_bucket(_Client(), "volcengine")


def test_public_artifact_bucket_rejects_owned_policy_conflict() -> None:
with pytest.raises(RuntimeError, match="policy has a conflict"):
release_deploy._merge_public_artifact_policy(
Expand Down
9 changes: 9 additions & 0 deletions veadk/cli/frontend_deploy_iam_vestack.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# 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.

"""VeStack IAM role provisioning for a VeFaaS-hosted Studio."""

Expand Down
9 changes: 9 additions & 0 deletions veadk/cli/frontend_sandbox_managed_tool_vestack.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# 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.

"""VeStack per-agent Tool provisioning for Studio Sandbox sessions."""

Expand Down
9 changes: 9 additions & 0 deletions veadk/cli/vestack_studio_deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# 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.

"""Deploy Studio as a VeStack VeFaaS image function behind APIG."""

Expand Down
Loading