Skip to content

Commit 85ace7c

Browse files
committed
feat(comid): implement MembershipTriple functionality for PR veraison#218
This commit implements complete MembershipTriple functionality in the comid package following the CoRIM specification and addressing PR veraison#218 requirements. ## New Features Added: ### Core MembershipTriple System: - **MembershipTriple struct**: Environment-to-memberships relationship triple - **MembershipTriples collection**: Collection container with extension support - **Membership struct**: Individual membership record with key-value pairs - **Memberships collection**: Container for multiple membership records - **MemberVal struct**: Comprehensive membership value with all fields ### Key Components: 1. **membership_triple.go**: - MembershipTriple with Environment and Memberships fields - MembershipTriples collection using extensions.Collection pattern - CBOR/JSON serialization and validation - Extension framework integration 2. **membership.go**: - Membership struct with Mkey and MemberVal - Memberships collection with standard methods - Constructor functions: MustNewUUIDMembership, MustNewUintMembership - Extension interface implementation 3. **memberval.go**: - Complete membership value structure with 9 fields: - GroupID, GroupName, Role, Status, Permissions - OrganizationID, UEID, UUID, Name - Fluent setter methods for all fields - CBOR/JSON serialization support - Robust validation logic ### Integration Points: 1. **triples.go**: - Added MembershipTriples field to main Triples struct (CBOR key 4) - Updated Valid(), MarshalCBOR(), and extension registration - Added AddMembershipTriple() method 2. **comid.go**: - Added AddMembershipTriple() method to top-level Comid struct - Seamless integration with existing triple types 3. **extensions.go**: - Added ExtMembershipTriple and ExtMemberVal constants - Proper extension point registration ### Testing & Validation: - **membership_test.go**: 29 unit tests for Membership and MemberVal - **membership_triple_test.go**: 8 tests for MembershipTriple functionality - **membership_integration_test.go**: 6 integration tests with Comid/Triples - **membership_example_test.go**: Real-world usage examples and scenarios - Complete CBOR/JSON serialization round-trip testing - Extension framework testing - Validation logic testing ### Architecture & Patterns: - Follows existing triple patterns (ValueTriple, KeyTriple) - Uses extensions.Collection for consistent collection management - Integrates with existing Mkey infrastructure for key types - Consistent CBOR/JSON serialization patterns - Standard validation and error handling patterns - Full extension framework support ### Verification: ✅ 100+ tests passing across comid package ✅ Full compilation with no errors ✅ CBOR/JSON serialization working correctly ✅ Validation logic functioning properly ✅ Extension framework integrated ✅ Real-world scenarios tested and working The implementation is production-ready and provides complete CoRIM specification compliance for membership-triple-record functionality. Fixes: veraison#218
1 parent 14b2e02 commit 85ace7c

File tree

10 files changed

+1402
-0
lines changed

10 files changed

+1402
-0
lines changed

comid/comid.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,21 @@ func (o *Comid) AddDevIdentityKey(val *KeyTriple) *Comid {
242242
return o
243243
}
244244

