feat(auth): add user access filtering rules
This commit is contained in:
71
internal/auth/auth.go
Normal file
71
internal/auth/auth.go
Normal 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
20
internal/auth/provider.go
Normal 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
33
internal/auth/service.go
Normal 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
|
||||
}
|
@ -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{},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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))
|
||||
}
|
||||
|
Reference in New Issue
Block a user