Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion handler_committee.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import (
"errors"
"fmt"

"github.com/linuxfoundation/lfx-v2-fga-sync/internal/domain"
"github.com/linuxfoundation/lfx-v2-fga-sync/internal/service"
"github.com/linuxfoundation/lfx-v2-fga-sync/pkg/constants"
"github.com/openfga/go-sdk/client" // Only for client types, not the full SDK
)
Expand All @@ -20,6 +22,7 @@ type committeeStub struct {
Public bool `json:"public"`
Relations map[string][]string `json:"relations"`
References map[string]string `json:"references"`
Policies []domain.Policy `json:"policies"`
}

// committeeUpdateAccessHandler handles committee access control updates.
Expand Down Expand Up @@ -72,12 +75,28 @@ func (h *HandlerService) committeeUpdateAccessHandler(message INatsMsg) error {
}
}

tuplesWrites, tuplesDeletes, err := h.fgaService.SyncObjectTuples(ctx, object, tuples)
// Sync committee tuples
tuplesWrites, tuplesDeletes, err := h.fgaService.SyncObjectTuples(ctx, object, tuples, "member")
if err != nil {
logger.With(errKey, err, "tuples", tuples, "object", object).ErrorContext(ctx, "failed to sync tuples")
return err
}

if len(committee.Policies) > 0 {
policyEval := service.NewPolicyHandler(logger, h.fgaService)

// Evaluate each policy associated with the committee
for _, policy := range committee.Policies {

err = policyEval.EvaluatePolicy(ctx, policy, object)
if err != nil {
logger.With(errKey, err, "policy", policy, "object", object).ErrorContext(ctx, "failed to evaluate policy")
return err
}
}

}

logger.With(
"tuples", tuples,
"object", object,
Expand Down
42 changes: 42 additions & 0 deletions internal/domain/policy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Copyright The Linux Foundation and each contributor to LFX.
// SPDX-License-Identifier: MIT

package domain

import "fmt"

// Policy represents a fine-grained authorization policy.
type Policy struct {
Name string `json:"name"`
Relation string `json:"relation"`
Value string `json:"value"`
}

// Validate checks if the policy has all required fields.
// Returns an error if any field is empty.
func (p Policy) Validate() error {
if p.Name == "" {
return fmt.Errorf("policy name cannot be empty")
}
if p.Value == "" {
return fmt.Errorf("policy value cannot be empty")
}
if p.Relation == "" {
return fmt.Errorf("policy relation cannot be empty")
}
return nil
}

// ObjectID returns the OpenFGA object identifier for this policy.
// Format: policy.Name:policy.Value
// Example: "visibility_policy:basic_profile"
func (p Policy) ObjectID() string {
return fmt.Sprintf("%s:%s", p.Name, p.Value)
}

// UserRelation returns the user relation string for a given object.
// Format: objectID#relation
// Example: "committee:123#member"
func (p Policy) UserRelation(objectID, relation string) string {
return fmt.Sprintf("%s#%s", objectID, relation)
}
212 changes: 212 additions & 0 deletions internal/domain/policy_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
// Copyright The Linux Foundation and each contributor to LFX.
// SPDX-License-Identifier: MIT

package domain

import (
"testing"
)

func TestPolicy_Validate(t *testing.T) {
tests := []struct {
name string
policy Policy
wantErr bool
errMsg string
}{
{
name: "valid policy",
policy: Policy{
Name: "visibility_policy",
Value: "basic_profile",
Relation: "allows_basic_profile",
},
wantErr: false,
},
{
name: "empty name",
policy: Policy{
Name: "",
Value: "basic_profile",
Relation: "allows_basic_profile",
},
wantErr: true,
errMsg: "policy name cannot be empty",
},
{
name: "empty value",
policy: Policy{
Name: "visibility_policy",
Value: "",
Relation: "allows_basic_profile",
},
wantErr: true,
errMsg: "policy value cannot be empty",
},
{
name: "empty relation",
policy: Policy{
Name: "visibility_policy",
Value: "basic_profile",
Relation: "",
},
wantErr: true,
errMsg: "policy relation cannot be empty",
},
{
name: "all fields empty",
policy: Policy{
Name: "",
Value: "",
Relation: "",
},
wantErr: true,
errMsg: "policy name cannot be empty",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.policy.Validate()
if (err != nil) != tt.wantErr {
t.Errorf("Policy.Validate() error = %v, wantErr %v", err, tt.wantErr)
return
}
if err != nil && err.Error() != tt.errMsg {
t.Errorf("Policy.Validate() error message = %v, want %v", err.Error(), tt.errMsg)
}
})
}
}

func TestPolicy_ObjectID(t *testing.T) {
tests := []struct {
name string
policy Policy
want string
}{
{
name: "visibility policy",
policy: Policy{
Name: "visibility_policy",
Value: "basic_profile",
},
want: "visibility_policy:basic_profile",
},
{
name: "access policy",
policy: Policy{
Name: "access_policy",
Value: "admin",
},
want: "access_policy:admin",
},
{
name: "empty values",
policy: Policy{
Name: "",
Value: "",
},
want: ":",
},
{
name: "special characters",
policy: Policy{
Name: "policy-name",
Value: "value_123",
},
want: "policy-name:value_123",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.policy.ObjectID(); got != tt.want {
t.Errorf("Policy.ObjectID() = %v, want %v", got, tt.want)
}
})
}
}

func TestPolicy_UserRelation(t *testing.T) {
tests := []struct {
name string
policy Policy
objectID string
relation string
want string
}{
{
name: "committee member relation",
policy: Policy{},
objectID: "committee:123",
relation: "member",
want: "committee:123#member",
},
{
name: "project viewer relation",
policy: Policy{},
objectID: "project:abc-def-ghi",
relation: "viewer",
want: "project:abc-def-ghi#viewer",
},
{
name: "team owner relation",
policy: Policy{},
objectID: "team:xyz",
relation: "owner",
want: "team:xyz#owner",
},
{
name: "empty objectID",
policy: Policy{},
objectID: "",
relation: "member",
want: "#member",
},
{
name: "empty relation",
policy: Policy{},
objectID: "committee:123",
relation: "",
want: "committee:123#",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.policy.UserRelation(tt.objectID, tt.relation); got != tt.want {
t.Errorf("Policy.UserRelation() = %v, want %v", got, tt.want)
}
})
}
}

func TestPolicy_Integration(t *testing.T) {
// Test that all methods work together correctly
policy := Policy{
Name: "visibility_policy",
Value: "basic_profile",
Relation: "allows_basic_profile",
}

// Validate the policy
if err := policy.Validate(); err != nil {
t.Errorf("Policy.Validate() failed for valid policy: %v", err)
}

// Get object ID
objectID := policy.ObjectID()
expectedObjectID := "visibility_policy:basic_profile"
if objectID != expectedObjectID {
t.Errorf("Policy.ObjectID() = %v, want %v", objectID, expectedObjectID)
}

// Get user relation
committeeID := "committee:f01dec3e-2611-482e-bffc-b4a6d9cd0afd"
userRelation := policy.UserRelation(committeeID, "member")
expectedUserRelation := "committee:f01dec3e-2611-482e-bffc-b4a6d9cd0afd#member"
if userRelation != expectedUserRelation {
t.Errorf("Policy.UserRelation() = %v, want %v", userRelation, expectedUserRelation)
}
}
Loading
Loading