mirror of
https://github.com/NVIDIA/OpenShell.git
synced 2026-10-20 06:00:06 +08:00
feat(policy)!: finalize public policy protobuf contract
BREAKING CHANGE: Policy YAML and public policy APIs now use the protobuf-shaped openshell.policy.v1.PolicyDocument contract. Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com>
This commit is contained in:
@@ -7,6 +7,7 @@ from __future__ import annotations
|
||||
|
||||
from .errors import ErrorInfo, FieldViolation, GatewayError, from_grpc_error
|
||||
from .mutations import DeletionOutcome, DeletionResult
|
||||
from .policy import PolicyDocument, validate_policy_document
|
||||
from .sandbox import (
|
||||
ClientCredentialsAuth,
|
||||
ExecChunk,
|
||||
@@ -44,6 +45,7 @@ __all__ = [
|
||||
"GatewayError",
|
||||
"Page",
|
||||
"Pager",
|
||||
"PolicyDocument",
|
||||
"Sandbox",
|
||||
"SandboxClient",
|
||||
"SandboxError",
|
||||
@@ -57,4 +59,5 @@ __all__ = [
|
||||
"WorkspaceRef",
|
||||
"__version__",
|
||||
"from_grpc_error",
|
||||
"validate_policy_document",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
"""Validation helpers for the public authored policy contract."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import protovalidate
|
||||
|
||||
from ._proto import policy_pb2
|
||||
|
||||
PolicyDocument = policy_pb2.PolicyDocument
|
||||
|
||||
|
||||
def validate_policy_document(document: PolicyDocument) -> PolicyDocument:
|
||||
"""Validate a policy against the portable rules declared in policy.proto."""
|
||||
protovalidate.validate(document)
|
||||
return document
|
||||
@@ -0,0 +1,54 @@
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import protovalidate
|
||||
import pytest
|
||||
|
||||
from openshell.policy import PolicyDocument, validate_policy_document
|
||||
|
||||
|
||||
def test_policy_document_validation_runs_portable_rules() -> None:
|
||||
assert validate_policy_document(PolicyDocument(version=1)).version == 1
|
||||
|
||||
|
||||
def test_policy_document_validation_reports_portable_rule_ids() -> None:
|
||||
documents: list[tuple[str, PolicyDocument, str]] = []
|
||||
|
||||
documents.append(("version", PolicyDocument(), "uint32.const"))
|
||||
|
||||
missing_ports = PolicyDocument(version=1)
|
||||
missing_ports.network_policies["api"].endpoints.add(host="api.example.com")
|
||||
documents.append(("missing ports", missing_ports, "repeated.min_items"))
|
||||
|
||||
duplicate_ports = PolicyDocument(version=1)
|
||||
duplicate_ports.network_policies["api"].endpoints.add(
|
||||
host="api.example.com", ports=[443, 443]
|
||||
)
|
||||
documents.append(("duplicate ports", duplicate_ports, "repeated.unique"))
|
||||
|
||||
port_range = PolicyDocument(version=1)
|
||||
port_range.network_policies["api"].endpoints.add(
|
||||
host="api.example.com", ports=[65536]
|
||||
)
|
||||
documents.append(("port range", port_range, "uint32.gte_lte"))
|
||||
|
||||
binary_path = PolicyDocument(version=1)
|
||||
binary_path.network_policies["api"].binaries.add(path="")
|
||||
documents.append(("binary path", binary_path, "string.min_len"))
|
||||
|
||||
matcher_choice = PolicyDocument(version=1)
|
||||
endpoint = matcher_choice.network_policies["api"].endpoints.add(
|
||||
host="api.example.com", ports=[443]
|
||||
)
|
||||
endpoint.rules.add().allow.query["owner"].SetInParent()
|
||||
documents.append(("matcher choice", matcher_choice, "required"))
|
||||
|
||||
for name, document, rule_id in documents:
|
||||
with pytest.raises(protovalidate.ValidationError) as error:
|
||||
validate_policy_document(document)
|
||||
|
||||
assert rule_id in {
|
||||
violation.proto.rule_id for violation in error.value.violations
|
||||
}, name
|
||||
@@ -30,6 +30,7 @@ from ._proto import (
|
||||
)
|
||||
from .errors import _error_mapping_channel
|
||||
from .mutations import DeletionOutcome, DeletionResult
|
||||
from .policy import validate_policy_document
|
||||
|
||||
_ClientCallDetailsBase = namedtuple(
|
||||
"_ClientCallDetailsBase",
|
||||
@@ -766,6 +767,8 @@ class SandboxClient:
|
||||
labels: Mapping[str, str] | None = None,
|
||||
) -> SandboxRef:
|
||||
request_spec = spec if spec is not None else _default_spec()
|
||||
if request_spec.HasField("policy"):
|
||||
validate_policy_document(request_spec.policy)
|
||||
response = self._stub.CreateSandbox(
|
||||
openshell_pb2.CreateSandboxRequest(
|
||||
workspace_scope=_workspace_scope(workspace),
|
||||
@@ -792,6 +795,8 @@ class SandboxClient:
|
||||
if not workload_template.strip():
|
||||
raise SandboxError("workload_template is required")
|
||||
request_spec = spec if spec is not None else openshell_pb2.SandboxSpec()
|
||||
if request_spec.HasField("policy"):
|
||||
validate_policy_document(request_spec.policy)
|
||||
response = self._stub.CreateSandbox(
|
||||
openshell_pb2.CreateSandboxRequest(
|
||||
workspace_scope=_workspace_scope(workspace),
|
||||
|
||||
Reference in New Issue
Block a user