mirror of
https://github.com/NVIDIA/OpenShell.git
synced 2026-10-31 07:44:57 +08:00
fix(sdk): canonicalize projected policy lists
Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com>
This commit is contained in:
@@ -245,7 +245,7 @@ func publicNetworkEndpointFromInternalProto(endpoint *sbv1.NetworkEndpoint) *pol
|
||||
SigningRegion: endpoint.GetSigningRegion(),
|
||||
}
|
||||
if ports := endpoint.GetPorts(); len(ports) > 0 {
|
||||
result.Ports = append([]uint32(nil), ports...)
|
||||
result.Ports = stableUnique(ports)
|
||||
} else if port := endpoint.GetPort(); port != 0 {
|
||||
// Older internal records may still use the legacy scalar port. The
|
||||
// authored contract has only the canonical list representation.
|
||||
@@ -435,7 +435,7 @@ func publicMatcherMapFromInternalProto(matchers map[string]*sbv1.L7QueryMatcher)
|
||||
func publicMatcherFromInternalProto(matcher *sbv1.L7QueryMatcher) *policyv1.Matcher {
|
||||
if matcher != nil && len(matcher.GetAny()) > 0 {
|
||||
return &policyv1.Matcher{Kind: &policyv1.Matcher_Any{Any: &policyv1.AnyMatcher{
|
||||
Values: CopyStringSlice(matcher.GetAny()),
|
||||
Values: stableUnique(matcher.GetAny()),
|
||||
}}}
|
||||
}
|
||||
glob := ""
|
||||
@@ -445,6 +445,22 @@ func publicMatcherFromInternalProto(matcher *sbv1.L7QueryMatcher) *policyv1.Matc
|
||||
return &policyv1.Matcher{Kind: &policyv1.Matcher_Glob{Glob: glob}}
|
||||
}
|
||||
|
||||
// stableUnique canonicalizes legacy runtime lists for public fields whose
|
||||
// protobuf contract requires unique values. Retaining the first occurrence
|
||||
// keeps the projection deterministic without changing caller-visible order.
|
||||
func stableUnique[T comparable](values []T) []T {
|
||||
result := make([]T, 0, len(values))
|
||||
seen := make(map[T]struct{}, len(values))
|
||||
for _, value := range values {
|
||||
if _, found := seen[value]; found {
|
||||
continue
|
||||
}
|
||||
seen[value] = struct{}{}
|
||||
result = append(result, value)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func publicTLSMode(mode sbv1.NetworkTlsMode) string {
|
||||
// Numeric cases keep the explicit projection compatible with deprecated
|
||||
// internal enum values without making new SDK code depend on their names.
|
||||
|
||||
@@ -393,6 +393,39 @@ func TestPolicyDocumentFromInternalProtoExplicitProjection(t *testing.T) {
|
||||
assert.Equal(t, []string{"2025-11-25"}, policy.NetworkPolicies["api"].Endpoints[0].Mcp.Versions)
|
||||
}
|
||||
|
||||
func TestPolicyDocumentFromInternalProtoDeduplicatesLegacyValues(t *testing.T) {
|
||||
internal := &sbv1.SandboxPolicy{
|
||||
Version: 1,
|
||||
NetworkPolicies: map[string]*sbv1.NetworkPolicyRule{
|
||||
"api": {
|
||||
Endpoints: []*sbv1.NetworkEndpoint{{
|
||||
Host: "api.example.com",
|
||||
Ports: []uint32{443, 8443, 443},
|
||||
Protocol: "http",
|
||||
Rules: []*sbv1.L7Rule{{Allow: &sbv1.L7Allow{
|
||||
Method: "GET",
|
||||
Query: map[string]*sbv1.L7QueryMatcher{
|
||||
"state": {Any: []string{"open", "closed", "open"}},
|
||||
},
|
||||
}}},
|
||||
}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
policy := PolicyDocumentFromInternalProto(internal)
|
||||
require.NotNil(t, policy)
|
||||
endpoint := policy.NetworkPolicies["api"].Endpoints[0]
|
||||
assert.Equal(t, []uint32{443, 8443}, endpoint.Ports)
|
||||
require.NotNil(t, endpoint.Rules[0].Allow)
|
||||
assert.Equal(t, []string{"open", "closed"}, endpoint.Rules[0].Allow.Query["state"].Any)
|
||||
|
||||
// Policy updates use the checked conversion path, so a projected policy
|
||||
// must satisfy the public protobuf uniqueness constraints before submission.
|
||||
_, err := PolicyDocumentToProtoChecked(policy)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestSandboxPolicyRoundTrip(t *testing.T) {
|
||||
original := &v1.PolicyDocument{
|
||||
Version: 5,
|
||||
|
||||
@@ -1317,6 +1317,56 @@ describe('config / policy', () => {
|
||||
expect(Object.hasOwn(Object, 'children')).toBe(false);
|
||||
});
|
||||
|
||||
it('getConfig canonicalizes legacy lists before a checked policy update', async () => {
|
||||
let submittedPolicy: unknown;
|
||||
const sandbox = client({
|
||||
getSandbox: () => readySandbox('sb', 'sb-id'),
|
||||
getSandboxConfig: () => ({
|
||||
policy: {
|
||||
version: 1,
|
||||
networkPolicies: {
|
||||
api: {
|
||||
endpoints: [
|
||||
{
|
||||
host: 'api.example.com',
|
||||
ports: [443, 8443, 443],
|
||||
protocol: 'http',
|
||||
rules: [
|
||||
{
|
||||
allow: {
|
||||
method: 'GET',
|
||||
query: { state: { any: ['open', 'closed', 'open'] } },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
settings: {},
|
||||
}),
|
||||
updateConfig: (req) => {
|
||||
submittedPolicy = req.policy;
|
||||
return { version: 2, policyHash: 'canonical', settingsRevision: 0n, deleted: false };
|
||||
},
|
||||
});
|
||||
|
||||
const config = await sandbox.getConfig('sb');
|
||||
const projectedPolicy = config.policy;
|
||||
expect(projectedPolicy).toBeDefined();
|
||||
if (!projectedPolicy) throw new Error('expected projected policy');
|
||||
const endpoint = projectedPolicy.networkPolicies.api?.endpoints[0];
|
||||
expect(endpoint?.ports).toEqual([443, 8443]);
|
||||
expect(endpoint?.rules[0]?.allow?.query.state?.kind).toEqual({
|
||||
case: 'any',
|
||||
value: expect.objectContaining({ values: ['open', 'closed'] }),
|
||||
});
|
||||
|
||||
await expect(sandbox.setPolicy('sb', projectedPolicy)).resolves.toMatchObject({ policyHash: 'canonical' });
|
||||
expect(submittedPolicy).toBeDefined();
|
||||
});
|
||||
|
||||
it('setPolicy sends global=false + version pin and (wait) polls until the hash matches', async () => {
|
||||
let updateReq: {
|
||||
sandbox?: string;
|
||||
|
||||
@@ -606,7 +606,7 @@ function policyEndpointFromRuntime(
|
||||
const mcpProtocol = endpoint.protocol.toLowerCase() === 'mcp';
|
||||
return {
|
||||
host: endpoint.host,
|
||||
ports: endpoint.ports.length > 0 ? [...endpoint.ports] : endpoint.port === 0 ? [] : [endpoint.port],
|
||||
ports: endpoint.ports.length > 0 ? stableUnique(endpoint.ports) : endpoint.port === 0 ? [] : [endpoint.port],
|
||||
protocol: endpoint.protocol,
|
||||
tls: runtimeTLSMode(endpoint.tls),
|
||||
enforcement: runtimeEnforcementMode(endpoint.enforcement),
|
||||
@@ -703,11 +703,17 @@ function policyMCPMethod(
|
||||
|
||||
function policyMatcherFromRuntime(matcher: RuntimeMatcher | undefined): MatcherInit {
|
||||
if (matcher && matcher.any.length > 0) {
|
||||
return { kind: { case: 'any', value: { values: [...matcher.any] } } };
|
||||
return { kind: { case: 'any', value: { values: stableUnique(matcher.any) } } };
|
||||
}
|
||||
return { kind: { case: 'glob', value: matcher?.glob ?? '' } };
|
||||
}
|
||||
|
||||
// The public protobuf contract requires these projected lists to be unique.
|
||||
// Set iteration retains the first-seen order from legacy runtime records.
|
||||
function stableUnique<T>(values: readonly T[]): T[] {
|
||||
return [...new Set(values)];
|
||||
}
|
||||
|
||||
function policyMatcherMapFromRuntime(matchers: Record<string, RuntimeMatcher>): Record<string, MatcherInit> {
|
||||
return Object.fromEntries(
|
||||
Object.entries(matchers).map(([name, matcher]) => [name, policyMatcherFromRuntime(matcher)]),
|
||||
|
||||
Reference in New Issue
Block a user