diff --git a/go.mod b/go.mod index faa91c4..20016bd 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,7 @@ module github.com/sdcio/schema-server go 1.25.0 -replace github.com/openconfig/goyang v1.6.0 => github.com/sdcio/goyang v1.6.2-2 +replace github.com/openconfig/goyang v1.6.0 => github.com/sdcio/goyang v1.6.2-2.0.20260608121857-4668a077cf72 require ( github.com/dgraph-io/badger/v4 v4.9.1 @@ -14,7 +14,7 @@ require ( github.com/olekukonko/tablewriter v1.1.4 github.com/openconfig/goyang v1.6.0 github.com/prometheus/client_golang v1.23.2 - github.com/sdcio/sdc-protos v0.0.54 + github.com/sdcio/sdc-protos v0.0.55-0.20260610090020-aeb8edf494c4 github.com/sirupsen/logrus v1.9.4 github.com/spf13/pflag v1.0.10 google.golang.org/grpc v1.81.1 diff --git a/go.sum b/go.sum index 58c9802..53f4fd9 100644 --- a/go.sum +++ b/go.sum @@ -161,12 +161,12 @@ github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlT github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sdcio/goyang v1.6.2-2 h1:qfeUKBmoKpiKAruuEc3+V8wgHKP/n1jRDEnTy23knV8= -github.com/sdcio/goyang v1.6.2-2/go.mod h1:5WolITjek1NF8yrNERyVZ7jqjOClJTpO8p/+OwmETM4= +github.com/sdcio/goyang v1.6.2-2.0.20260608121857-4668a077cf72 h1:CRTnwetSF4zG2q3Weoj0gSlZAx0/VXggHGXDRD4X7jI= +github.com/sdcio/goyang v1.6.2-2.0.20260608121857-4668a077cf72/go.mod h1:5WolITjek1NF8yrNERyVZ7jqjOClJTpO8p/+OwmETM4= github.com/sdcio/logger v0.0.3 h1:IFUbObObGry+S8lHGwOQKKRxJSuOphgRU/hxVhOdMOM= github.com/sdcio/logger v0.0.3/go.mod h1:yWaOxK/G6vszjg8tKZiMqiEjlZouHsjFME4zSk+SAEA= -github.com/sdcio/sdc-protos v0.0.54 h1:1EbtU9ZbbJHFPOFGi5aW8Th79cuY9i+AJaP0ABVx8hw= -github.com/sdcio/sdc-protos v0.0.54/go.mod h1:YMLHbey0/aL1qtLW8csSYVPafsgnnn7aY54HkV5dbyQ= +github.com/sdcio/sdc-protos v0.0.55-0.20260610090020-aeb8edf494c4 h1:M1C1wzt2ni0fssvPEaMNPdHNKqTzBtT8VTT3/EzbrCE= +github.com/sdcio/sdc-protos v0.0.55-0.20260610090020-aeb8edf494c4/go.mod h1:NsvzvHnTonLcwQ/WNzxzBCauQmqxpuviaW0wh7Lkrts= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= diff --git a/pkg/schema/leaf.go b/pkg/schema/leaf.go index 6809f7e..734672b 100644 --- a/pkg/schema/leaf.go +++ b/pkg/schema/leaf.go @@ -41,6 +41,7 @@ func (sc *Schema) leafFromYEntry(e *yang.Entry, withDesc bool) (*sdcpb.LeafSchem IsState: isState(e), Reference: make([]string, 0), IfFeature: getIfFeature(e), + Sensitive: isSensitiveEntry(e), } if withDesc { @@ -183,6 +184,18 @@ func getMustStatement(e *yang.Entry) []*sdcpb.MustStatement { return rs } +// isSensitiveEntry reports whether the goyang entry carries the +// sdcio-ext:sensitive extension, either directly on the leaf or propagated +// onto it via a deviate add/replace in a device-profile overlay YANG file. +func isSensitiveEntry(e *yang.Entry) bool { + for _, ext := range e.Exts { + if ext.Keyword == "sdcio-ext:sensitive" { + return true + } + } + return false +} + func getIfFeature(e *yang.Entry) []string { ifFeatures, ok := e.Extra["if-feature"] if !ok { diff --git a/pkg/schema/leaf_list.go b/pkg/schema/leaf_list.go index 70a4284..978f556 100644 --- a/pkg/schema/leaf_list.go +++ b/pkg/schema/leaf_list.go @@ -34,6 +34,7 @@ func (sc *Schema) leafListFromYEntry(e *yang.Entry, withDesc bool) (*sdcpb.LeafL IsUserOrdered: false, IfFeature: getIfFeature(e), Defaults: e.DefaultValues(), + Sensitive: isSensitiveEntry(e), } if withDesc { ll.Description = e.Description diff --git a/pkg/schema/leaf_sensitive_test.go b/pkg/schema/leaf_sensitive_test.go new file mode 100644 index 0000000..f673996 --- /dev/null +++ b/pkg/schema/leaf_sensitive_test.go @@ -0,0 +1,147 @@ +// Copyright 2024 Nokia +// +// 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. + +package schema + +import ( + "testing" + + "github.com/openconfig/goyang/pkg/yang" + "github.com/sdcio/schema-server/pkg/config" + sdcpb "github.com/sdcio/sdc-protos/sdcpb" +) + +// sensitiveSchema loads the YANG fixture under testdata/sensitive and returns +// the resulting Schema for use in sub-tests. +func sensitiveSchema(t *testing.T) *Schema { + t.Helper() + sc, err := NewSchema(&config.SchemaConfig{ + Name: "sensitive-test", + Vendor: "test", + Version: "0.0.1", + Files: []string{ + "testdata/sensitive", + }, + }) + if err != nil { + t.Fatalf("NewSchema: %v", err) + } + return sc +} + +// TestIsSensitiveEntry verifies the low-level helper against hand-crafted +// yang.Entry objects — no YANG parsing required. +func TestIsSensitiveEntry(t *testing.T) { + tests := []struct { + name string + exts []*yang.Statement + want bool + }{ + { + name: "nil exts → not sensitive", + exts: nil, + want: false, + }, + { + name: "empty exts → not sensitive", + exts: []*yang.Statement{}, + want: false, + }, + { + name: "unrelated extension → not sensitive", + exts: []*yang.Statement{{Keyword: "other:tag"}}, + want: false, + }, + { + name: "sdcio-ext:sensitive → sensitive", + exts: []*yang.Statement{{Keyword: "sdcio-ext:sensitive"}}, + want: true, + }, + { + name: "mixed exts including sdcio-ext:sensitive → sensitive", + exts: []*yang.Statement{ + {Keyword: "other:tag"}, + {Keyword: "sdcio-ext:sensitive"}, + }, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + e := &yang.Entry{Exts: tt.exts} + if got := isSensitiveEntry(e); got != tt.want { + t.Errorf("isSensitiveEntry() = %v, want %v", got, tt.want) + } + }) + } +} + +// TestLeafSchema_SensitiveFlag loads the YANG fixture and verifies that +// LeafSchema.Sensitive is set correctly for leaves with and without the +// sdcio-ext:sensitive extension. +func TestLeafSchema_SensitiveFlag(t *testing.T) { + sc := sensitiveSchema(t) + + tests := []struct { + path []string + wantSens bool + wantField string + }{ + {path: []string{"auth-password"}, wantSens: true, wantField: "auth-password (direct annotation)"}, + {path: []string{"description"}, wantSens: false, wantField: "description (no annotation)"}, + {path: []string{"plain-secret"}, wantSens: true, wantField: "plain-secret (deviate add in overlay)"}, + } + + for _, tt := range tests { + t.Run(tt.wantField, func(t *testing.T) { + e, err := sc.GetEntry(tt.path) + if err != nil { + t.Fatalf("GetEntry(%v): %v", tt.path, err) + } + elem, err := sc.SchemaElemFromYEntry(e, false) + if err != nil { + t.Fatalf("SchemaElemFromYEntry: %v", err) + } + leaf, ok := elem.Schema.(*sdcpb.SchemaElem_Field) + if !ok { + t.Fatalf("expected LeafSchema, got %T", elem.Schema) + } + if got := leaf.Field.GetSensitive(); got != tt.wantSens { + t.Errorf("LeafSchema.Sensitive = %v, want %v", got, tt.wantSens) + } + }) + } +} + +// TestLeafListSchema_SensitiveFlag verifies that LeafListSchema.Sensitive is +// set for leaf-lists annotated with sdcio-ext:sensitive. +func TestLeafListSchema_SensitiveFlag(t *testing.T) { + sc := sensitiveSchema(t) + + e, err := sc.GetEntry([]string{"allowed-keys"}) + if err != nil { + t.Fatalf("GetEntry: %v", err) + } + elem, err := sc.SchemaElemFromYEntry(e, false) + if err != nil { + t.Fatalf("SchemaElemFromYEntry: %v", err) + } + ll, ok := elem.Schema.(*sdcpb.SchemaElem_Leaflist) + if !ok { + t.Fatalf("expected LeafListSchema, got %T", elem.Schema) + } + if !ll.Leaflist.GetSensitive() { + t.Error("LeafListSchema.Sensitive = false, want true") + } +} diff --git a/pkg/schema/testdata/sensitive/device.yang b/pkg/schema/testdata/sensitive/device.yang new file mode 100644 index 0000000..42d7d7e --- /dev/null +++ b/pkg/schema/testdata/sensitive/device.yang @@ -0,0 +1,29 @@ +module device { + yang-version 1.1; + namespace "urn:device"; + prefix "device"; + + import sdcio-ext { prefix "sdcio-ext"; } + + // Leaf with sdcio-ext:sensitive annotated directly. + leaf auth-password { + sdcio-ext:sensitive; + type string; + } + + // Leaf-list with sdcio-ext:sensitive annotated directly. + leaf-list allowed-keys { + sdcio-ext:sensitive; + type string; + } + + // Plain leaf — must NOT be marked sensitive. + leaf description { + type string; + } + + // Plain leaf to be marked sensitive via deviate add in overlay.yang. + leaf plain-secret { + type string; + } +} diff --git a/pkg/schema/testdata/sensitive/overlay.yang b/pkg/schema/testdata/sensitive/overlay.yang new file mode 100644 index 0000000..9de2d1e --- /dev/null +++ b/pkg/schema/testdata/sensitive/overlay.yang @@ -0,0 +1,16 @@ +module overlay { + yang-version 1.1; + namespace "urn:overlay"; + prefix "overlay"; + + import device { prefix "device"; } + import sdcio-ext { prefix "sdcio-ext"; } + + // Device-profile overlay: marks plain-secret sensitive via deviate add, + // exercising the ApplyDeviate Exts-propagation fix in sdcio/goyang. + deviation /device:plain-secret { + deviate add { + sdcio-ext:sensitive; + } + } +} diff --git a/pkg/schema/testdata/sensitive/sdcio-ext.yang b/pkg/schema/testdata/sensitive/sdcio-ext.yang new file mode 100644 index 0000000..ab0a7d0 --- /dev/null +++ b/pkg/schema/testdata/sensitive/sdcio-ext.yang @@ -0,0 +1,12 @@ +module sdcio-ext { + yang-version 1.1; + namespace "urn:sdcio-ext"; + prefix "sdcio-ext"; + + extension sensitive { + description + "Marks a leaf or leaf-list as containing sensitive data. + The northbound API redacts its value with '***' unless + include_sensitive is set on the request."; + } +}