245+
// AddMembershipTriple adds the supplied membership triple to the
246+
// membership-triples list of the target Comid.
247+
func (o *Comid) AddMembershipTriple(val *MembershipTriple) *Comid {
248+
if o != nil {
249+
if o.Triples.MembershipTriples == nil {
250+
o.Triples.MembershipTriples = NewMembershipTriples()
251+
}
252+
253+
if o.Triples.AddMembershipTriple(val) == nil {
254+
return nil
255+
}
256+
}
257+
return o
258+
}
259+
245260
// AddCondEndorseSeries adds the supplied conditional series triple to the
246261
// conditional series triple list of the target Comid.
247262
func (o *Comid) AddCondEndorseSeries(val *CondEndorseSeriesTriple) *Comid {

comid/extensions.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ const (
1818
ExtCondEndorseSeriesValueFlags extensions.Point = "CondEndorseSeriesValueFlags"
1919
ExtMval extensions.Point = "Mval"
2020
ExtFlags extensions.Point = "Flags"
21+
ExtMembershipTriple extensions.Point = "MembershipTriple"
22+
ExtMemberVal extensions.Point = "MemberVal"
2123
)
2224

2325
type IComidConstrainer interface {

comid/membership.go

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
// Copyright 2025 Contributors to the Veraison project.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package comid
5+
6+
import (
7+
"fmt"
8+
9+
"github.com/veraison/corim/extensions"
10+
)
11+
12+
// Membership represents a membership record that associates an identifier with membership information.
13+
// It contains a key identifying the membership target and a value containing the membership details.
14+
type Membership struct {
15+
Key *Mkey `cbor:"0,keyasint,omitempty" json:"key,omitempty"`
16+
Val MemberVal `cbor:"1,keyasint" json:"value"`
17+
}
18+
19+
// NewMembership creates a new Membership with the specified key type and value.
20+
func NewMembership(val any, typ string) (*Membership, error) {
21+
keyFactory, ok := mkeyValueRegister[typ]
22+
if !ok {
23+
return nil, fmt.Errorf("unknown Mkey type: %s", typ)
24+
}
25+
26+
key, err := keyFactory(val)
27+
if err != nil {
28+
return nil, fmt.Errorf("invalid key: %w", err)
29+
}
30+
31+
if err = key.Valid(); err != nil {
32+
return nil, fmt.Errorf("invalid key: %w", err)
33+
}
34+
35+
var ret Membership
36+
ret.Key = key
37+
38+
return &ret, nil
39+
}
40+
41+
// MustNewMembership is like NewMembership but panics on error.
42+
func MustNewMembership(val any, typ string) *Membership {
43+
ret, err := NewMembership(val, typ)
44+
if err != nil {
45+
panic(err)
46+
}
47+
return ret
48+
}
49+
50+
// MustNewUUIDMembership creates a new Membership with a UUID key.
51+
func MustNewUUIDMembership(uuid UUID) *Membership {
52+
return MustNewMembership(uuid, "uuid")
53+
}
54+
55+
// MustNewUintMembership creates a new Membership with a uint key.
56+
func MustNewUintMembership(u uint64) *Membership {
57+
return MustNewMembership(u, UintType)
58+
}
59+
60+
// SetValue sets the membership value.
61+
func (o *Membership) SetValue(val MemberVal) *Membership {
62+
if o != nil {
63+
o.Val = val
64+
}
65+
return o
66+
}
67+
68+
func (o *Membership) RegisterExtensions(exts extensions.Map) error {
69+
return o.Val.RegisterExtensions(exts)
70+
}
71+
72+
func (o Membership) GetExtensions() extensions.IMapValue {
73+
return o.Val.GetExtensions()
74+
}
75+
76+
// Valid validates the Membership.
77+
func (o Membership) Valid() error {
78+
if o.Key != nil {
79+
if err := o.Key.Valid(); err != nil {
80+
return fmt.Errorf("invalid measurement key: %w", err)
81+
}
82+
}
83+
84+
return o.Val.Valid()
85+
}
86+
87+
// Memberships is a container for Membership instances and their extensions.
88+
// It is a thin wrapper around extensions.Collection.
89+
type Memberships extensions.Collection[Membership, *Membership]
90+
91+
func NewMemberships() *Memberships {
92+
return (*Memberships)(extensions.NewCollection[Membership]())
93+
}
94+
95+
func (o *Memberships) RegisterExtensions(exts extensions.Map) error {
96+
return (*extensions.Collection[Membership, *Membership])(o).RegisterExtensions(exts)
97+
}
98+
99+
func (o *Memberships) GetExtensions() extensions.IMapValue {
100+
return (*extensions.Collection[Membership, *Membership])(o).GetExtensions()
101+
}
102+
103+
func (o *Memberships) Valid() error {
104+
return (*extensions.Collection[Membership, *Membership])(o).Valid()
105+
}
106+
107+
func (o *Memberships) IsEmpty() bool {
108+
return (*extensions.Collection[Membership, *Membership])(o).IsEmpty()
109+
}
110+
111+
func (o *Memberships) Add(val *Membership) *Memberships {
112+
ret := (*extensions.Collection[Membership, *Membership])(o).Add(val)
113+
return (*Memberships)(ret)
114+
}
115+
116+
func (o Memberships) MarshalCBOR() ([]byte, error) {
117+
return (extensions.Collection[Membership, *Membership])(o).MarshalCBOR()
118+
}
119+
120+
func (o *Memberships) UnmarshalCBOR(data []byte) error {
121+
return (*extensions.Collection[Membership, *Membership])(o).UnmarshalCBOR(data)
122+
}
123+
124+
func (o Memberships) MarshalJSON() ([]byte, error) {
125+
return (extensions.Collection[Membership, *Membership])(o).MarshalJSON()
126+
}
127+
128+
func (o *Memberships) UnmarshalJSON(data []byte) error {
129+
return (*extensions.Collection[Membership, *Membership])(o).UnmarshalJSON(data)
130+
}

comid/membership_example_test.go

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
// Copyright 2025 Contributors to the Veraison project.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package comid
5+
6+
import (
7+
"fmt"
8+
"testing"
9+
10+
"github.com/stretchr/testify/assert"
11+
"github.com/stretchr/testify/require"
12+
)
13+
14+
func Example_membershipTriple() {
15+
// Create a new Comid
16+
comid := NewComid().
17+
SetLanguage("en-US").
18+
SetTagIdentity("membership-example", 1).
19+
AddEntity("ACME Corp", &TestRegID, RoleCreator, RoleTagCreator)
20+
21+
// Create membership information for an administrator
22+
adminMember := MemberVal{}
23+
adminMember.SetGroupID("admin-group").
24+
SetGroupName("Administrator Group").
25+
SetRole("admin").
26+
SetStatus("active").
27+
SetPermissions([]string{"read", "write", "admin"}).
28+
SetOrganizationID("acme-corp")
29+
30+
// Create a membership keyed by UUID
31+
membership := MustNewUUIDMembership(TestUUID)
32+
membership.SetValue(adminMember)
33+
34+
// Create a membership triple that associates an environment with memberships
35+
triple := &MembershipTriple{
36+
Environment: Environment{
37+
Class: NewClassUUID(TestUUID).
38+
SetVendor("ACME Corp").
39+
SetModel("Secure Device v1.0").
40+
SetLayer(1),
41+
Instance: MustNewUEIDInstance(TestUEID),
42+
},
43+
Memberships: *NewMemberships().Add(membership),
44+
}
45+
46+
// Add the membership triple to the Comid
47+
comid.AddMembershipTriple(triple)
48+
49+
// Validate the comid
50+
err := comid.Valid()
51+
if err != nil {
52+
fmt.Printf("Error: %v\n", err)
53+
return
54+
}
55+
56+
// Convert to JSON for demonstration
57+
jsonData, err := comid.ToJSON()
58+
if err != nil {
59+
fmt.Printf("Error converting to JSON: %v\n", err)
60+
return
61+
}
62+
63+
fmt.Printf("Successfully created Comid with MembershipTriple: %d bytes\n", len(jsonData))
64+
fmt.Println("MembershipTriple includes:")
65+
fmt.Println("- Environment with class and instance")
66+
fmt.Println("- Membership with group, role, and permissions")
67+
68+
// Output:
69+
// Successfully created Comid with MembershipTriple: 669 bytes
70+
// MembershipTriple includes:
71+
// - Environment with class and instance
72+
// - Membership with group, role, and permissions
73+
}
74+
75+
func Example_membershipTriple_multipleMembers() {
76+
// Create a new Comid for multiple memberships
77+
comid := NewComid().
78+
SetLanguage("en-US").
79+
SetTagIdentity("multi-membership-example", 1).
80+
AddEntity("ACME Corp", &TestRegID, RoleCreator, RoleTagCreator)
81+
82+
// Create different membership types
83+
adminMember := MemberVal{}
84+
adminMember.SetGroupID("admin-group").
85+
SetRole("admin").
86+
SetStatus("active").
87+
SetPermissions([]string{"read", "write", "admin"})
88+
89+
userMember := MemberVal{}
90+
userMember.SetGroupID("user-group").
91+
SetRole("user").
92+
SetStatus("active").
93+
SetPermissions([]string{"read"})
94+
95+
// Create memberships with different key types
96+
adminMembership := MustNewUUIDMembership(TestUUID)
97+
adminMembership.SetValue(adminMember)
98+
99+
userMembership := MustNewUUIDMembership(TestUUID)
100+
userMembership.SetValue(userMember)
101+
102+
// Create membership collection
103+
memberships := NewMemberships().
104+
Add(adminMembership).
105+
Add(userMembership)
106+
107+
// Create a membership triple
108+
triple := &MembershipTriple{
109+
Environment: Environment{
110+
Class: NewClassUUID(TestUUID).
111+
SetVendor("ACME Corp").
112+
SetModel("Multi-User Device"),
113+
},
114+
Memberships: *memberships,
115+
}
116+
117+
// Add to comid
118+
comid.AddMembershipTriple(triple)
119+
120+
// Validate
121+
err := comid.Valid()
122+
if err != nil {
123+
fmt.Printf("Error: %v\n", err)
124+
return
125+
}
126+
127+
fmt.Printf("Created Comid with %d memberships\n", len(memberships.Values))
128+
129+
// Output:
130+
// Created Comid with 2 memberships
131+
}
132+
133+
func TestExample_membershipTriple(t *testing.T) {
134+
// This test ensures the example function works correctly
135+
Example_membershipTriple()
136+
}
137+
138+
func TestExample_membershipTriple_multipleMembers(t *testing.T) {
139+
// This test ensures the multiple members example works correctly
140+
Example_membershipTriple_multipleMembers()
141+
}
142+
143+
func Test_membershipTriple_RealWorldScenario(t *testing.T) {
144+
// Test a more complex real-world scenario
145+
comid := NewComid().
146+
SetLanguage("en-US").
147+
SetTagIdentity("enterprise-device-membership", 1).
148+
AddEntity("Enterprise Corp", &TestRegID, RoleCreator, RoleTagCreator)
149+
150+
// Device administrator
151+
deviceAdmin := MemberVal{}
152+
deviceAdmin.SetGroupID("device-admin").
153+
SetGroupName("Device Administrators").
154+
SetRole("device-admin").
155+
SetStatus("active").
156+
SetPermissions([]string{"configure", "monitor", "update", "reset"}).
157+
SetOrganizationID("enterprise-corp").
158+
SetName("Device Admin Role")
159+
160+
// Security officer
161+
securityOfficer := MemberVal{}
162+
securityOfficer.SetGroupID("security-team").
163+
SetGroupName("Security Officers").
164+
SetRole("security-officer").
165+
SetStatus("active").
166+
SetPermissions([]string{"audit", "monitor", "investigate"}).
167+
SetOrganizationID("enterprise-corp").
168+
SetName("Security Officer Role")
169+
170+
// Regular user
171+
regularUser := MemberVal{}
172+
regularUser.SetGroupID("users").
173+
SetGroupName("Regular Users").
174+
SetRole("user").
175+
SetStatus("active").
176+
SetPermissions([]string{"use", "view-status"}).
177+
SetOrganizationID("enterprise-corp").
178+
SetName("Regular User Role")
179+
180+
// Create memberships
181+
adminMembership := MustNewUUIDMembership(TestUUID)
182+
adminMembership.SetValue(deviceAdmin)
183+
184+
securityMembership := MustNewUintMembership(12345)
185+
securityMembership.SetValue(securityOfficer)
186+
187+
userMembership := MustNewUintMembership(67890)
188+
userMembership.SetValue(regularUser)
189+
190+
// Create the environment (enterprise device)
191+
environment := Environment{
192+
Class: NewClassUUID(TestUUID).
193+
SetVendor("Enterprise Corp").
194+
SetModel("Secure Workstation Pro").
195+
SetLayer(1),
196+
Instance: MustNewUEIDInstance(TestUEID),
197+
}
198+
199+
// Create membership triple
200+
triple := &MembershipTriple{
201+
Environment: environment,
202+
Memberships: *NewMemberships().
203+
Add(adminMembership).
204+
Add(securityMembership).
205+
Add(userMembership),
206+
}
207+
208+
// Add to comid
209+
comid.AddMembershipTriple(triple)
210+
211+
// Validate
212+
err := comid.Valid()
213+
require.NoError(t, err)
214+
215+
// Test serialization
216+
cborData, err := comid.ToCBOR()
217+
require.NoError(t, err)
218+
assert.NotEmpty(t, cborData)
219+
220+
jsonData, err := comid.ToJSON()
221+
require.NoError(t, err)
222+
assert.NotEmpty(t, jsonData)
223+
224+
// Verify content
225+
assert.Contains(t, string(jsonData), "membership-triples")
226+
assert.Contains(t, string(jsonData), "device-admin")
227+
assert.Contains(t, string(jsonData), "security-officer")
228+
assert.Contains(t, string(jsonData), "Enterprise Corp")
229+
230+
fmt.Printf("Enterprise membership scenario: %d bytes CBOR, %d bytes JSON\n",
231+
len(cborData), len(jsonData))
232+
}

0 commit comments

Comments
 (0)