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:
parent
bc56c9dbae
commit
3ef495445a
@ -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
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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) {
|
||||
|
7
internal/graph/error.go
Normal file
7
internal/graph/error.go
Normal file
@ -0,0 +1,7 @@
|
||||
package graph
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrForbidden = errors.New("forbidden")
|
||||
)
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
13
internal/model/action.go
Normal file
13
internal/model/action.go
Normal file
@ -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"
|
||||
)
|
@ -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")
|
||||
|
||||
|
48
internal/model/dsf_voter.go
Normal file
48
internal/model/dsf_voter.go
Normal file
@ -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{}
|
||||
}
|
15
internal/model/helper.go
Normal file
15
internal/model/helper.go
Normal file
@ -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
|
||||
}
|
@ -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}
|
||||
}
|
||||
|
54
internal/model/workgroup_voter.go
Normal file
54
internal/model/workgroup_voter.go
Normal file
@ -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{}
|
||||
}
|
@ -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{})
|
||||
|
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))
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user