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:
2020-09-04 10:10:32 +02:00
parent bc56c9dbae
commit 3ef495445a
19 changed files with 669 additions and 44 deletions

View 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
View 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}
}

View 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
View 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
}

View 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
}

View 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))
}
}
}