From 031c7eebd388f211af79d2c9c61e069473bd384d Mon Sep 17 00:00:00 2001 From: JoannaaKL Date: Wed, 6 May 2026 15:36:59 +0200 Subject: [PATCH 1/7] Add ifc labels --- pkg/github/context_tools.go | 9 +- pkg/ifc/labelling_engine_readers.go | 6 + pkg/ifc/lattice.go | 221 ++++++++++++ pkg/ifc/readers_lattice.go | 510 ++++++++++++++++++++++++++++ 4 files changed, 745 insertions(+), 1 deletion(-) create mode 100644 pkg/ifc/labelling_engine_readers.go create mode 100644 pkg/ifc/lattice.go create mode 100644 pkg/ifc/readers_lattice.go diff --git a/pkg/github/context_tools.go b/pkg/github/context_tools.go index 902734481a..074df320e3 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/ifc/labelling_engine_readers.go b/pkg/ifc/labelling_engine_readers.go new file mode 100644 index 0000000000..1226309c04 --- /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 0000000000..8c48940b8c --- /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 0000000000..790a48c73e --- /dev/null +++ b/pkg/ifc/readers_lattice.go @@ -0,0 +1,510 @@ +// 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 +// universe := UniversalReaders[string]() +// publicReaders := NewFiniteReaderSetFromSlice([]string{"alice", "bob"}) +// label, _ := NewPowersetLattice(publicReaders, 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" + "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 { + return false + } + 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 { + return f + } + 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 { + return NewFiniteReaderSet[T](make(map[T]struct{})) + } + 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({})" + } + var b strings.Builder + b.WriteString("FiniteReaderSet({") + first := true + for member := range f.members { + if !first { + b.WriteString(", ") + } + first = false + fmt.Fprintf(&b, "%v", member) + } + 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 { + return p.subset.IsSubset(other.subset) +} + +func (p *PowersetLattice[T]) Join(other *PowersetLattice[T]) *PowersetLattice[T] { + return &PowersetLattice[T]{ + subset: p.subset.Union(other.subset), + universe: p.universe, + } +} + +func (p *PowersetLattice[T]) Meet(other *PowersetLattice[T]) *PowersetLattice[T] { + return &PowersetLattice[T]{ + subset: p.subset.Intersection(other.subset), + universe: p.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) + } + 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 an 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() +) From 2d354fa18591537c59f86c56ba77925a14bf50f3 Mon Sep 17 00:00:00 2001 From: JoannaaKL Date: Wed, 6 May 2026 15:57:39 +0200 Subject: [PATCH 2/7] Add test --- pkg/github/context_tools_test.go | 60 ++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/pkg/github/context_tools_test.go b/pkg/github/context_tools_test.go index 39f2058bec..86677b6a7b 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() From 120d0c65acb8a416ade8a3e75ebb73a8f7fd475d Mon Sep 17 00:00:00 2001 From: JoannaaKL Date: Wed, 6 May 2026 16:34:29 +0200 Subject: [PATCH 3/7] Address PR review: deterministic output, type safety, universe validation, and tests - Fix grammar in ReadersSecurityLabelFromDict godoc - Sort GetReaders and FiniteReaderSet.String output for determinism - Fix godoc example to use UniversalReaders for public label - Panic on unsupported ReaderSet types in Union/Intersection/IsSubset - Add universe mismatch validation in PowersetLattice Join/Meet/Leq - Add comprehensive unit tests for pkg/ifc (lattice laws, serialization, panics) --- pkg/ifc/readers_lattice.go | 50 ++- pkg/ifc/readers_lattice_test.go | 610 ++++++++++++++++++++++++++++++++ 2 files changed, 648 insertions(+), 12 deletions(-) create mode 100644 pkg/ifc/readers_lattice_test.go diff --git a/pkg/ifc/readers_lattice.go b/pkg/ifc/readers_lattice.go index 790a48c73e..a2a9c3b620 100644 --- a/pkg/ifc/readers_lattice.go +++ b/pkg/ifc/readers_lattice.go @@ -10,10 +10,9 @@ // // Example usage: // -// // Create a label readable by everyone +// // Create a label readable by everyone (public) // universe := UniversalReaders[string]() -// publicReaders := NewFiniteReaderSetFromSlice([]string{"alice", "bob"}) -// label, _ := NewPowersetLattice(publicReaders, universe) +// label, _ := NewPowersetLattice(universe, universe) // // // Create a label for specific readers only // finiteUniverse := NewFiniteReaderSetFromSlice([]string{"alice", "bob", "charlie"}) @@ -25,6 +24,7 @@ package ifc import ( "encoding/json" "fmt" + "sort" "strings" ) @@ -90,7 +90,7 @@ func (f *FiniteReaderSet[T]) IsSubset(other ReaderSet[T]) bool { } otherFinite, ok := other.(*FiniteReaderSet[T]) if !ok { - return false + panic(fmt.Sprintf("unsupported ReaderSet implementation: %T", other)) } for member := range f.members { if _, exists := otherFinite.members[member]; !exists { @@ -106,7 +106,7 @@ func (f *FiniteReaderSet[T]) Union(other ReaderSet[T]) ReaderSet[T] { } otherFinite, ok := other.(*FiniteReaderSet[T]) if !ok { - return f + panic(fmt.Sprintf("unsupported ReaderSet implementation: %T", other)) } union := make(map[T]struct{}, len(f.members)+len(otherFinite.members)) for member := range f.members { @@ -124,7 +124,7 @@ func (f *FiniteReaderSet[T]) Intersection(other ReaderSet[T]) ReaderSet[T] { } otherFinite, ok := other.(*FiniteReaderSet[T]) if !ok { - return NewFiniteReaderSet[T](make(map[T]struct{})) + panic(fmt.Sprintf("unsupported ReaderSet implementation: %T", other)) } intersection := make(map[T]struct{}) for member := range f.members { @@ -139,15 +139,18 @@ 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({") - first := true - for member := range f.members { - if !first { + for i, s := range strs { + if i > 0 { b.WriteString(", ") } - first = false - fmt.Fprintf(&b, "%v", member) + b.WriteString(s) } b.WriteString("})") return b.String() @@ -192,10 +195,12 @@ func copySet[T comparable](in map[T]struct{}) map[T]struct{} { } 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, @@ -203,12 +208,32 @@ func (p *PowersetLattice[T]) Join(other *PowersetLattice[T]) *PowersetLattice[T] } 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()) } @@ -318,6 +343,7 @@ func (l ReadersSecurityLabel) GetReaders() []string { for reader := range finiteSet.members { readers = append(readers, reader) } + sort.Strings(readers) return readers } @@ -396,7 +422,7 @@ func (l *ReadersSecurityLabel) UnmarshalJSON(data []byte) error { return nil } -// ReadersSecurityLabelFromDict creates an ReadersSecurityLabel from a dictionary format. +// ReadersSecurityLabelFromDict creates a ReadersSecurityLabel from a dictionary format. func ReadersSecurityLabelFromDict(dict map[string]any) ReadersSecurityLabel { // Parse integrity integrityStr := "high" diff --git a/pkg/ifc/readers_lattice_test.go b/pkg/ifc/readers_lattice_test.go new file mode 100644 index 0000000000..da322da0d8 --- /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()) + }) +} From 5565f955120de0fdd6a578ca6c20cff2319e3a05 Mon Sep 17 00:00:00 2001 From: JoannaaKL Date: Mon, 11 May 2026 09:37:40 +0200 Subject: [PATCH 4/7] Add a test --- pkg/github/context_tools.go | 5 +++-- pkg/ifc/labelling_engine_readers_test.go | 13 +++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) create mode 100644 pkg/ifc/labelling_engine_readers_test.go diff --git a/pkg/github/context_tools.go b/pkg/github/context_tools.go index 074df320e3..9f84c02118 100644 --- a/pkg/github/context_tools.go +++ b/pkg/github/context_tools.go @@ -106,9 +106,10 @@ func GetMe(t translations.TranslationHelperFunc) inventory.ServerTool { result := MarshalledTextResult(minimalUser) if deps.GetFlags(ctx).InsidersMode { - result.Meta = mcp.Meta{ - "ifc": ifc.LabelGetMe(), + if result.Meta == nil { + result.Meta = mcp.Meta{} } + result.Meta["ifc"] = ifc.LabelGetMe() } return result, nil, nil }, diff --git a/pkg/ifc/labelling_engine_readers_test.go b/pkg/ifc/labelling_engine_readers_test.go new file mode 100644 index 0000000000..94e384eb69 --- /dev/null +++ b/pkg/ifc/labelling_engine_readers_test.go @@ -0,0 +1,13 @@ +package ifc + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestLabelGetMe(t *testing.T) { + label := LabelGetMe() + assert.True(t, label.IsHighIntegrity()) + assert.True(t, label.IsPublicConfidentiality()) +} From 6aab5aca5da45cb6b4016033ac4206e9c6dc32f1 Mon Sep 17 00:00:00 2001 From: JoannaaKL Date: Mon, 11 May 2026 09:58:30 +0200 Subject: [PATCH 5/7] Pass parameters --- script/get-me | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/script/get-me b/script/get-me index 954f57cec0..eac5600a1d 100755 --- a/script/get-me +++ b/script/get-me @@ -7,11 +7,11 @@ output=$( echo '{"jsonrpc":"2.0","method":"notifications/initialized","params":{}}' echo '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"get_me","arguments":{}}}' sleep 1 - ) | go run cmd/github-mcp-server/main.go stdio 2>/dev/null | tail -1 + ) | go run cmd/github-mcp-server/main.go stdio "$@" 2>/dev/null | tail -1 ) if command -v jq &> /dev/null; then - echo "$output" | jq '.result.content[0].text | fromjson' + echo "$output" | jq '.result | {_meta, content: (.content[0].text | fromjson)}' else echo "$output" fi From 68a99a3971129e9f49e2f2f923b0b075ab672b0d Mon Sep 17 00:00:00 2001 From: JoannaaKL Date: Mon, 11 May 2026 11:05:00 +0200 Subject: [PATCH 6/7] Remove lattice --- pkg/github/context_tools_test.go | 2 +- pkg/ifc/ifc.go | 29 ++ pkg/ifc/labelling_engine_readers.go | 6 - pkg/ifc/labelling_engine_readers_test.go | 13 - pkg/ifc/lattice.go | 221 -------- pkg/ifc/readers_lattice.go | 536 -------------------- pkg/ifc/readers_lattice_test.go | 610 ----------------------- 7 files changed, 30 insertions(+), 1387 deletions(-) create mode 100644 pkg/ifc/ifc.go delete mode 100644 pkg/ifc/labelling_engine_readers.go delete mode 100644 pkg/ifc/labelling_engine_readers_test.go delete mode 100644 pkg/ifc/lattice.go delete mode 100644 pkg/ifc/readers_lattice.go delete mode 100644 pkg/ifc/readers_lattice_test.go diff --git a/pkg/github/context_tools_test.go b/pkg/github/context_tools_test.go index 86677b6a7b..365a019ab6 100644 --- a/pkg/github/context_tools_test.go +++ b/pkg/github/context_tools_test.go @@ -191,7 +191,7 @@ func Test_GetMe_IFC_InsidersMode(t *testing.T) { err = json.Unmarshal(ifcJSON, &ifcMap) require.NoError(t, err) - assert.Equal(t, "high", ifcMap["integrity"]) + assert.Equal(t, "trusted", ifcMap["integrity"]) confList, ok := ifcMap["confidentiality"].([]any) require.True(t, ok, "confidentiality should be a list") require.Len(t, confList, 1) diff --git a/pkg/ifc/ifc.go b/pkg/ifc/ifc.go new file mode 100644 index 0000000000..cf0d72114f --- /dev/null +++ b/pkg/ifc/ifc.go @@ -0,0 +1,29 @@ +// Package ifc provides Information Flow Control labels for annotating MCP tool outputs. +// The actual IFC enforcement engine lives in a separate service; this package only +// defines the label schema used for annotations. +package ifc + +type Integrity string + +const ( + IntegrityTrusted Integrity = "trusted" + IntegrityUntrusted Integrity = "untrusted" +) + +type Confidentiality string + +const ( + ConfidentialityPublic Confidentiality = "public" +) + +type SecurityLabel struct { + Integrity Integrity `json:"integrity"` + Confidentiality []Confidentiality `json:"confidentiality"` +} + +func LabelGetMe() SecurityLabel { + return SecurityLabel{ + Integrity: IntegrityTrusted, + Confidentiality: []Confidentiality{ConfidentialityPublic}, + } +} diff --git a/pkg/ifc/labelling_engine_readers.go b/pkg/ifc/labelling_engine_readers.go deleted file mode 100644 index 1226309c04..0000000000 --- a/pkg/ifc/labelling_engine_readers.go +++ /dev/null @@ -1,6 +0,0 @@ -package ifc - -// LabelGetMe returns a label for get_me: trusted, universal readers. -func LabelGetMe() ReadersSecurityLabel { - return PublicTrusted() -} diff --git a/pkg/ifc/labelling_engine_readers_test.go b/pkg/ifc/labelling_engine_readers_test.go deleted file mode 100644 index 94e384eb69..0000000000 --- a/pkg/ifc/labelling_engine_readers_test.go +++ /dev/null @@ -1,13 +0,0 @@ -package ifc - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestLabelGetMe(t *testing.T) { - label := LabelGetMe() - assert.True(t, label.IsHighIntegrity()) - assert.True(t, label.IsPublicConfidentiality()) -} diff --git a/pkg/ifc/lattice.go b/pkg/ifc/lattice.go deleted file mode 100644 index 8c48940b8c..0000000000 --- a/pkg/ifc/lattice.go +++ /dev/null @@ -1,221 +0,0 @@ -// 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 deleted file mode 100644 index a2a9c3b620..0000000000 --- a/pkg/ifc/readers_lattice.go +++ /dev/null @@ -1,536 +0,0 @@ -// 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 deleted file mode 100644 index da322da0d8..0000000000 --- a/pkg/ifc/readers_lattice_test.go +++ /dev/null @@ -1,610 +0,0 @@ -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()) - }) -} From fb761209bcbfc9ba864f619b59e4653d5f7c8b0a Mon Sep 17 00:00:00 2001 From: JoannaaKL Date: Mon, 11 May 2026 11:15:38 +0200 Subject: [PATCH 7/7] Script update --- script/get-me | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/script/get-me b/script/get-me index eac5600a1d..ffd24a357f 100755 --- a/script/get-me +++ b/script/get-me @@ -6,12 +6,12 @@ output=$( echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"get-me-script","version":"1.0.0"}}}' echo '{"jsonrpc":"2.0","method":"notifications/initialized","params":{}}' echo '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"get_me","arguments":{}}}' - sleep 1 - ) | go run cmd/github-mcp-server/main.go stdio "$@" 2>/dev/null | tail -1 + sleep 3 + ) | go run cmd/github-mcp-server/main.go stdio "$@" 2>/dev/null | grep '"id":2' ) if command -v jq &> /dev/null; then - echo "$output" | jq '.result | {_meta, content: (.content[0].text | fromjson)}' + echo "$output" | jq '{_meta: .result._meta, content: (.result.content[0].text | fromjson)}' else echo "$output" fi