diff --git a/pkg/github/context_tools.go b/pkg/github/context_tools.go index 902734481..074df320e 100644 --- a/pkg/github/context_tools.go +++ b/pkg/github/context_tools.go @@ -6,6 +6,7 @@ import ( "time" ghErrors "github.com/github/github-mcp-server/pkg/errors" + "github.com/github/github-mcp-server/pkg/ifc" "github.com/github/github-mcp-server/pkg/inventory" "github.com/github/github-mcp-server/pkg/scopes" "github.com/github/github-mcp-server/pkg/translations" @@ -103,7 +104,13 @@ func GetMe(t translations.TranslationHelperFunc) inventory.ServerTool { }, } - return MarshalledTextResult(minimalUser), nil, nil + result := MarshalledTextResult(minimalUser) + if deps.GetFlags(ctx).InsidersMode { + result.Meta = mcp.Meta{ + "ifc": ifc.LabelGetMe(), + } + } + return result, nil, nil }, ) } diff --git a/pkg/github/context_tools_test.go b/pkg/github/context_tools_test.go index 39f2058be..86677b6a7 100644 --- a/pkg/github/context_tools_test.go +++ b/pkg/github/context_tools_test.go @@ -139,6 +139,66 @@ func Test_GetMe(t *testing.T) { } } +func Test_GetMe_IFC_InsidersMode(t *testing.T) { + t.Parallel() + + serverTool := GetMe(translations.NullTranslationHelper) + + mockUser := &github.User{ + Login: github.Ptr("testuser"), + HTMLURL: github.Ptr("https://github.com/testuser"), + CreatedAt: &github.Timestamp{Time: time.Now()}, + } + mockedHTTPClient := MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ + GetUser: mockResponse(t, http.StatusOK, mockUser), + }) + + t.Run("insiders mode disabled omits ifc label from result meta", func(t *testing.T) { + deps := BaseDeps{ + Client: github.NewClient(mockedHTTPClient), + Flags: FeatureFlags{InsidersMode: false}, + } + handler := serverTool.Handler(deps) + + request := createMCPRequest(map[string]any{}) + result, err := handler(ContextWithDeps(context.Background(), deps), &request) + require.NoError(t, err) + require.False(t, result.IsError) + + assert.Nil(t, result.Meta, "result meta should be nil when insiders mode is disabled") + }) + + t.Run("insiders mode enabled includes ifc label in result meta", func(t *testing.T) { + deps := BaseDeps{ + Client: github.NewClient(mockedHTTPClient), + Flags: FeatureFlags{InsidersMode: true}, + } + handler := serverTool.Handler(deps) + + request := createMCPRequest(map[string]any{}) + result, err := handler(ContextWithDeps(context.Background(), deps), &request) + require.NoError(t, err) + require.False(t, result.IsError) + + require.NotNil(t, result.Meta, "result meta should be set when insiders mode is enabled") + ifcLabel, ok := result.Meta["ifc"] + require.True(t, ok, "result meta should contain ifc key") + + ifcJSON, err := json.Marshal(ifcLabel) + require.NoError(t, err) + + var ifcMap map[string]any + err = json.Unmarshal(ifcJSON, &ifcMap) + require.NoError(t, err) + + assert.Equal(t, "high", ifcMap["integrity"]) + confList, ok := ifcMap["confidentiality"].([]any) + require.True(t, ok, "confidentiality should be a list") + require.Len(t, confList, 1) + assert.Equal(t, "public", confList[0]) + }) +} + func Test_GetTeams(t *testing.T) { t.Parallel() diff --git a/pkg/ifc/labelling_engine_readers.go b/pkg/ifc/labelling_engine_readers.go new file mode 100644 index 000000000..1226309c0 --- /dev/null +++ b/pkg/ifc/labelling_engine_readers.go @@ -0,0 +1,6 @@ +package ifc + +// LabelGetMe returns a label for get_me: trusted, universal readers. +func LabelGetMe() ReadersSecurityLabel { + return PublicTrusted() +} diff --git a/pkg/ifc/lattice.go b/pkg/ifc/lattice.go new file mode 100644 index 000000000..8c48940b8 --- /dev/null +++ b/pkg/ifc/lattice.go @@ -0,0 +1,221 @@ +// Package ifc implements Information Flow Control (IFC) lattices and security labels. +// +// This package provides the fundamental lattice structures used for IFC: +// - Confidentiality lattice (LOW, HIGH) +// - Integrity lattice (TRUSTED, UNTRUSTED) +package ifc + +import "fmt" + +type Lattice[T any] interface { + Leq(other T) bool // self <= other + Join(other T) T // least upper bound + Meet(other T) T // greatest lower bound + fmt.Stringer // String() string +} + +type ConfidentialityLevel int + +const ( + ConfidentialityLow ConfidentialityLevel = iota + ConfidentialityHigh +) + +func (l ConfidentialityLevel) String() string { + switch l { + case ConfidentialityLow: + return "LOW" + case ConfidentialityHigh: + return "HIGH" + default: + return fmt.Sprintf("ConfidentialityLevel(%d)", int(l)) + } +} + +type ConfidentialityLabel struct { + Level ConfidentialityLevel +} + +func LowConfidentiality() ConfidentialityLabel { + return ConfidentialityLabel{Level: ConfidentialityLow} +} + +func HighConfidentiality() ConfidentialityLabel { + return ConfidentialityLabel{Level: ConfidentialityHigh} +} + +type SecurityLabel struct { + ProductLabel[ConfidentialityLabel, IntegrityLabel] +} + +func NewSecurityLabel(c ConfidentialityLabel, i IntegrityLabel) SecurityLabel { + return SecurityLabel{ + ProductLabel: ProductLabel[ConfidentialityLabel, IntegrityLabel]{ + Left: c, + Right: i, + }, + } +} + +func (s SecurityLabel) Leq(other SecurityLabel) bool { + return s.ProductLabel.Leq(other.ProductLabel) +} + +func (s SecurityLabel) Join(other SecurityLabel) SecurityLabel { + return SecurityLabel{ + ProductLabel: s.ProductLabel.Join(other.ProductLabel), + } +} + +func (s SecurityLabel) Meet(other SecurityLabel) SecurityLabel { + return SecurityLabel{ + ProductLabel: s.ProductLabel.Meet(other.ProductLabel), + } +} + +func (s SecurityLabel) String() string { + return s.ProductLabel.String() +} + +var _ Lattice[SecurityLabel] = SecurityLabel{} + +var LabelHighConfidentialityTrusted = NewSecurityLabel(HighConfidentiality(), Trusted()) +var LabelPublicTrusted = NewSecurityLabel(LowConfidentiality(), Trusted()) +var LabelUserUntrusted = NewSecurityLabel(HighConfidentiality(), Untrusted()) +var LabelPublicUntrusted = NewSecurityLabel(LowConfidentiality(), Untrusted()) + +func (c ConfidentialityLabel) Leq(other ConfidentialityLabel) bool { + return int(c.Level) <= int(other.Level) +} + +func (c ConfidentialityLabel) Join(other ConfidentialityLabel) ConfidentialityLabel { + if c.Leq(other) { + return other + } + return c +} + +func (c ConfidentialityLabel) Meet(other ConfidentialityLabel) ConfidentialityLabel { + if c.Leq(other) { + return c + } + return other +} + +func (c ConfidentialityLabel) String() string { + return c.Level.String() +} + +var _ Lattice[ConfidentialityLabel] = ConfidentialityLabel{} + +type IntegrityLevel int + +const ( + IntegrityTrusted IntegrityLevel = iota + IntegrityUntrusted +) + +func (l IntegrityLevel) String() string { + switch l { + case IntegrityTrusted: + return "TRUSTED" + case IntegrityUntrusted: + return "UNTRUSTED" + default: + return fmt.Sprintf("IntegrityLevel(%d)", int(l)) + } +} + +type IntegrityLabel struct { + Level IntegrityLevel +} + +// Trusted: content originating from the user, from trusted collaborators, or system prompts. +func Trusted() IntegrityLabel { + return IntegrityLabel{Level: IntegrityTrusted} +} + +// Untrusted: content from untrusted users (e.g., no push access), or from external/public sources. +func Untrusted() IntegrityLabel { + return IntegrityLabel{Level: IntegrityUntrusted} +} + +func (i IntegrityLabel) Leq(other IntegrityLabel) bool { + return int(i.Level) <= int(other.Level) +} + +func (i IntegrityLabel) Join(other IntegrityLabel) IntegrityLabel { + if i.Leq(other) { + return other + } + return i +} + +func (i IntegrityLabel) Meet(other IntegrityLabel) IntegrityLabel { + if i.Leq(other) { + return i + } + return other +} + +func (i IntegrityLabel) String() string { + return i.Level.String() +} + +var _ Lattice[IntegrityLabel] = IntegrityLabel{} + +// ProductLabel is a product lattice of two lattices L1 × L2. +type ProductLabel[L1 Lattice[L1], L2 Lattice[L2]] struct { + Left L1 + Right L2 +} + +func (p ProductLabel[L1, L2]) Leq(other ProductLabel[L1, L2]) bool { + return p.Left.Leq(other.Left) && p.Right.Leq(other.Right) +} + +func (p ProductLabel[L1, L2]) Join(other ProductLabel[L1, L2]) ProductLabel[L1, L2] { + return ProductLabel[L1, L2]{ + Left: p.Left.Join(other.Left), + Right: p.Right.Join(other.Right), + } +} + +func (p ProductLabel[L1, L2]) Meet(other ProductLabel[L1, L2]) ProductLabel[L1, L2] { + return ProductLabel[L1, L2]{ + Left: p.Left.Meet(other.Left), + Right: p.Right.Meet(other.Right), + } +} + +func (p ProductLabel[L1, L2]) String() string { + return fmt.Sprintf("(%s, %s)", p.Left.String(), p.Right.String()) +} + +var ProductLabelLattice Lattice[ProductLabel[ConfidentialityLabel, IntegrityLabel]] = ProductLabel[ConfidentialityLabel, IntegrityLabel]{} + +// InverseLattice inverts the order of an underlying lattice. +type InverseLattice[L Lattice[L]] struct { + Inner L +} + +func (i InverseLattice[L]) Leq(other InverseLattice[L]) bool { + // Invert order: i <= other ⇔ other.Inner <= i.Inner + return other.Inner.Leq(i.Inner) +} + +func (i InverseLattice[L]) Join(other InverseLattice[L]) InverseLattice[L] { + // join in inverse is meet in the original + return InverseLattice[L]{Inner: i.Inner.Meet(other.Inner)} +} + +func (i InverseLattice[L]) Meet(other InverseLattice[L]) InverseLattice[L] { + // meet in inverse is join in the original + return InverseLattice[L]{Inner: i.Inner.Join(other.Inner)} +} + +func (i InverseLattice[L]) String() string { + return fmt.Sprintf("Inverse(%s)", i.Inner.String()) +} + +var _ Lattice[InverseLattice[ConfidentialityLabel]] = InverseLattice[ConfidentialityLabel]{} diff --git a/pkg/ifc/readers_lattice.go b/pkg/ifc/readers_lattice.go new file mode 100644 index 000000000..a2a9c3b62 --- /dev/null +++ b/pkg/ifc/readers_lattice.go @@ -0,0 +1,536 @@ +// readers_lattice.go extends the basic lattice structures with reader-based confidentiality. +// +// It provides: +// - Powerset lattice with support for both finite sets and universal sets +// - ReadersSecurityLabel combining integrity with reader-based confidentiality +// +// The confidentiality dimension uses an inverse powerset lattice over reader sets, +// where fewer readers means higher confidentiality (more restrictive). +// UniversalReaderSet represents "public" data readable by everyone. +// +// Example usage: +// +// // Create a label readable by everyone (public) +// universe := UniversalReaders[string]() +// label, _ := NewPowersetLattice(universe, universe) +// +// // Create a label for specific readers only +// finiteUniverse := NewFiniteReaderSetFromSlice([]string{"alice", "bob", "charlie"}) +// privateReaders := NewFiniteReaderSetFromSlice([]string{"alice"}) +// privateLabel, _ := NewPowersetLattice(privateReaders, finiteUniverse) + +package ifc + +import ( + "encoding/json" + "fmt" + "sort" + "strings" +) + +// ReaderSet represents either a finite set of readers or the universal set of all readers. +// This allows representing "all possible readers" without enumerating them. +type ReaderSet[T comparable] interface { + IsUniversal() bool + IsSubset(other ReaderSet[T]) bool + Union(other ReaderSet[T]) ReaderSet[T] + Intersection(other ReaderSet[T]) ReaderSet[T] + fmt.Stringer +} + +// UniversalReaderSet represents the universe of all possible readers. +// Any finite set is considered a subset of the universal set. +type UniversalReaderSet[T comparable] struct{} + +func NewUniversalReaderSet[T comparable]() *UniversalReaderSet[T] { + return &UniversalReaderSet[T]{} +} + +func (u *UniversalReaderSet[T]) IsUniversal() bool { + return true +} + +func (u *UniversalReaderSet[T]) IsSubset(other ReaderSet[T]) bool { + // Universal set is only a subset of itself + return other.IsUniversal() +} + +func (u *UniversalReaderSet[T]) Union(_ ReaderSet[T]) ReaderSet[T] { + // Union with universal set is always the universal set + return u +} + +func (u *UniversalReaderSet[T]) Intersection(other ReaderSet[T]) ReaderSet[T] { + // Intersection with universal set returns the other set unchanged + return other +} + +func (u *UniversalReaderSet[T]) String() string { + return "UniversalReaderSet()" +} + +// FiniteReaderSet represents a finite set of readers. +type FiniteReaderSet[T comparable] struct { + members map[T]struct{} +} + +func NewFiniteReaderSet[T comparable](members map[T]struct{}) *FiniteReaderSet[T] { + return &FiniteReaderSet[T]{ + members: copySet(members), + } +} + +func (f *FiniteReaderSet[T]) IsUniversal() bool { + return false +} + +func (f *FiniteReaderSet[T]) IsSubset(other ReaderSet[T]) bool { + if other.IsUniversal() { + return true + } + otherFinite, ok := other.(*FiniteReaderSet[T]) + if !ok { + panic(fmt.Sprintf("unsupported ReaderSet implementation: %T", other)) + } + for member := range f.members { + if _, exists := otherFinite.members[member]; !exists { + return false + } + } + return true +} + +func (f *FiniteReaderSet[T]) Union(other ReaderSet[T]) ReaderSet[T] { + if other.IsUniversal() { + return other + } + otherFinite, ok := other.(*FiniteReaderSet[T]) + if !ok { + panic(fmt.Sprintf("unsupported ReaderSet implementation: %T", other)) + } + union := make(map[T]struct{}, len(f.members)+len(otherFinite.members)) + for member := range f.members { + union[member] = struct{}{} + } + for member := range otherFinite.members { + union[member] = struct{}{} + } + return NewFiniteReaderSet(union) +} + +func (f *FiniteReaderSet[T]) Intersection(other ReaderSet[T]) ReaderSet[T] { + if other.IsUniversal() { + return f + } + otherFinite, ok := other.(*FiniteReaderSet[T]) + if !ok { + panic(fmt.Sprintf("unsupported ReaderSet implementation: %T", other)) + } + intersection := make(map[T]struct{}) + for member := range f.members { + if _, exists := otherFinite.members[member]; exists { + intersection[member] = struct{}{} + } + } + return NewFiniteReaderSet(intersection) +} + +func (f *FiniteReaderSet[T]) String() string { + if len(f.members) == 0 { + return "FiniteReaderSet({})" + } + strs := make([]string, 0, len(f.members)) + for member := range f.members { + strs = append(strs, fmt.Sprintf("%v", member)) + } + sort.Strings(strs) + var b strings.Builder + b.WriteString("FiniteReaderSet({") + for i, s := range strs { + if i > 0 { + b.WriteString(", ") + } + b.WriteString(s) + } + b.WriteString("})") + return b.String() +} + +// PowersetLattice is a powerset lattice that can represent either a finite set or the universal set. +// subset and universe are represented using the ReaderSet interface. +// T must be comparable to be used as a map key. +type PowersetLattice[T comparable] struct { + subset ReaderSet[T] + universe ReaderSet[T] +} + +// NewPowersetLattice constructs a PowersetLattice, checking that +// subset ⊆ universe. +func NewPowersetLattice[T comparable](subset, universe ReaderSet[T]) (*PowersetLattice[T], error) { + if !subset.IsSubset(universe) { + return nil, fmt.Errorf("subset must be within the universe") + } + return &PowersetLattice[T]{ + subset: subset, + universe: universe, + }, nil +} + +// NewPowersetLatticeUnchecked constructs a PowersetLattice without validation. +// Use this when you know the subset is valid. +func NewPowersetLatticeUnchecked[T comparable](subset, universe ReaderSet[T]) *PowersetLattice[T] { + return &PowersetLattice[T]{ + subset: subset, + universe: universe, + } +} + +// helper to copy sets so callers don't mutate internals. +func copySet[T comparable](in map[T]struct{}) map[T]struct{} { + out := make(map[T]struct{}, len(in)) + for k := range in { + out[k] = struct{}{} + } + return out +} + +func (p *PowersetLattice[T]) Leq(other *PowersetLattice[T]) bool { + p.mustMatchUniverse(other) + return p.subset.IsSubset(other.subset) +} + +func (p *PowersetLattice[T]) Join(other *PowersetLattice[T]) *PowersetLattice[T] { + p.mustMatchUniverse(other) + return &PowersetLattice[T]{ + subset: p.subset.Union(other.subset), + universe: p.universe, + } +} + +func (p *PowersetLattice[T]) Meet(other *PowersetLattice[T]) *PowersetLattice[T] { + p.mustMatchUniverse(other) + return &PowersetLattice[T]{ + subset: p.subset.Intersection(other.subset), + universe: p.universe, + } +} + +func (p *PowersetLattice[T]) mustMatchUniverse(other *PowersetLattice[T]) { + pUniv := p.universe.IsUniversal() + oUniv := other.universe.IsUniversal() + if pUniv != oUniv { + panic(fmt.Sprintf("universe mismatch: %s vs %s", p.universe, other.universe)) + } + if pUniv { + return + } + pFinite, pOK := p.universe.(*FiniteReaderSet[T]) + oFinite, oOK := other.universe.(*FiniteReaderSet[T]) + if !pOK || !oOK { + panic(fmt.Sprintf("universe mismatch: %T vs %T", p.universe, other.universe)) + } + if !pFinite.IsSubset(oFinite) || !oFinite.IsSubset(pFinite) { + panic(fmt.Sprintf("universe mismatch: %s vs %s", p.universe, other.universe)) + } +} + +func (p *PowersetLattice[T]) String() string { + return fmt.Sprintf("Powerset(%s)", p.subset.String()) +} + +// Bottom returns the bottom element (empty subset). +func BottomPowerset[T comparable](universe ReaderSet[T]) *PowersetLattice[T] { + return &PowersetLattice[T]{ + subset: NewFiniteReaderSet[T](make(map[T]struct{})), + universe: universe, + } +} + +// Top returns the top element (the full universe). +func TopPowerset[T comparable](universe ReaderSet[T]) *PowersetLattice[T] { + return &PowersetLattice[T]{ + subset: universe, + universe: universe, + } +} + +// Satisfy Lattice[*PowersetLattice[T]]. +var _ Lattice[*PowersetLattice[int]] = (*PowersetLattice[int])(nil) + +// NewFiniteReaderSetFromSlice creates a FiniteReaderSet from a slice of elements. +func NewFiniteReaderSetFromSlice[T comparable](elements []T) *FiniteReaderSet[T] { + members := make(map[T]struct{}, len(elements)) + for _, elem := range elements { + members[elem] = struct{}{} + } + return NewFiniteReaderSet(members) +} + +// EmptyReaderSet creates an empty finite reader set. +func EmptyReaderSet[T comparable]() *FiniteReaderSet[T] { + return NewFiniteReaderSet[T](make(map[T]struct{})) +} + +// UniversalReaders creates a universal reader set (all possible readers). +func UniversalReaders[T comparable]() *UniversalReaderSet[T] { + return NewUniversalReaderSet[T]() +} + +// ReadersSecurityLabel represents an Information Flow Control label with: +// - IntegrityLabel: TRUSTED ⊑ UNTRUSTED +// - InverseLattice[PowersetLattice[string]]: For confidentiality using readers +// +// This matches the Python implementation with proper lattice operations where: +// - public ⊔ Alice = Alice (more restrictive wins) +// - trusted ⊔ untrusted = untrusted (lower integrity wins) +type ReadersSecurityLabel struct { + Integrity IntegrityLabel + Confidentiality InverseLattice[*PowersetLattice[string]] +} + +// Leq returns true if self <= other in the lattice. +func (l ReadersSecurityLabel) Leq(other ReadersSecurityLabel) bool { + return l.Integrity.Leq(other.Integrity) && + l.Confidentiality.Leq(other.Confidentiality) +} + +// Join returns the least upper bound of self and other. +// For integrity: TRUSTED ⊔ UNTRUSTED = UNTRUSTED +// For confidentiality: public ⊔ Alice = Alice (intersection of readers = more restrictive) +func (l ReadersSecurityLabel) Join(other ReadersSecurityLabel) ReadersSecurityLabel { + return ReadersSecurityLabel{ + Integrity: l.Integrity.Join(other.Integrity), + Confidentiality: l.Confidentiality.Join(other.Confidentiality), + } +} + +// Meet returns the greatest lower bound of self and other. +func (l ReadersSecurityLabel) Meet(other ReadersSecurityLabel) ReadersSecurityLabel { + return ReadersSecurityLabel{ + Integrity: l.Integrity.Meet(other.Integrity), + Confidentiality: l.Confidentiality.Meet(other.Confidentiality), + } +} + +// IsLowIntegrity returns true if this label has untrusted integrity. +func (l ReadersSecurityLabel) IsLowIntegrity() bool { + return l.Integrity.Level == IntegrityUntrusted +} + +// IsHighIntegrity returns true if this label has trusted integrity. +func (l ReadersSecurityLabel) IsHighIntegrity() bool { + return l.Integrity.Level == IntegrityTrusted +} + +// IsPublicConfidentiality returns true if the confidentiality is public (universal readers). +func (l ReadersSecurityLabel) IsPublicConfidentiality() bool { + return l.Confidentiality.Inner.subset.IsUniversal() +} + +// GetReaders returns the set of readers for this label. +// Returns nil if the readers are universal (public). +func (l ReadersSecurityLabel) GetReaders() []string { + if l.IsPublicConfidentiality() { + return nil + } + + finiteSet, ok := l.Confidentiality.Inner.subset.(*FiniteReaderSet[string]) + if !ok { + return nil + } + + readers := make([]string, 0, len(finiteSet.members)) + for reader := range finiteSet.members { + readers = append(readers, reader) + } + sort.Strings(readers) + return readers +} + +// ReaderSetFromList creates a ReaderSet from a list of readers. +func ReaderSetFromList(readers []string) ReaderSet[string] { + if len(readers) == 0 { + return NewFiniteReaderSet[string](make(map[string]struct{})) + } + if len(readers) == 1 && readers[0] == "public" { + return NewUniversalReaderSet[string]() + } + members := make(map[string]struct{}, len(readers)) + for _, r := range readers { + members[r] = struct{}{} + } + return NewFiniteReaderSet(members) +} + +func ConfidentialityLabelFromReaderSet(readers ReaderSet[string]) InverseLattice[*PowersetLattice[string]] { + universe := UniversalReaders[string]() + return InverseLattice[*PowersetLattice[string]]{ + Inner: NewPowersetLatticeUnchecked(readers, universe), + } +} + +// String returns a human-readable representation of the label. +func (l ReadersSecurityLabel) String() string { + integrityStr := "trusted" + if l.IsLowIntegrity() { + integrityStr = "untrusted" + } + + confStr := "public" + if !l.IsPublicConfidentiality() { + readers := l.GetReaders() + confStr = fmt.Sprintf("{%v}", readers) + } + + return fmt.Sprintf("ReadersSecurityLabel(%s, %s)", integrityStr, confStr) +} + +// ToDict converts the label to a dictionary format for backward compatibility and serialization. +func (l ReadersSecurityLabel) ToDict() map[string]any { + integrityStr := "high" + if l.IsLowIntegrity() { + integrityStr = "low" + } + + confidentiality := []string{"public"} + if !l.IsPublicConfidentiality() { + confidentiality = l.GetReaders() + if confidentiality == nil { + confidentiality = []string{} + } + } + + return map[string]any{ + "integrity": integrityStr, + "confidentiality": confidentiality, + } +} + +// MarshalJSON implements json.Marshaler. +func (l ReadersSecurityLabel) MarshalJSON() ([]byte, error) { + return json.Marshal(l.ToDict()) +} + +// UnmarshalJSON implements json.Unmarshaler. +func (l *ReadersSecurityLabel) UnmarshalJSON(data []byte) error { + var dict map[string]any + if err := json.Unmarshal(data, &dict); err != nil { + return err + } + + *l = ReadersSecurityLabelFromDict(dict) + return nil +} + +// ReadersSecurityLabelFromDict creates a ReadersSecurityLabel from a dictionary format. +func ReadersSecurityLabelFromDict(dict map[string]any) ReadersSecurityLabel { + // Parse integrity + integrityStr := "high" + if i, ok := dict["integrity"].(string); ok { + integrityStr = i + } + + var integrity IntegrityLabel + if integrityStr == "low" { + integrity = Untrusted() + } else { + integrity = Trusted() + } + + // Parse confidentiality + confList := []string{"public"} + if c, ok := dict["confidentiality"].([]any); ok { + confList = make([]string, len(c)) + for i, v := range c { + if s, ok := v.(string); ok { + confList[i] = s + } + } + } else if c, ok := dict["confidentiality"].([]string); ok { + confList = c + } + + // Check if it's public + isPublic := len(confList) == 1 && confList[0] == "public" + + var confidentiality InverseLattice[*PowersetLattice[string]] + universe := UniversalReaders[string]() + + if isPublic { + // Public means universal readers + confidentiality = InverseLattice[*PowersetLattice[string]]{ + Inner: TopPowerset(universe), + } + } else { + // Specific readers + readers := NewFiniteReaderSetFromSlice(confList) + confidentiality = InverseLattice[*PowersetLattice[string]]{ + Inner: NewPowersetLatticeUnchecked(readers, universe), + } + } + + return ReadersSecurityLabel{ + Integrity: integrity, + Confidentiality: confidentiality, + } +} + +// PublicTrusted creates a public trusted label (most permissive). +func PublicTrusted() ReadersSecurityLabel { + universe := UniversalReaders[string]() + return ReadersSecurityLabel{ + Integrity: Trusted(), + Confidentiality: InverseLattice[*PowersetLattice[string]]{ + Inner: TopPowerset(universe), + }, + } +} + +// PublicUntrusted creates a public untrusted label. +func PublicUntrusted() ReadersSecurityLabel { + universe := UniversalReaders[string]() + return ReadersSecurityLabel{ + Integrity: Untrusted(), + Confidentiality: InverseLattice[*PowersetLattice[string]]{ + Inner: TopPowerset(universe), + }, + } +} + +// PrivateTrusted creates a private trusted label for specific readers. +func PrivateTrusted(readers []string) ReadersSecurityLabel { + universe := UniversalReaders[string]() + readerSet := NewFiniteReaderSetFromSlice(readers) + return ReadersSecurityLabel{ + Integrity: Trusted(), + Confidentiality: InverseLattice[*PowersetLattice[string]]{ + Inner: NewPowersetLatticeUnchecked(readerSet, universe), + }, + } +} + +// PrivateUntrusted creates a private untrusted label for specific readers. +func PrivateUntrusted(readers []string) ReadersSecurityLabel { + universe := UniversalReaders[string]() + readerSet := NewFiniteReaderSetFromSlice(readers) + return ReadersSecurityLabel{ + Integrity: Untrusted(), + Confidentiality: InverseLattice[*PowersetLattice[string]]{ + Inner: NewPowersetLatticeUnchecked(readerSet, universe), + }, + } +} + +// ReadersLabel builds a confidentiality label from a readers set. +func ReadersLabel(readers ReaderSet[string]) InverseLattice[*PowersetLattice[string]] { + universe := UniversalReaders[string]() + return InverseLattice[*PowersetLattice[string]]{ + Inner: NewPowersetLatticeUnchecked(readers, universe), + } +} + +// Predefined labels for common cases +var ( + ReadersSecurityLabelPublicTrusted = PublicTrusted() + ReadersSecurityLabelPublicUntrusted = PublicUntrusted() +) diff --git a/pkg/ifc/readers_lattice_test.go b/pkg/ifc/readers_lattice_test.go new file mode 100644 index 000000000..da322da0d --- /dev/null +++ b/pkg/ifc/readers_lattice_test.go @@ -0,0 +1,610 @@ +package ifc + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestUniversalReaderSet(t *testing.T) { + u := NewUniversalReaderSet[string]() + + t.Run("IsUniversal", func(t *testing.T) { + assert.True(t, u.IsUniversal()) + }) + + t.Run("IsSubset", func(t *testing.T) { + u2 := NewUniversalReaderSet[string]() + assert.True(t, u.IsSubset(u2)) + + finite := NewFiniteReaderSetFromSlice([]string{"alice", "bob"}) + assert.False(t, u.IsSubset(finite)) + }) + + t.Run("Union", func(t *testing.T) { + finite := NewFiniteReaderSetFromSlice([]string{"alice"}) + result := u.Union(finite) + assert.True(t, result.IsUniversal()) + }) + + t.Run("Intersection", func(t *testing.T) { + finite := NewFiniteReaderSetFromSlice([]string{"alice"}) + result := u.Intersection(finite) + assert.False(t, result.IsUniversal()) + assert.Equal(t, finite.String(), result.String()) + }) + + t.Run("String", func(t *testing.T) { + assert.Equal(t, "UniversalReaderSet()", u.String()) + }) +} + +func TestFiniteReaderSet(t *testing.T) { + t.Run("IsUniversal", func(t *testing.T) { + f := NewFiniteReaderSetFromSlice([]string{"alice", "bob"}) + assert.False(t, f.IsUniversal()) + }) + + t.Run("IsSubset", func(t *testing.T) { + tests := []struct { + name string + set []string + other []string + expected bool + }{ + {"empty subset of any", []string{}, []string{"alice"}, true}, + {"set subset of itself", []string{"alice"}, []string{"alice"}, true}, + {"proper subset", []string{"alice"}, []string{"alice", "bob"}, true}, + {"not subset", []string{"alice", "bob"}, []string{"alice"}, false}, + {"disjoint not subset", []string{"alice"}, []string{"bob"}, false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f := NewFiniteReaderSetFromSlice(tt.set) + other := NewFiniteReaderSetFromSlice(tt.other) + assert.Equal(t, tt.expected, f.IsSubset(other)) + }) + } + + t.Run("finite subset of universal", func(t *testing.T) { + f := NewFiniteReaderSetFromSlice([]string{"alice"}) + u := NewUniversalReaderSet[string]() + assert.True(t, f.IsSubset(u)) + }) + }) + + t.Run("Union", func(t *testing.T) { + tests := []struct { + name string + set []string + other []string + expected []string + }{ + {"empty with empty", []string{}, []string{}, []string{}}, + {"empty with non-empty", []string{}, []string{"alice"}, []string{"alice"}}, + {"disjoint sets", []string{"alice"}, []string{"bob"}, []string{"alice", "bob"}}, + {"overlapping sets", []string{"alice", "bob"}, []string{"bob", "charlie"}, []string{"alice", "bob", "charlie"}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f1 := NewFiniteReaderSetFromSlice(tt.set) + f2 := NewFiniteReaderSetFromSlice(tt.other) + result := f1.Union(f2).(*FiniteReaderSet[string]) + + expected := NewFiniteReaderSetFromSlice(tt.expected) + assert.True(t, result.IsSubset(expected)) + assert.True(t, expected.IsSubset(result)) + }) + } + + t.Run("union with universal", func(t *testing.T) { + f := NewFiniteReaderSetFromSlice([]string{"alice"}) + u := NewUniversalReaderSet[string]() + result := f.Union(u) + assert.True(t, result.IsUniversal()) + }) + }) + + t.Run("Intersection", func(t *testing.T) { + tests := []struct { + name string + set []string + other []string + expected []string + }{ + {"empty with empty", []string{}, []string{}, []string{}}, + {"empty with non-empty", []string{}, []string{"alice"}, []string{}}, + {"disjoint sets", []string{"alice"}, []string{"bob"}, []string{}}, + {"overlapping sets", []string{"alice", "bob"}, []string{"bob", "charlie"}, []string{"bob"}}, + {"identical sets", []string{"alice", "bob"}, []string{"alice", "bob"}, []string{"alice", "bob"}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f1 := NewFiniteReaderSetFromSlice(tt.set) + f2 := NewFiniteReaderSetFromSlice(tt.other) + result := f1.Intersection(f2).(*FiniteReaderSet[string]) + + expected := NewFiniteReaderSetFromSlice(tt.expected) + assert.True(t, result.IsSubset(expected)) + assert.True(t, expected.IsSubset(result)) + }) + } + + t.Run("intersection with universal", func(t *testing.T) { + f := NewFiniteReaderSetFromSlice([]string{"alice"}) + u := NewUniversalReaderSet[string]() + result := f.Intersection(u) + assert.False(t, result.IsUniversal()) + assert.Equal(t, f.String(), result.String()) + }) + }) + + t.Run("String deterministic sorted", func(t *testing.T) { + tests := []struct { + name string + input []string + expected string + }{ + {"empty set", []string{}, "FiniteReaderSet({})"}, + {"single element", []string{"alice"}, "FiniteReaderSet({alice})"}, + {"multiple elements", []string{"charlie", "alice", "bob"}, "FiniteReaderSet({alice, bob, charlie})"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f := NewFiniteReaderSetFromSlice(tt.input) + assert.Equal(t, tt.expected, f.String()) + + f2 := NewFiniteReaderSetFromSlice(tt.input) + assert.Equal(t, f.String(), f2.String()) + }) + } + }) +} + +type unknownReaderSet struct{} + +func (u *unknownReaderSet) IsUniversal() bool { return false } +func (u *unknownReaderSet) IsSubset(_ ReaderSet[string]) bool { return false } +func (u *unknownReaderSet) Union(_ ReaderSet[string]) ReaderSet[string] { return u } +func (u *unknownReaderSet) Intersection(_ ReaderSet[string]) ReaderSet[string] { return u } +func (u *unknownReaderSet) String() string { return "unknown" } + +func TestFiniteReaderSetPanicsOnUnknownType(t *testing.T) { + unknown := &unknownReaderSet{} + + t.Run("IsSubset panics", func(t *testing.T) { + f := NewFiniteReaderSetFromSlice([]string{"alice"}) + assert.Panics(t, func() { + f.IsSubset(unknown) + }) + }) + + t.Run("Union panics", func(t *testing.T) { + f := NewFiniteReaderSetFromSlice([]string{"alice"}) + assert.Panics(t, func() { + f.Union(unknown) + }) + }) + + t.Run("Intersection panics", func(t *testing.T) { + f := NewFiniteReaderSetFromSlice([]string{"alice"}) + assert.Panics(t, func() { + f.Intersection(unknown) + }) + }) +} + +func TestPowersetLatticeConstruction(t *testing.T) { + universe := NewFiniteReaderSetFromSlice([]string{"alice", "bob", "charlie"}) + + t.Run("valid subset", func(t *testing.T) { + subset := NewFiniteReaderSetFromSlice([]string{"alice", "bob"}) + pl, err := NewPowersetLattice(subset, universe) + require.NoError(t, err) + assert.NotNil(t, pl) + }) + + t.Run("empty subset valid", func(t *testing.T) { + subset := EmptyReaderSet[string]() + pl, err := NewPowersetLattice(subset, universe) + require.NoError(t, err) + assert.NotNil(t, pl) + }) + + t.Run("universe as subset valid", func(t *testing.T) { + pl, err := NewPowersetLattice(universe, universe) + require.NoError(t, err) + assert.NotNil(t, pl) + }) + + t.Run("invalid subset returns error", func(t *testing.T) { + invalidSubset := NewFiniteReaderSetFromSlice([]string{"alice", "david"}) + pl, err := NewPowersetLattice(invalidSubset, universe) + assert.Error(t, err) + assert.Nil(t, pl) + }) + + t.Run("universal universe accepts any finite subset", func(t *testing.T) { + universalUniverse := UniversalReaders[string]() + subset := NewFiniteReaderSetFromSlice([]string{"alice", "anyone"}) + pl, err := NewPowersetLattice(subset, universalUniverse) + require.NoError(t, err) + assert.NotNil(t, pl) + }) +} + +func TestPowersetLatticeLaws(t *testing.T) { + universe := NewFiniteReaderSetFromSlice([]string{"alice", "bob", "charlie"}) + + createPowerset := func(readers []string) *PowersetLattice[string] { + subset := NewFiniteReaderSetFromSlice(readers) + pl, _ := NewPowersetLattice(subset, universe) + return pl + } + + t.Run("Leq reflexivity", func(t *testing.T) { + tests := [][]string{ + {}, + {"alice"}, + {"alice", "bob"}, + {"alice", "bob", "charlie"}, + } + + for _, readers := range tests { + pl := createPowerset(readers) + assert.True(t, pl.Leq(pl)) + } + }) + + t.Run("Join idempotency", func(t *testing.T) { + pl := createPowerset([]string{"alice", "bob"}) + joinResult := pl.Join(pl) + assert.True(t, pl.Leq(joinResult)) + assert.True(t, joinResult.Leq(pl)) + }) + + t.Run("Meet idempotency", func(t *testing.T) { + pl := createPowerset([]string{"alice", "bob"}) + meetResult := pl.Meet(pl) + assert.True(t, pl.Leq(meetResult)) + assert.True(t, meetResult.Leq(pl)) + }) + + t.Run("Join commutativity", func(t *testing.T) { + pl1 := createPowerset([]string{"alice"}) + pl2 := createPowerset([]string{"bob"}) + + join1 := pl1.Join(pl2) + join2 := pl2.Join(pl1) + + assert.True(t, join1.Leq(join2)) + assert.True(t, join2.Leq(join1)) + }) + + t.Run("Meet commutativity", func(t *testing.T) { + pl1 := createPowerset([]string{"alice", "bob"}) + pl2 := createPowerset([]string{"bob", "charlie"}) + + meet1 := pl1.Meet(pl2) + meet2 := pl2.Meet(pl1) + + assert.True(t, meet1.Leq(meet2)) + assert.True(t, meet2.Leq(meet1)) + }) +} + +func TestPowersetLatticeMustMatchUniverse(t *testing.T) { + universe1 := NewFiniteReaderSetFromSlice([]string{"alice", "bob"}) + universe2 := NewFiniteReaderSetFromSlice([]string{"charlie", "david"}) + + t.Run("mismatched finite universes panic", func(t *testing.T) { + pl1, _ := NewPowersetLattice(NewFiniteReaderSetFromSlice([]string{"alice"}), universe1) + pl2, _ := NewPowersetLattice(NewFiniteReaderSetFromSlice([]string{"charlie"}), universe2) + + assert.Panics(t, func() { pl1.Leq(pl2) }) + assert.Panics(t, func() { pl1.Join(pl2) }) + assert.Panics(t, func() { pl1.Meet(pl2) }) + }) + + t.Run("universal vs finite universe panic", func(t *testing.T) { + universalUniverse := UniversalReaders[string]() + pl1, _ := NewPowersetLattice(NewFiniteReaderSetFromSlice([]string{"alice"}), universe1) + pl2, _ := NewPowersetLattice(NewFiniteReaderSetFromSlice([]string{"alice"}), universalUniverse) + + assert.Panics(t, func() { pl1.Leq(pl2) }) + assert.Panics(t, func() { pl1.Join(pl2) }) + assert.Panics(t, func() { pl1.Meet(pl2) }) + }) + + t.Run("same universe does not panic", func(t *testing.T) { + pl1, _ := NewPowersetLattice(NewFiniteReaderSetFromSlice([]string{"alice"}), universe1) + pl2, _ := NewPowersetLattice(NewFiniteReaderSetFromSlice([]string{"bob"}), universe1) + + assert.NotPanics(t, func() { + pl1.Leq(pl2) + pl1.Join(pl2) + pl1.Meet(pl2) + }) + }) +} + +func TestReadersSecurityLabelConstructors(t *testing.T) { + t.Run("PublicTrusted", func(t *testing.T) { + label := PublicTrusted() + assert.True(t, label.IsHighIntegrity()) + assert.True(t, label.IsPublicConfidentiality()) + }) + + t.Run("PublicUntrusted", func(t *testing.T) { + label := PublicUntrusted() + assert.True(t, label.IsLowIntegrity()) + assert.True(t, label.IsPublicConfidentiality()) + }) + + t.Run("PrivateTrusted", func(t *testing.T) { + label := PrivateTrusted([]string{"alice", "bob"}) + assert.True(t, label.IsHighIntegrity()) + assert.False(t, label.IsPublicConfidentiality()) + readers := label.GetReaders() + assert.Equal(t, []string{"alice", "bob"}, readers) + }) + + t.Run("PrivateUntrusted", func(t *testing.T) { + label := PrivateUntrusted([]string{"alice"}) + assert.True(t, label.IsLowIntegrity()) + assert.False(t, label.IsPublicConfidentiality()) + readers := label.GetReaders() + assert.Equal(t, []string{"alice"}, readers) + }) +} + +func TestReadersSecurityLabelLeq(t *testing.T) { + publicTrusted := PublicTrusted() + publicUntrusted := PublicUntrusted() + privateTrusted := PrivateTrusted([]string{"alice"}) + privateUntrusted := PrivateUntrusted([]string{"alice"}) + + t.Run("public <= private in inverse lattice", func(t *testing.T) { + assert.True(t, publicTrusted.Leq(privateTrusted)) + assert.True(t, publicUntrusted.Leq(privateUntrusted)) + }) + + t.Run("private not <= public", func(t *testing.T) { + assert.False(t, privateTrusted.Leq(publicTrusted)) + assert.False(t, privateUntrusted.Leq(publicUntrusted)) + }) + + t.Run("trusted <= untrusted", func(t *testing.T) { + assert.True(t, publicTrusted.Leq(publicUntrusted)) + assert.True(t, privateTrusted.Leq(privateUntrusted)) + }) + + t.Run("untrusted not <= trusted", func(t *testing.T) { + assert.False(t, publicUntrusted.Leq(publicTrusted)) + assert.False(t, privateUntrusted.Leq(privateTrusted)) + }) + + t.Run("reflexivity", func(t *testing.T) { + assert.True(t, publicTrusted.Leq(publicTrusted)) + assert.True(t, privateTrusted.Leq(privateTrusted)) + }) +} + +func TestReadersSecurityLabelJoin(t *testing.T) { + publicTrusted := PublicTrusted() + privateTrustedAlice := PrivateTrusted([]string{"alice"}) + privateTrustedBob := PrivateTrusted([]string{"bob"}) + publicUntrusted := PublicUntrusted() + + t.Run("public join private equals private", func(t *testing.T) { + result := publicTrusted.Join(privateTrustedAlice) + assert.False(t, result.IsPublicConfidentiality()) + readers := result.GetReaders() + assert.Equal(t, []string{"alice"}, readers) + }) + + t.Run("private join public equals private", func(t *testing.T) { + result := privateTrustedAlice.Join(publicTrusted) + assert.False(t, result.IsPublicConfidentiality()) + readers := result.GetReaders() + assert.Equal(t, []string{"alice"}, readers) + }) + + t.Run("alice join bob equals intersection", func(t *testing.T) { + result := privateTrustedAlice.Join(privateTrustedBob) + assert.False(t, result.IsPublicConfidentiality()) + readers := result.GetReaders() + assert.Empty(t, readers) + }) + + t.Run("trusted join untrusted equals untrusted", func(t *testing.T) { + result := publicTrusted.Join(publicUntrusted) + assert.True(t, result.IsLowIntegrity()) + }) +} + +func TestReadersSecurityLabelMeet(t *testing.T) { + publicTrusted := PublicTrusted() + privateTrustedAlice := PrivateTrusted([]string{"alice"}) + privateTrustedBob := PrivateTrusted([]string{"bob"}) + publicUntrusted := PublicUntrusted() + privateTrustedAliceBob := PrivateTrusted([]string{"alice", "bob"}) + + t.Run("alice meet bob equals union", func(t *testing.T) { + result := privateTrustedAlice.Meet(privateTrustedBob) + readers := result.GetReaders() + assert.ElementsMatch(t, []string{"alice", "bob"}, readers) + }) + + t.Run("private meet public equals public", func(t *testing.T) { + result := privateTrustedAlice.Meet(publicTrusted) + assert.True(t, result.IsPublicConfidentiality()) + }) + + t.Run("alice meet alice-bob equals alice-bob", func(t *testing.T) { + result := privateTrustedAlice.Meet(privateTrustedAliceBob) + readers := result.GetReaders() + assert.ElementsMatch(t, []string{"alice", "bob"}, readers) + }) + + t.Run("trusted meet untrusted equals trusted", func(t *testing.T) { + result := publicTrusted.Meet(publicUntrusted) + assert.True(t, result.IsHighIntegrity()) + }) +} + +func TestGetReaders(t *testing.T) { + t.Run("public returns nil", func(t *testing.T) { + label := PublicTrusted() + assert.Nil(t, label.GetReaders()) + }) + + t.Run("private returns sorted slice", func(t *testing.T) { + label := PrivateTrusted([]string{"charlie", "alice", "bob"}) + readers := label.GetReaders() + assert.Equal(t, []string{"alice", "bob", "charlie"}, readers) + }) + + t.Run("empty private returns empty slice", func(t *testing.T) { + label := PrivateTrusted([]string{}) + readers := label.GetReaders() + assert.NotNil(t, readers) + assert.Empty(t, readers) + }) +} + +func TestReadersSecurityLabelJSON(t *testing.T) { + t.Run("public round-trip", func(t *testing.T) { + original := PublicTrusted() + data, err := json.Marshal(original) + require.NoError(t, err) + + var restored ReadersSecurityLabel + err = json.Unmarshal(data, &restored) + require.NoError(t, err) + + assert.True(t, original.Leq(restored)) + assert.True(t, restored.Leq(original)) + assert.True(t, restored.IsPublicConfidentiality()) + assert.True(t, restored.IsHighIntegrity()) + }) + + t.Run("private round-trip", func(t *testing.T) { + original := PrivateTrusted([]string{"alice", "bob"}) + data, err := json.Marshal(original) + require.NoError(t, err) + + var restored ReadersSecurityLabel + err = json.Unmarshal(data, &restored) + require.NoError(t, err) + + assert.True(t, original.Leq(restored)) + assert.True(t, restored.Leq(original)) + assert.False(t, restored.IsPublicConfidentiality()) + assert.ElementsMatch(t, []string{"alice", "bob"}, restored.GetReaders()) + }) + + t.Run("untrusted round-trip", func(t *testing.T) { + original := PrivateUntrusted([]string{"alice"}) + data, err := json.Marshal(original) + require.NoError(t, err) + + var restored ReadersSecurityLabel + err = json.Unmarshal(data, &restored) + require.NoError(t, err) + + assert.True(t, original.Leq(restored)) + assert.True(t, restored.Leq(original)) + assert.True(t, restored.IsLowIntegrity()) + }) +} + +func TestToDict(t *testing.T) { + t.Run("public trusted", func(t *testing.T) { + label := PublicTrusted() + dict := label.ToDict() + + assert.Equal(t, "high", dict["integrity"]) + confidentiality := dict["confidentiality"].([]string) + assert.Equal(t, []string{"public"}, confidentiality) + }) + + t.Run("public untrusted", func(t *testing.T) { + label := PublicUntrusted() + dict := label.ToDict() + + assert.Equal(t, "low", dict["integrity"]) + confidentiality := dict["confidentiality"].([]string) + assert.Equal(t, []string{"public"}, confidentiality) + }) + + t.Run("private sorted readers", func(t *testing.T) { + label := PrivateTrusted([]string{"charlie", "alice", "bob"}) + dict := label.ToDict() + + assert.Equal(t, "high", dict["integrity"]) + confidentiality := dict["confidentiality"].([]string) + assert.Equal(t, []string{"alice", "bob", "charlie"}, confidentiality) + }) +} + +func TestReadersSecurityLabelFromDict(t *testing.T) { + t.Run("parse public", func(t *testing.T) { + dict := map[string]any{ + "integrity": "high", + "confidentiality": []string{"public"}, + } + + label := ReadersSecurityLabelFromDict(dict) + assert.True(t, label.IsHighIntegrity()) + assert.True(t, label.IsPublicConfidentiality()) + }) + + t.Run("parse private", func(t *testing.T) { + dict := map[string]any{ + "integrity": "low", + "confidentiality": []string{"alice", "bob"}, + } + + label := ReadersSecurityLabelFromDict(dict) + assert.True(t, label.IsLowIntegrity()) + assert.False(t, label.IsPublicConfidentiality()) + readers := label.GetReaders() + assert.ElementsMatch(t, []string{"alice", "bob"}, readers) + }) + + t.Run("parse with []any confidentiality", func(t *testing.T) { + dict := map[string]any{ + "integrity": "high", + "confidentiality": []any{"alice", "bob"}, + } + + label := ReadersSecurityLabelFromDict(dict) + readers := label.GetReaders() + assert.ElementsMatch(t, []string{"alice", "bob"}, readers) + }) + + t.Run("defaults to high integrity", func(t *testing.T) { + dict := map[string]any{ + "confidentiality": []string{"public"}, + } + + label := ReadersSecurityLabelFromDict(dict) + assert.True(t, label.IsHighIntegrity()) + }) + + t.Run("defaults to public confidentiality", func(t *testing.T) { + dict := map[string]any{ + "integrity": "high", + } + + label := ReadersSecurityLabelFromDict(dict) + assert.True(t, label.IsPublicConfidentiality()) + }) +}