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"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"forge.cadoles.com/Cadoles/daddy/internal/model"
|
||||||
|
"forge.cadoles.com/Cadoles/daddy/internal/voter"
|
||||||
|
|
||||||
"github.com/wader/gormstore"
|
"github.com/wader/gormstore"
|
||||||
|
|
||||||
"forge.cadoles.com/Cadoles/daddy/internal/auth"
|
"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(auth.ServiceName, auth.ServiceProvider(conf.Auth.Rules))
|
||||||
|
|
||||||
|
ctn.Provide(voter.ServiceName, voter.ServiceProvider(
|
||||||
|
voter.StrategyUnanimous,
|
||||||
|
model.NewDecisionSupportFileVoter(),
|
||||||
|
model.NewWorkgroupVoter(),
|
||||||
|
))
|
||||||
|
|
||||||
return ctn, nil
|
return ctn, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -90,7 +90,7 @@ func NewDefault() *Config {
|
||||||
Address: ":8081",
|
Address: ":8081",
|
||||||
CookieAuthenticationKey: "",
|
CookieAuthenticationKey: "",
|
||||||
CookieEncryptionKey: "",
|
CookieEncryptionKey: "",
|
||||||
CookieMaxAge: int((time.Hour * 1).Seconds()), // 1 hour
|
CookieMaxAge: int((time.Hour * 24).Seconds()), // 24 hours
|
||||||
TemplateDir: "template",
|
TemplateDir: "template",
|
||||||
PublicDir: "public",
|
PublicDir: "public",
|
||||||
FrontendURL: "http://localhost:8080",
|
FrontendURL: "http://localhost:8080",
|
||||||
|
|
|
@ -12,6 +12,15 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func handleCreateDecisionSupportFile(ctx context.Context, changes *model.DecisionSupportFileChanges) (*model.DecisionSupportFile, error) {
|
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)
|
ctn := container.Must(ctx)
|
||||||
db := orm.Must(ctn).DB()
|
db := orm.Must(ctn).DB()
|
||||||
|
|
||||||
|
@ -31,7 +40,21 @@ func handleUpdateDecisionSupportFile(ctx context.Context, id string, changes *mo
|
||||||
|
|
||||||
repo := model.NewDSFRepository(db)
|
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 {
|
if err != nil {
|
||||||
return nil, errs.WithStack(err)
|
return nil, errs.WithStack(err)
|
||||||
}
|
}
|
||||||
|
@ -45,7 +68,25 @@ func handleDecisionSupportFiles(ctx context.Context, filter *model.DecisionSuppo
|
||||||
|
|
||||||
repo := model.NewDSFRepository(db)
|
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) {
|
func handleSections(ctx context.Context, dsf *model.DecisionSupportFile) (map[string]interface{}, error) {
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
package graph
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrForbidden = errors.New("forbidden")
|
||||||
|
)
|
|
@ -3,6 +3,8 @@ package graph
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
|
"forge.cadoles.com/Cadoles/daddy/internal/voter"
|
||||||
|
|
||||||
"forge.cadoles.com/Cadoles/daddy/internal/model"
|
"forge.cadoles.com/Cadoles/daddy/internal/model"
|
||||||
"forge.cadoles.com/Cadoles/daddy/internal/orm"
|
"forge.cadoles.com/Cadoles/daddy/internal/orm"
|
||||||
"forge.cadoles.com/Cadoles/daddy/internal/session"
|
"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
|
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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"forge.cadoles.com/Cadoles/daddy/internal/model"
|
"forge.cadoles.com/Cadoles/daddy/internal/model"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
errs "github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
func handleWorkgroups(ctx context.Context, filter *model.WorkgroupsFilter) ([]*model.Workgroup, error) {
|
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 {
|
if err != nil {
|
||||||
return nil, errors.WithStack(err)
|
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
|
return workgroups, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleJoinWorkgroup(ctx context.Context, rawWorkgroupID string) (*model.Workgroup, error) {
|
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)
|
user, db, err := getSessionUser(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.WithStack(err)
|
return nil, errors.WithStack(err)
|
||||||
|
@ -45,7 +53,21 @@ func handleJoinWorkgroup(ctx context.Context, rawWorkgroupID string) (*model.Wor
|
||||||
|
|
||||||
repo := model.NewWorkgroupRepository(db)
|
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 {
|
if err != nil {
|
||||||
return nil, errors.WithStack(err)
|
return nil, errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
@ -53,12 +75,7 @@ func handleJoinWorkgroup(ctx context.Context, rawWorkgroupID string) (*model.Wor
|
||||||
return workgroup, nil
|
return workgroup, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleLeaveWorkgroup(ctx context.Context, rawWorkgroupID string) (*model.Workgroup, error) {
|
func handleLeaveWorkgroup(ctx context.Context, workgroupID string) (*model.Workgroup, error) {
|
||||||
workgroupID, err := parseWorkgroupID(rawWorkgroupID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
user, db, err := getSessionUser(ctx)
|
user, db, err := getSessionUser(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.WithStack(err)
|
return nil, errors.WithStack(err)
|
||||||
|
@ -66,7 +83,21 @@ func handleLeaveWorkgroup(ctx context.Context, rawWorkgroupID string) (*model.Wo
|
||||||
|
|
||||||
repo := model.NewWorkgroupRepository(db)
|
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 {
|
if err != nil {
|
||||||
return nil, errors.WithStack(err)
|
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) {
|
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)
|
db, err := getDB(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.WithStack(err)
|
return nil, errors.WithStack(err)
|
||||||
|
@ -90,12 +130,7 @@ func handleCreateWorkgroup(ctx context.Context, changes model.WorkgroupChanges)
|
||||||
return workgroup, nil
|
return workgroup, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleCloseWorkgroup(ctx context.Context, rawWorkgroupID string) (*model.Workgroup, error) {
|
func handleCloseWorkgroup(ctx context.Context, workgroupID string) (*model.Workgroup, error) {
|
||||||
workgroupID, err := parseWorkgroupID(rawWorkgroupID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
db, err := getDB(ctx)
|
db, err := getDB(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.WithStack(err)
|
return nil, errors.WithStack(err)
|
||||||
|
@ -103,7 +138,21 @@ func handleCloseWorkgroup(ctx context.Context, rawWorkgroupID string) (*model.Wo
|
||||||
|
|
||||||
repo := model.NewWorkgroupRepository(db)
|
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 {
|
if err != nil {
|
||||||
return nil, errors.WithStack(err)
|
return nil, errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
@ -111,12 +160,7 @@ func handleCloseWorkgroup(ctx context.Context, rawWorkgroupID string) (*model.Wo
|
||||||
return workgroup, nil
|
return workgroup, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleUpdateWorkgroup(ctx context.Context, rawWorkgroupID string, changes model.WorkgroupChanges) (*model.Workgroup, error) {
|
func handleUpdateWorkgroup(ctx context.Context, workgroupID string, changes model.WorkgroupChanges) (*model.Workgroup, error) {
|
||||||
workgroupID, err := parseWorkgroupID(rawWorkgroupID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
db, err := getDB(ctx)
|
db, err := getDB(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.WithStack(err)
|
return nil, errors.WithStack(err)
|
||||||
|
@ -124,19 +168,24 @@ func handleUpdateWorkgroup(ctx context.Context, rawWorkgroupID string, changes m
|
||||||
|
|
||||||
repo := model.NewWorkgroupRepository(db)
|
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 {
|
if err != nil {
|
||||||
return nil, errors.WithStack(err)
|
return nil, errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return workgroup, nil
|
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
|
|
||||||
}
|
|
||||||
|
|
|
@ -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
|
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) {
|
func (r *DSFRepository) Search(ctx context.Context, filter *DecisionSupportFileFilter) ([]*DecisionSupportFile, error) {
|
||||||
query := r.db.Model(&DecisionSupportFile{}).Preload("Workgroup")
|
query := r.db.Model(&DecisionSupportFile{}).Preload("Workgroup")
|
||||||
|
|
||||||
|
|
|
@ -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{}
|
||||||
|
}
|
|
@ -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/jinzhu/gorm"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
errs "github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
type WorkgroupRepository struct {
|
type WorkgroupRepository struct {
|
||||||
|
@ -135,6 +136,17 @@ func (r *WorkgroupRepository) RemoveUserFromWorkgroup(ctx context.Context, userI
|
||||||
return workgroup, nil
|
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 {
|
func NewWorkgroupRepository(db *gorm.DB) *WorkgroupRepository {
|
||||||
return &WorkgroupRepository{db}
|
return &WorkgroupRepository{db}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
}).Handler)
|
||||||
r.Use(session.UserEmailMiddleware)
|
r.Use(session.UserEmailMiddleware)
|
||||||
|
|
||||||
|
gqlConfig := generated.Config{
|
||||||
|
Resolvers: &graph.Resolver{},
|
||||||
|
}
|
||||||
|
|
||||||
gql := handler.New(
|
gql := handler.New(
|
||||||
generated.NewExecutableSchema(generated.Config{
|
generated.NewExecutableSchema(gqlConfig),
|
||||||
Resolvers: &graph.Resolver{},
|
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
gql.AddTransport(transport.POST{})
|
gql.AddTransport(transport.POST{})
|
||||||
|
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
|
@ -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}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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