feat(auth): add user access filtering rules

This commit is contained in:
2020-08-09 11:59:03 +02:00
parent a30d6b76d3
commit f55bce9ee2
8 changed files with 174 additions and 1 deletions

71
internal/auth/auth.go Normal file
View File

@ -0,0 +1,71 @@
package auth
import (
"sync"
"forge.cadoles.com/Cadoles/guesstimate/internal/model"
"github.com/antonmedv/expr"
"github.com/antonmedv/expr/vm"
"github.com/pkg/errors"
)
var (
ErrUnexpectedRuleResult = errors.New("unexpected rule result")
)
type Service struct {
rules []*vm.Program
mutex sync.RWMutex
}
func (s *Service) LoadRules(rawRules ...string) error {
rules := make([]*vm.Program, 0, len(rawRules))
for _, rr := range rawRules {
r, err := expr.Compile(rr)
if err != nil {
return errors.WithStack(err)
}
rules = append(rules, r)
}
s.mutex.Lock()
s.rules = rules
s.mutex.Unlock()
return nil
}
func (s *Service) Authorize(user *model.User) (bool, error) {
s.mutex.RLock()
defer s.mutex.RUnlock()
env := map[string]interface{}{
"user": user,
}
for _, r := range s.rules {
result, err := expr.Run(r, env)
if err != nil {
return false, errors.WithStack(err)
}
authorized, ok := result.(bool)
if !ok {
return false, errors.WithStack(ErrUnexpectedRuleResult)
}
if !authorized {
return false, nil
}
}
return true, nil
}
func NewService() *Service {
return &Service{
rules: make([]*vm.Program, 0),
}
}

20
internal/auth/provider.go Normal file
View File

@ -0,0 +1,20 @@
package auth
import (
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/service"
)
func ServiceProvider(rules []string) service.Provider {
srv := NewService()
err := srv.LoadRules(rules...)
return func(ctn *service.Container) (interface{}, error) {
if err != nil {
return nil, errors.WithStack(err)
}
return srv, nil
}
}

33
internal/auth/service.go Normal file
View File

@ -0,0 +1,33 @@
package auth
import (
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/service"
)
const ServiceName service.Name = "auth"
// From retrieves the auth service in the given container.
func From(container *service.Container) (*Service, error) {
service, err := container.Service(ServiceName)
if err != nil {
return nil, errors.Wrapf(err, "error while retrieving '%s' service", ServiceName)
}
srv, ok := service.(*Service)
if !ok {
return nil, errors.Errorf("retrieved service is not a valid '%s' service", ServiceName)
}
return srv, nil
}
// Must retrieves the auth service in the given container or panic otherwise.
func Must(container *service.Container) *Service {
srv, err := From(container)
if err != nil {
panic(err)
}
return srv
}

View File

@ -18,6 +18,7 @@ type Config struct {
HTTP HTTPConfig `yaml:"http"`
OIDC OIDCConfig `yaml:"oidc"`
Database DatabaseConfig `yaml:"database"`
Auth AuthConfig `yaml:"auth"`
}
// NewFromFile retrieves the configuration from the given file
@ -69,6 +70,10 @@ type DatabaseConfig struct {
DSN string `yaml:"dsn" env:"DATABASE_DSN"`
}
type AuthConfig struct {
Rules []string `yaml:"rules" env:"AUTH_RULES"`
}
func NewDumpDefault() *Config {
config := NewDefault()
return config
@ -102,6 +107,9 @@ func NewDefault() *Config {
Database: DatabaseConfig{
DSN: "host=localhost database=guesstimate",
},
Auth: AuthConfig{
Rules: []string{},
},
}
}

View File

@ -1,8 +1,11 @@
package route
import (
"fmt"
"net/http"
"forge.cadoles.com/Cadoles/guesstimate/internal/auth"
"forge.cadoles.com/Cadoles/guesstimate/internal/model"
"forge.cadoles.com/Cadoles/guesstimate/internal/orm"
@ -31,6 +34,7 @@ func handleLoginCallback(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
ctn := container.Must(ctx)
conf := config.Must(ctn)
auth := auth.Must(ctn)
idToken, err := oidc.IDToken(w, r)
if err != nil {
@ -65,10 +69,26 @@ func handleLoginCallback(w http.ResponseWriter, r *http.Request) {
db := orm.Must(ctn).DB()
repo := model.NewUserRepository(db)
if _, err := repo.CreateOrConnectUser(ctx, claims.Email); err != nil {
user, err := repo.CreateOrConnectUser(ctx, claims.Email)
if err != nil {
panic(errors.Wrap(err, "could not upsert user"))
}
authorized, err := auth.Authorize(user)
if err != nil {
panic(errors.WithStack(err))
}
if !authorized {
message := fmt.Sprintf(
"You are not authorized to access this application. Disconnect by navigating to %s.",
"http://"+r.Host+"/logout",
)
http.Error(w, message, http.StatusForbidden)
return
}
if err := session.SaveUserEmail(w, r, claims.Email); err != nil {
panic(errors.WithStack(err))
}