diff --git a/cmd/server/container.go b/cmd/server/container.go index 7b51fd0..d9017f8 100644 --- a/cmd/server/container.go +++ b/cmd/server/container.go @@ -5,6 +5,9 @@ import ( "net/http" "time" + "forge.cadoles.com/Cadoles/daddy/internal/model" + "forge.cadoles.com/Cadoles/daddy/internal/voter" + "github.com/wader/gormstore" "forge.cadoles.com/Cadoles/daddy/internal/auth" @@ -99,5 +102,11 @@ func getServiceContainer(ctx context.Context, conf *config.Config) (*service.Con ctn.Provide(auth.ServiceName, auth.ServiceProvider(conf.Auth.Rules)) + ctn.Provide(voter.ServiceName, voter.ServiceProvider( + voter.StrategyUnanimous, + model.NewDecisionSupportFileVoter(), + model.NewWorkgroupVoter(), + )) + return ctn, nil } diff --git a/internal/config/config.go b/internal/config/config.go index 9c49eae..3745b4a 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -90,7 +90,7 @@ func NewDefault() *Config { Address: ":8081", CookieAuthenticationKey: "", CookieEncryptionKey: "", - CookieMaxAge: int((time.Hour * 1).Seconds()), // 1 hour + CookieMaxAge: int((time.Hour * 24).Seconds()), // 24 hours TemplateDir: "template", PublicDir: "public", FrontendURL: "http://localhost:8080", diff --git a/internal/graph/dsf_handler.go b/internal/graph/dsf_handler.go index c4a1ad3..b0fdc78 100644 --- a/internal/graph/dsf_handler.go +++ b/internal/graph/dsf_handler.go @@ -12,6 +12,15 @@ import ( ) func handleCreateDecisionSupportFile(ctx context.Context, changes *model.DecisionSupportFileChanges) (*model.DecisionSupportFile, error) { + authorized, err := isAuthorized(ctx, &model.DecisionSupportFile{}, model.ActionCreate) + if err != nil { + return nil, errs.WithStack(err) + } + + if !authorized { + return nil, errs.WithStack(ErrForbidden) + } + ctn := container.Must(ctx) db := orm.Must(ctn).DB() @@ -31,7 +40,21 @@ func handleUpdateDecisionSupportFile(ctx context.Context, id string, changes *mo repo := model.NewDSFRepository(db) - dsf, err := repo.Update(ctx, id, changes) + dsf, err := repo.Find(ctx, id) + if err != nil { + return nil, errs.WithStack(err) + } + + authorized, err := isAuthorized(ctx, dsf, model.ActionUpdate) + if err != nil { + return nil, errs.WithStack(err) + } + + if !authorized { + return nil, errs.WithStack(ErrForbidden) + } + + dsf, err = repo.Update(ctx, id, changes) if err != nil { return nil, errs.WithStack(err) } @@ -45,7 +68,25 @@ func handleDecisionSupportFiles(ctx context.Context, filter *model.DecisionSuppo repo := model.NewDSFRepository(db) - return repo.Search(ctx, filter) + found, err := repo.Search(ctx, filter) + if err != nil { + return nil, errs.WithStack(err) + } + + dsfs := make([]*model.DecisionSupportFile, 0) + + for _, d := range found { + authorized, err := isAuthorized(ctx, d, model.ActionRead) + if err != nil { + return nil, errs.WithStack(err) + } + + if authorized { + dsfs = append(dsfs, d) + } + } + + return dsfs, nil } func handleSections(ctx context.Context, dsf *model.DecisionSupportFile) (map[string]interface{}, error) { diff --git a/internal/graph/error.go b/internal/graph/error.go new file mode 100644 index 0000000..0228373 --- /dev/null +++ b/internal/graph/error.go @@ -0,0 +1,7 @@ +package graph + +import "errors" + +var ( + ErrForbidden = errors.New("forbidden") +) diff --git a/internal/graph/helper.go b/internal/graph/helper.go index 3351cc7..1607490 100644 --- a/internal/graph/helper.go +++ b/internal/graph/helper.go @@ -3,6 +3,8 @@ package graph import ( "context" + "forge.cadoles.com/Cadoles/daddy/internal/voter" + "forge.cadoles.com/Cadoles/daddy/internal/model" "forge.cadoles.com/Cadoles/daddy/internal/orm" "forge.cadoles.com/Cadoles/daddy/internal/session" @@ -46,3 +48,31 @@ func getSessionUser(ctx context.Context) (*model.User, *gorm.DB, error) { return user, db, nil } + +func isAuthorized(ctx context.Context, obj interface{}, action interface{}) (bool, error) { + user, _, err := getSessionUser(ctx) + if err != nil { + return false, errors.WithStack(err) + } + + ctn, err := container.From(ctx) + if err != nil { + return false, errors.WithStack(err) + } + + voterSrv, err := voter.From(ctn) + if err != nil { + return false, errors.WithStack(err) + } + + decision, err := voterSrv.Authorized(ctx, user, obj, action) + if err != nil { + return false, errors.WithStack(err) + } + + if decision == voter.Allow { + return true, nil + } + + return false, nil +} diff --git a/internal/graph/workgroup_handler.go b/internal/graph/workgroup_handler.go index 07f89dd..6eab3e4 100644 --- a/internal/graph/workgroup_handler.go +++ b/internal/graph/workgroup_handler.go @@ -2,10 +2,10 @@ package graph import ( "context" - "strconv" "forge.cadoles.com/Cadoles/daddy/internal/model" "github.com/pkg/errors" + errs "github.com/pkg/errors" ) func handleWorkgroups(ctx context.Context, filter *model.WorkgroupsFilter) ([]*model.Workgroup, error) { @@ -24,20 +24,28 @@ func handleWorkgroups(ctx context.Context, filter *model.WorkgroupsFilter) ([]*m } } - workgroups, err := repo.FindWorkgroups(ctx, criteria...) + found, err := repo.FindWorkgroups(ctx, criteria...) if err != nil { return nil, errors.WithStack(err) } + workgroups := make([]*model.Workgroup, 0) + + for _, wg := range found { + authorized, err := isAuthorized(ctx, wg, model.ActionRead) + if err != nil { + return nil, errs.WithStack(err) + } + + if authorized { + workgroups = append(workgroups, wg) + } + } + return workgroups, nil } func handleJoinWorkgroup(ctx context.Context, rawWorkgroupID string) (*model.Workgroup, error) { - workgroupID, err := parseWorkgroupID(rawWorkgroupID) - if err != nil { - return nil, errors.WithStack(err) - } - user, db, err := getSessionUser(ctx) if err != nil { return nil, errors.WithStack(err) @@ -45,7 +53,21 @@ func handleJoinWorkgroup(ctx context.Context, rawWorkgroupID string) (*model.Wor repo := model.NewWorkgroupRepository(db) - workgroup, err := repo.AddUserToWorkgroup(ctx, user.ID, workgroupID) + workgroup, err := repo.Find(ctx, rawWorkgroupID) + if err != nil { + return nil, errors.WithStack(err) + } + + authorized, err := isAuthorized(ctx, workgroup, model.ActionJoin) + if err != nil { + return nil, errs.WithStack(err) + } + + if !authorized { + return nil, errs.WithStack(ErrForbidden) + } + + workgroup, err = repo.AddUserToWorkgroup(ctx, user.ID, workgroup.ID) if err != nil { return nil, errors.WithStack(err) } @@ -53,12 +75,7 @@ func handleJoinWorkgroup(ctx context.Context, rawWorkgroupID string) (*model.Wor return workgroup, nil } -func handleLeaveWorkgroup(ctx context.Context, rawWorkgroupID string) (*model.Workgroup, error) { - workgroupID, err := parseWorkgroupID(rawWorkgroupID) - if err != nil { - return nil, errors.WithStack(err) - } - +func handleLeaveWorkgroup(ctx context.Context, workgroupID string) (*model.Workgroup, error) { user, db, err := getSessionUser(ctx) if err != nil { return nil, errors.WithStack(err) @@ -66,7 +83,21 @@ func handleLeaveWorkgroup(ctx context.Context, rawWorkgroupID string) (*model.Wo repo := model.NewWorkgroupRepository(db) - workgroup, err := repo.RemoveUserFromWorkgroup(ctx, user.ID, workgroupID) + workgroup, err := repo.Find(ctx, workgroupID) + if err != nil { + return nil, errors.WithStack(err) + } + + authorized, err := isAuthorized(ctx, workgroup, model.ActionLeave) + if err != nil { + return nil, errs.WithStack(err) + } + + if !authorized { + return nil, errs.WithStack(ErrForbidden) + } + + workgroup, err = repo.RemoveUserFromWorkgroup(ctx, user.ID, workgroup.ID) if err != nil { return nil, errors.WithStack(err) } @@ -75,6 +106,15 @@ func handleLeaveWorkgroup(ctx context.Context, rawWorkgroupID string) (*model.Wo } func handleCreateWorkgroup(ctx context.Context, changes model.WorkgroupChanges) (*model.Workgroup, error) { + authorized, err := isAuthorized(ctx, &model.Workgroup{}, model.ActionCreate) + if err != nil { + return nil, errs.WithStack(err) + } + + if !authorized { + return nil, errs.WithStack(ErrForbidden) + } + db, err := getDB(ctx) if err != nil { return nil, errors.WithStack(err) @@ -90,12 +130,7 @@ func handleCreateWorkgroup(ctx context.Context, changes model.WorkgroupChanges) return workgroup, nil } -func handleCloseWorkgroup(ctx context.Context, rawWorkgroupID string) (*model.Workgroup, error) { - workgroupID, err := parseWorkgroupID(rawWorkgroupID) - if err != nil { - return nil, errors.WithStack(err) - } - +func handleCloseWorkgroup(ctx context.Context, workgroupID string) (*model.Workgroup, error) { db, err := getDB(ctx) if err != nil { return nil, errors.WithStack(err) @@ -103,7 +138,21 @@ func handleCloseWorkgroup(ctx context.Context, rawWorkgroupID string) (*model.Wo repo := model.NewWorkgroupRepository(db) - workgroup, err := repo.CloseWorkgroup(ctx, workgroupID) + workgroup, err := repo.Find(ctx, workgroupID) + if err != nil { + return nil, errors.WithStack(err) + } + + authorized, err := isAuthorized(ctx, workgroup, model.ActionClose) + if err != nil { + return nil, errs.WithStack(err) + } + + if !authorized { + return nil, errs.WithStack(ErrForbidden) + } + + workgroup, err = repo.CloseWorkgroup(ctx, workgroup.ID) if err != nil { return nil, errors.WithStack(err) } @@ -111,12 +160,7 @@ func handleCloseWorkgroup(ctx context.Context, rawWorkgroupID string) (*model.Wo return workgroup, nil } -func handleUpdateWorkgroup(ctx context.Context, rawWorkgroupID string, changes model.WorkgroupChanges) (*model.Workgroup, error) { - workgroupID, err := parseWorkgroupID(rawWorkgroupID) - if err != nil { - return nil, errors.WithStack(err) - } - +func handleUpdateWorkgroup(ctx context.Context, workgroupID string, changes model.WorkgroupChanges) (*model.Workgroup, error) { db, err := getDB(ctx) if err != nil { return nil, errors.WithStack(err) @@ -124,19 +168,24 @@ func handleUpdateWorkgroup(ctx context.Context, rawWorkgroupID string, changes m repo := model.NewWorkgroupRepository(db) - workgroup, err := repo.UpdateWorkgroup(ctx, workgroupID, changes) + workgroup, err := repo.Find(ctx, workgroupID) + if err != nil { + return nil, errors.WithStack(err) + } + + authorized, err := isAuthorized(ctx, workgroup, model.ActionUpdate) + if err != nil { + return nil, errs.WithStack(err) + } + + if !authorized { + return nil, errs.WithStack(ErrForbidden) + } + + workgroup, err = repo.UpdateWorkgroup(ctx, workgroup.ID, changes) if err != nil { return nil, errors.WithStack(err) } return workgroup, nil } - -func parseWorkgroupID(workgroupID string) (uint, error) { - workgroupID64, err := strconv.ParseUint(workgroupID, 10, 32) - if err != nil { - return 0, errors.WithStack(err) - } - - return uint(workgroupID64), nil -} diff --git a/internal/model/action.go b/internal/model/action.go new file mode 100644 index 0000000..737096b --- /dev/null +++ b/internal/model/action.go @@ -0,0 +1,13 @@ +package model + +type Action string + +const ( + ActionCreate Action = "create" + ActionRead Action = "read" + ActionUpdate Action = "update" + ActionDelete Action = "delete" + ActionJoin Action = "join" + ActionLeave Action = "leave" + ActionClose Action = "close" +) diff --git a/internal/model/dsf_repository.go b/internal/model/dsf_repository.go index bfc3be0..1292458 100644 --- a/internal/model/dsf_repository.go +++ b/internal/model/dsf_repository.go @@ -88,6 +88,17 @@ func (r *DSFRepository) updateFromChanges(dsf *DecisionSupportFile, changes *Dec return nil } +func (r *DSFRepository) Find(ctx context.Context, id string) (*DecisionSupportFile, error) { + dsf := &DecisionSupportFile{} + query := r.db.Model(dsf).Preload("Workgroup").Where("id = ?", id) + + if err := query.First(&dsf).Error; err != nil { + return nil, errs.WithStack(err) + } + + return dsf, nil +} + func (r *DSFRepository) Search(ctx context.Context, filter *DecisionSupportFileFilter) ([]*DecisionSupportFile, error) { query := r.db.Model(&DecisionSupportFile{}).Preload("Workgroup") diff --git a/internal/model/dsf_voter.go b/internal/model/dsf_voter.go new file mode 100644 index 0000000..baa4c7d --- /dev/null +++ b/internal/model/dsf_voter.go @@ -0,0 +1,48 @@ +package model + +import ( + "context" + + "forge.cadoles.com/Cadoles/daddy/internal/voter" +) + +type DecisionSupportFileVoter struct { +} + +func (v *DecisionSupportFileVoter) Vote(ctx context.Context, subject interface{}, obj interface{}, act interface{}) (voter.Decision, error) { + user, ok := subject.(*User) + if !ok { + return voter.Abstain, nil + } + + dsf, ok := obj.(*DecisionSupportFile) + if !ok { + return voter.Abstain, nil + } + + action, ok := act.(Action) + if !ok { + return voter.Abstain, nil + } + + switch action { + case ActionCreate: + return voter.Allow, nil + case ActionRead: + return voter.Allow, nil + case ActionUpdate: + if inWorkgroup(user, dsf.Workgroup) { + return voter.Allow, nil + } + + return voter.Deny, nil + case ActionDelete: + return voter.Deny, nil + } + + return voter.Abstain, nil +} + +func NewDecisionSupportFileVoter() *DecisionSupportFileVoter { + return &DecisionSupportFileVoter{} +} diff --git a/internal/model/helper.go b/internal/model/helper.go new file mode 100644 index 0000000..9f56dd9 --- /dev/null +++ b/internal/model/helper.go @@ -0,0 +1,15 @@ +package model + +func inWorkgroup(user *User, workgroup *Workgroup) bool { + if workgroup == nil { + return false + } + + for _, w := range user.Workgroups { + if w.ID == workgroup.ID { + return true + } + } + + return false +} diff --git a/internal/model/workgroup_repository.go b/internal/model/workgroup_repository.go index 283c1fe..3c73ed1 100644 --- a/internal/model/workgroup_repository.go +++ b/internal/model/workgroup_repository.go @@ -6,6 +6,7 @@ import ( "github.com/jinzhu/gorm" "github.com/pkg/errors" + errs "github.com/pkg/errors" ) type WorkgroupRepository struct { @@ -135,6 +136,17 @@ func (r *WorkgroupRepository) RemoveUserFromWorkgroup(ctx context.Context, userI return workgroup, nil } +func (r *WorkgroupRepository) Find(ctx context.Context, id string) (*Workgroup, error) { + wg := &Workgroup{} + query := r.db.Model(wg).Where("id = ?", id) + + if err := query.First(&wg).Error; err != nil { + return nil, errs.WithStack(err) + } + + return wg, nil +} + func NewWorkgroupRepository(db *gorm.DB) *WorkgroupRepository { return &WorkgroupRepository{db} } diff --git a/internal/model/workgroup_voter.go b/internal/model/workgroup_voter.go new file mode 100644 index 0000000..d9e8fc0 --- /dev/null +++ b/internal/model/workgroup_voter.go @@ -0,0 +1,54 @@ +package model + +import ( + "context" + + "forge.cadoles.com/Cadoles/daddy/internal/voter" +) + +type WorkgroupVoter struct { +} + +func (v *WorkgroupVoter) Vote(ctx context.Context, subject interface{}, obj interface{}, act interface{}) (voter.Decision, error) { + user, ok := subject.(*User) + if !ok { + return voter.Abstain, nil + } + + workgroup, ok := obj.(*Workgroup) + if !ok { + return voter.Abstain, nil + } + + action, ok := act.(Action) + if !ok { + return voter.Abstain, nil + } + + switch action { + case ActionCreate: + return voter.Allow, nil + case ActionRead: + return voter.Allow, nil + case ActionJoin: + return voter.Allow, nil + case ActionLeave: + fallthrough + case ActionUpdate: + fallthrough + case ActionClose: + if inWorkgroup(user, workgroup) { + return voter.Allow, nil + } else { + return voter.Deny, nil + } + case ActionDelete: + return voter.Deny, nil + } + + return voter.Abstain, nil +} + +func NewWorkgroupVoter() *WorkgroupVoter { + return &WorkgroupVoter{} +} diff --git a/internal/route/mount.go b/internal/route/mount.go index aa8109a..b295101 100644 --- a/internal/route/mount.go +++ b/internal/route/mount.go @@ -36,10 +36,12 @@ func Mount(r *chi.Mux, config *config.Config) error { }).Handler) r.Use(session.UserEmailMiddleware) + gqlConfig := generated.Config{ + Resolvers: &graph.Resolver{}, + } + gql := handler.New( - generated.NewExecutableSchema(generated.Config{ - Resolvers: &graph.Resolver{}, - }), + generated.NewExecutableSchema(gqlConfig), ) gql.AddTransport(transport.POST{}) diff --git a/internal/voter/decision.go b/internal/voter/decision.go new file mode 100644 index 0000000..bbf299e --- /dev/null +++ b/internal/voter/decision.go @@ -0,0 +1,22 @@ +package voter + +type Decision int + +const ( + Allow Decision = iota + Deny + Abstain +) + +func AsString(d Decision) string { + switch d { + case Allow: + return "allow" + case Deny: + return "deny" + case Abstain: + return "abstain" + default: + return "unknown" + } +} diff --git a/internal/voter/manager.go b/internal/voter/manager.go new file mode 100644 index 0000000..0e99ca9 --- /dev/null +++ b/internal/voter/manager.go @@ -0,0 +1,64 @@ +package voter + +import ( + "context" + + "github.com/davecgh/go-spew/spew" + + "github.com/pkg/errors" + "gitlab.com/wpetit/goweb/logger" +) + +type Voter interface { + Vote(ctx context.Context, subject interface{}, obj interface{}, action interface{}) (Decision, error) +} + +type Strategy func(ctx context.Context, decisions []Decision) (Decision, error) + +type Manager struct { + strategy Strategy + voters []Voter +} + +func (m *Manager) Authorized(ctx context.Context, subject interface{}, obj interface{}, action interface{}) (Decision, error) { + decisions := make([]Decision, 0, len(m.voters)) + + logger.Debug( + ctx, + "checking authorization", + logger.F("subject", subject), + logger.F("object", obj), + logger.F("action", action), + ) + + for _, v := range m.voters { + dec, err := v.Vote(ctx, subject, obj, action) + if err != nil { + return Deny, errors.WithStack(err) + } + + decisions = append(decisions, dec) + } + + spew.Dump(decisions) + + result, err := m.strategy(ctx, decisions) + if err != nil { + return Deny, errors.WithStack(err) + } + + logger.Debug( + ctx, + "authorization checked", + logger.F("subject", subject), + logger.F("object", obj), + logger.F("action", action), + logger.F("result", AsString(result)), + ) + + return result, nil +} + +func NewManager(strategy Strategy, voters ...Voter) *Manager { + return &Manager{strategy, voters} +} diff --git a/internal/voter/provider.go b/internal/voter/provider.go new file mode 100644 index 0000000..e20f68e --- /dev/null +++ b/internal/voter/provider.go @@ -0,0 +1,13 @@ +package voter + +import ( + "gitlab.com/wpetit/goweb/service" +) + +func ServiceProvider(strategy Strategy, voters ...Voter) service.Provider { + manager := NewManager(strategy, voters...) + + return func(ctn *service.Container) (interface{}, error) { + return manager, nil + } +} diff --git a/internal/voter/service.go b/internal/voter/service.go new file mode 100644 index 0000000..9f1d355 --- /dev/null +++ b/internal/voter/service.go @@ -0,0 +1,33 @@ +package voter + +import ( + "github.com/pkg/errors" + "gitlab.com/wpetit/goweb/service" +) + +const ServiceName service.Name = "voter" + +// From retrieves the voter service in the given container. +func From(container *service.Container) (*Manager, error) { + service, err := container.Service(ServiceName) + if err != nil { + return nil, errors.Wrapf(err, "error while retrieving '%s' service", ServiceName) + } + + srv, ok := service.(*Manager) + if !ok { + return nil, errors.Errorf("retrieved service is not a valid '%s' service", ServiceName) + } + + return srv, nil +} + +// Must retrieves the voter service in the given container or panic otherwise. +func Must(container *service.Container) *Manager { + srv, err := From(container) + if err != nil { + panic(err) + } + + return srv +} diff --git a/internal/voter/strategy.go b/internal/voter/strategy.go new file mode 100644 index 0000000..34d9668 --- /dev/null +++ b/internal/voter/strategy.go @@ -0,0 +1,77 @@ +package voter + +import "context" + +// StrategyUnanimous returns Allow if all voters allow the operations. +func StrategyUnanimous(ctx context.Context, decisions []Decision) (Decision, error) { + allAbstains := true + + for _, d := range decisions { + if d == Deny { + return Deny, nil + } + + if d != Abstain { + allAbstains = false + } + } + + if allAbstains { + return Abstain, nil + } + + return Allow, nil +} + +// StrategyAffirmative returns Allow if at least one voter allow the operation. +func StrategyAffirmative(ctx context.Context, decisions []Decision) (Decision, error) { + allAbstains := true + + for _, d := range decisions { + if d == Allow { + return Allow, nil + } + + if allAbstains && d != Abstain { + allAbstains = false + } + } + + if allAbstains { + return Abstain, nil + } + + return Deny, nil +} + +// StrategyConsensus returns Allow if the majority of voters allow the operation. +func StrategyConsensus(ctx context.Context, decisions []Decision) (Decision, error) { + deny := 0 + allow := 0 + abstain := 0 + + for _, d := range decisions { + switch { + case d == Allow: + allow++ + case d == Deny: + deny++ + case d == Abstain: + abstain++ + } + } + + if abstain > allow && abstain > deny { + return Abstain, nil + } + + if allow > abstain && allow > deny { + return Allow, nil + } + + if deny > allow && deny > abstain { + return Deny, nil + } + + return Abstain, nil +} diff --git a/internal/voter/strategy_test.go b/internal/voter/strategy_test.go new file mode 100644 index 0000000..f01de52 --- /dev/null +++ b/internal/voter/strategy_test.go @@ -0,0 +1,125 @@ +package voter + +import ( + "context" + "testing" +) + +func TestStrategyUnanimous(t *testing.T) { + testCases := []struct { + Decisions []Decision + Expect Decision + }{ + { + Decisions: []Decision{Allow, Allow, Allow}, + Expect: Allow, + }, + { + Decisions: []Decision{Abstain, Abstain, Abstain}, + Expect: Abstain, + }, + { + Decisions: []Decision{Deny, Abstain, Abstain}, + Expect: Deny, + }, + { + Decisions: []Decision{Deny, Allow, Abstain}, + Expect: Deny, + }, + } + + for _, tc := range testCases { + ctx := context.Background() + + result, err := StrategyUnanimous(ctx, tc.Decisions) + if err != nil { + t.Error(err) + } + + if e, g := tc.Expect, result; e != g { + t.Errorf("result: expected '%v', got '%v'", AsString(e), AsString(g)) + } + } +} + +func TestStrategyAffirmative(t *testing.T) { + testCases := []struct { + Decisions []Decision + Expect Decision + }{ + { + Decisions: []Decision{Allow, Allow, Allow}, + Expect: Allow, + }, + { + Decisions: []Decision{Abstain, Abstain, Abstain}, + Expect: Abstain, + }, + { + Decisions: []Decision{Deny, Abstain, Abstain}, + Expect: Deny, + }, + { + Decisions: []Decision{Deny, Allow, Abstain}, + Expect: Allow, + }, + } + + for _, tc := range testCases { + ctx := context.Background() + + result, err := StrategyAffirmative(ctx, tc.Decisions) + if err != nil { + t.Error(err) + } + + if e, g := tc.Expect, result; e != g { + t.Errorf("result: expected '%v', got '%v'", AsString(e), AsString(g)) + } + } +} + +func TestStrategyConsensus(t *testing.T) { + testCases := []struct { + Decisions []Decision + Expect Decision + }{ + { + Decisions: []Decision{Allow, Allow, Allow}, + Expect: Allow, + }, + { + Decisions: []Decision{Abstain, Abstain, Abstain}, + Expect: Abstain, + }, + { + Decisions: []Decision{Deny, Allow, Abstain}, + Expect: Abstain, + }, + { + Decisions: []Decision{Deny, Deny, Allow}, + Expect: Deny, + }, + { + Decisions: []Decision{Deny, Deny, Allow, Allow}, + Expect: Abstain, + }, + { + Decisions: []Decision{Deny, Deny, Allow, Allow, Allow}, + Expect: Allow, + }, + } + + for _, tc := range testCases { + ctx := context.Background() + + result, err := StrategyConsensus(ctx, tc.Decisions) + if err != nil { + t.Error(err) + } + + if e, g := tc.Expect, result; e != g { + t.Errorf("result: expected '%v', got '%v'", AsString(e), AsString(g)) + } + } +}