Mise en place d'un système de vérification des autorisations côté
serveur - Création d'un service d'autorisation dynamique basé sur des "voter" (à la Symfony) - Mise en place des autorisations sur les principales queries/mutations de l'API GraphQL
This commit is contained in:
22
internal/voter/decision.go
Normal file
22
internal/voter/decision.go
Normal file
@ -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"
|
||||
}
|
||||
}
|
64
internal/voter/manager.go
Normal file
64
internal/voter/manager.go
Normal file
@ -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}
|
||||
}
|
13
internal/voter/provider.go
Normal file
13
internal/voter/provider.go
Normal file
@ -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
|
||||
}
|
||||
}
|
33
internal/voter/service.go
Normal file
33
internal/voter/service.go
Normal file
@ -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
|
||||
}
|
77
internal/voter/strategy.go
Normal file
77
internal/voter/strategy.go
Normal file
@ -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
|
||||
}
|
125
internal/voter/strategy_test.go
Normal file
125
internal/voter/strategy_test.go
Normal file
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user