Compare commits

...

2 Commits

Author SHA1 Message Date
c9da46ea98 feat: add basic auth tutorial implementation as exemple
All checks were successful
Cadoles/bouncer/pipeline/head This commit looks good
2023-06-24 12:29:03 -06:00
8d91f646c2 feat: refactor layers registration
All checks were successful
Cadoles/bouncer/pipeline/head This commit looks good
2023-06-23 17:54:34 -06:00
22 changed files with 258 additions and 118 deletions

View File

@ -245,7 +245,19 @@ func (s *Server) updateLayer(w http.ResponseWriter, r *http.Request) {
}
if updateLayerReq.Options != nil {
if err := schema.ValidateLayerOptions(ctx, layer.Type, updateLayerReq.Options); err != nil {
layerOptionsSchema, err := setup.GetLayerOptionsSchema(layer.Type)
if err != nil {
logger.Error(r.Context(), "could not retrieve layer options schema", logger.E(errors.WithStack(err)))
api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil)
return
}
rawOptions := func(opts *store.LayerOptions) map[string]any {
return *opts
}(updateLayerReq.Options)
if err := schema.Validate(ctx, layerOptionsSchema, rawOptions); err != nil {
logger.Error(r.Context(), "could not validate layer options", logger.E(errors.WithStack(err)))
var invalidDataErr *schema.InvalidDataError

View File

@ -28,7 +28,7 @@ func RunCommand() *cli.Command {
logger.SetFormat(logger.Format(conf.Logger.Format))
logger.SetLevel(logger.Level(conf.Logger.Level))
layers, err := setup.CreateLayers(ctx.Context, conf)
layers, err := setup.GetLayers(ctx.Context, conf)
if err != nil {
return errors.Wrap(err, "could not initialize director layers")
}

View File

@ -0,0 +1,112 @@
package basicauth
import (
"crypto/sha256"
"crypto/subtle"
"net/http"
proxy "forge.cadoles.com/Cadoles/go-proxy"
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director"
"forge.cadoles.com/cadoles/bouncer/internal/store"
"gitlab.com/wpetit/goweb/logger"
)
const LayerType store.LayerType = "basicauth"
type BasicAuth struct{}
// LayerType implements director.MiddlewareLayer.
func (*BasicAuth) LayerType() store.LayerType {
return LayerType
}
// Middleware implements director.MiddlewareLayer.
func (*BasicAuth) Middleware(layer *store.Layer) proxy.Middleware {
// La méthode doit retourner un "Middleware" qui est un alias
// pour les fonctions généralement utilisées
// dans les librairies http en Go pour créer
// une fonction d'interception/transformation de requête.
return func(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
// On récupère les identifiants "basic auth" transmis (ou non)
// avec la requête
username, password, ok := r.BasicAuth()
// On créait une méthode locale pour gérer le cas d'une erreur d'authentification.
unauthorized := func() {
// On ajoute cette entête HTTP à la réponse pour déclencher l'affichage
// de la popup d'authentification dans le navigateur web de l'utilisateur.
w.Header().Set("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`)
// On retoure un code d'erreur HTTP 401 (Unauthorized)
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
}
if !ok {
// L'entête Authorization est absente ou ne correspondant
// pas à du Basic Auth, on retourne une erreur HTTP 401 et
// on interrompt le traitement de la requête ici
unauthorized()
return
}
// On extrait les identifiants des options associées
// à notre layer
expectedUsername, usernameExists := layer.Options["username"].(string)
expectedPassword, passwordExists := layer.Options["password"].(string)
// Si le nom d'utilisateur ou le mot de passe attendu n'existe pas
// alors on retourne une erreur HTTP 500 à l'utilisateur.
if !usernameExists || !passwordExists {
logger.Error(r.Context(), "basicauth layer missing password or username option")
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
// On vérifie les identifiants associés à la requête
isAuthenticated := authenticate(username, password, expectedUsername, expectedPassword)
// Si les identifiants sont non reconnus alors
// on interrompt le traitement de la requête
if !isAuthenticated {
unauthorized()
return
}
// L'authentification a réussie ! On passe la main au handler HTTP suivant
next.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}
}
func authenticate(username, password string, expectedUsername, expectedPassword string) bool {
// On génère une empreinte au format sha256 pour nos identifiants
usernameHash := sha256.Sum256([]byte(username))
passwordHash := sha256.Sum256([]byte(password))
// On effectue de même avec les identifiants attendus.
// Pour l'instant, on utilise un couple d'identifiants en "dur".
expectedUsernameHash := sha256.Sum256([]byte(expectedUsername))
expectedPasswordHash := sha256.Sum256([]byte(expectedPassword))
// On utilise la méthode subtle.ConstantTimeCompare()
// pour faire la comparaison des identifiants en temps constant
// et ainsi éviter les attaques par timing.
usernameMatch := (subtle.ConstantTimeCompare(usernameHash[:], expectedUsernameHash[:]) == 1)
passwordMatch := (subtle.ConstantTimeCompare(passwordHash[:], expectedPasswordHash[:]) == 1)
// L'utilisateur est authentifié si son nom et son mot de passe
// correspondent avec ceux attendus.
return usernameMatch && passwordMatch
}
func New() *BasicAuth {
return &BasicAuth{}
}
var _ director.MiddlewareLayer = &BasicAuth{}

View File

@ -0,0 +1,14 @@
{
"$id": "https://forge.cadoles.com/cadoles/bouncer/schemas/basicauth-layer-options",
"title": "BasicAuth layer options",
"type": "object",
"properties": {
"username": {
"type": "string"
},
"password": {
"type": "string"
}
},
"additionalProperties": false
}

View File

@ -0,0 +1,8 @@
package basicauth
import (
_ "embed"
)
//go:embed layer-options.json
var RawLayerOptionsSchema []byte

View File

@ -6,7 +6,7 @@ import (
"strings"
"time"
"forge.cadoles.com/cadoles/bouncer/internal/queue"
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director/layer/queue"
"github.com/pkg/errors"
"github.com/redis/go-redis/v9"
)

View File

@ -0,0 +1,8 @@
package queue
import (
_ "embed"
)
//go:embed schema/layer-options.json
var RawLayerOptionsSchema []byte

View File

@ -1,20 +0,0 @@
package queue
import (
_ "embed"
"forge.cadoles.com/cadoles/bouncer/internal/schema"
"github.com/pkg/errors"
)
//go:embed schema/layer-options.json
var rawLayerOptionsSchema []byte
func init() {
layerOptionsSchema, err := schema.Parse(rawLayerOptionsSchema)
if err != nil {
panic(errors.Wrap(err, "could not parse queue layer options schema"))
}
schema.RegisterLayerOptionsSchema(LayerType, layerOptionsSchema)
}

View File

@ -7,8 +7,10 @@ import (
"github.com/qri-io/jsonschema"
)
func Parse(data []byte) (*jsonschema.Schema, error) {
var schema jsonschema.Schema
type Schema = jsonschema.Schema
func Parse(data []byte) (*Schema, error) {
var schema Schema
if err := json.Unmarshal(data, &schema); err != nil {
return nil, errors.WithStack(err)
}

View File

@ -1,56 +0,0 @@
package schema
import (
"context"
"forge.cadoles.com/cadoles/bouncer/internal/store"
"github.com/pkg/errors"
"github.com/qri-io/jsonschema"
)
var defaultRegistry = NewRegistry()
func RegisterLayerOptionsSchema(layerType store.LayerType, schema *jsonschema.Schema) {
defaultRegistry.RegisterLayerOptionsSchema(layerType, schema)
}
func ValidateLayerOptions(ctx context.Context, layerType store.LayerType, options *store.LayerOptions) error {
if err := defaultRegistry.ValidateLayerOptions(ctx, layerType, options); err != nil {
return errors.WithStack(err)
}
return nil
}
type Registry struct {
layerOptionSchemas map[store.LayerType]*jsonschema.Schema
}
func (r *Registry) RegisterLayerOptionsSchema(layerType store.LayerType, schema *jsonschema.Schema) {
r.layerOptionSchemas[layerType] = schema
}
func (r *Registry) ValidateLayerOptions(ctx context.Context, layerType store.LayerType, options *store.LayerOptions) error {
schema, exists := r.layerOptionSchemas[layerType]
if !exists {
return errors.WithStack(ErrSchemaNotFound)
}
rawOptions := func(opts *store.LayerOptions) map[string]any {
return *opts
}(options)
state := schema.Validate(ctx, rawOptions)
if len(*state.Errs) > 0 {
return errors.WithStack(NewInvalidDataError(*state.Errs...))
}
return nil
}
func NewRegistry() *Registry {
return &Registry{
layerOptionSchemas: make(map[store.LayerType]*jsonschema.Schema),
}
}

View File

@ -0,0 +1,17 @@
package schema
import (
"context"
"github.com/pkg/errors"
)
func Validate(ctx context.Context, schema *Schema, data map[string]any) error {
state := schema.Validate(ctx, data)
if len(*state.Errs) > 0 {
return errors.WithStack(NewInvalidDataError(*state.Errs...))
}
return nil
}

View File

@ -0,0 +1,15 @@
package setup
import (
"forge.cadoles.com/cadoles/bouncer/internal/config"
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director"
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director/layer/basicauth"
)
func init() {
RegisterLayer(basicauth.LayerType, setupBasicAuthLayer, basicauth.RawLayerOptionsSchema)
}
func setupBasicAuthLayer(conf *config.Config) (director.Layer, error) {
return &basicauth.BasicAuth{}, nil
}

View File

@ -7,16 +7,26 @@ import (
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director"
"forge.cadoles.com/cadoles/bouncer/internal/store"
"github.com/pkg/errors"
"github.com/qri-io/jsonschema"
)
var defaultRegistry = NewRegistry()
func RegisterLayer(layerType store.LayerType, setupFunc LayerSetupFunc) {
defaultRegistry.RegisterLayer(layerType, setupFunc)
func RegisterLayer(layerType store.LayerType, setupFunc LayerSetupFunc, rawOptionsSchema []byte) {
defaultRegistry.RegisterLayer(layerType, setupFunc, rawOptionsSchema)
}
func CreateLayers(ctx context.Context, conf *config.Config) ([]director.Layer, error) {
layers, err := defaultRegistry.CreateLayers(ctx, conf)
func GetLayerOptionsSchema(layerType store.LayerType) (*jsonschema.Schema, error) {
schema, err := defaultRegistry.GetLayerOptionsSchema(layerType)
if err != nil {
return nil, errors.WithStack(err)
}
return schema, nil
}
func GetLayers(ctx context.Context, conf *config.Config) ([]director.Layer, error) {
layers, err := defaultRegistry.GetLayers(ctx, conf)
if err != nil {
return nil, errors.WithStack(err)
}

5
internal/setup/error.go Normal file
View File

@ -0,0 +1,5 @@
package setup
import "errors"
var ErrNotFound = errors.New("not found")

View File

@ -1,21 +1,22 @@
package setup
import (
"context"
"time"
"forge.cadoles.com/cadoles/bouncer/internal/config"
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director"
"forge.cadoles.com/cadoles/bouncer/internal/queue"
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director/layer/queue"
queueRedis "forge.cadoles.com/cadoles/bouncer/internal/proxy/director/layer/queue/redis"
"github.com/pkg/errors"
"github.com/redis/go-redis/v9"
)
func init() {
RegisterLayer(queue.LayerType, setupQueueLayer)
RegisterLayer(queue.LayerType, setupQueueLayer, queue.RawLayerOptionsSchema)
}
func setupQueueLayer(ctx context.Context, conf *config.Config) (director.Layer, error) {
adapter, err := NewQueueAdapter(ctx, conf.Redis)
func setupQueueLayer(conf *config.Config) (director.Layer, error) {
adapter, err := newQueueAdapter(conf.Redis)
if err != nil {
return nil, errors.WithStack(err)
}
@ -33,3 +34,12 @@ func setupQueueLayer(ctx context.Context, conf *config.Config) (director.Layer,
options...,
), nil
}
func newQueueAdapter(redisConf config.RedisConfig) (queue.Adapter, error) {
rdb := redis.NewUniversalClient(&redis.UniversalOptions{
Addrs: redisConf.Adresses,
MasterName: string(redisConf.Master),
})
return queueRedis.NewAdapter(rdb, 2), nil
}

View File

@ -1,20 +0,0 @@
package setup
import (
"context"
"forge.cadoles.com/cadoles/bouncer/internal/config"
"forge.cadoles.com/cadoles/bouncer/internal/queue"
"github.com/redis/go-redis/v9"
queueRedis "forge.cadoles.com/cadoles/bouncer/internal/queue/redis"
)
func NewQueueAdapter(ctx context.Context, redisConf config.RedisConfig) (queue.Adapter, error) {
rdb := redis.NewUniversalClient(&redis.UniversalOptions{
Addrs: redisConf.Adresses,
MasterName: string(redisConf.Master),
})
return queueRedis.NewAdapter(rdb, 2), nil
}

View File

@ -5,25 +5,48 @@ import (
"forge.cadoles.com/cadoles/bouncer/internal/config"
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director"
"forge.cadoles.com/cadoles/bouncer/internal/schema"
"forge.cadoles.com/cadoles/bouncer/internal/store"
"github.com/pkg/errors"
)
type layerEntry struct {
setup LayerSetupFunc
rawOptionsSchema []byte
}
type Registry struct {
layers map[store.LayerType]LayerSetupFunc
layers map[store.LayerType]layerEntry
}
type LayerSetupFunc func(context.Context, *config.Config) (director.Layer, error)
type LayerSetupFunc func(*config.Config) (director.Layer, error)
func (r *Registry) RegisterLayer(layerType store.LayerType, layerSetup LayerSetupFunc) {
r.layers[layerType] = layerSetup
func (r *Registry) RegisterLayer(layerType store.LayerType, layerSetup LayerSetupFunc, rawOptionsSchema []byte) {
r.layers[layerType] = layerEntry{
setup: layerSetup,
rawOptionsSchema: rawOptionsSchema,
}
}
func (r *Registry) CreateLayers(ctx context.Context, conf *config.Config) ([]director.Layer, error) {
func (r *Registry) GetLayerOptionsSchema(layerType store.LayerType) (*schema.Schema, error) {
layerEntry, exists := r.layers[layerType]
if !exists {
return nil, errors.WithStack(ErrNotFound)
}
schema, err := schema.Parse(layerEntry.rawOptionsSchema)
if err != nil {
return nil, errors.WithStack(err)
}
return schema, nil
}
func (r *Registry) GetLayers(ctx context.Context, conf *config.Config) ([]director.Layer, error) {
layers := make([]director.Layer, 0, len(r.layers))
for layerType, layerSetup := range r.layers {
layer, err := layerSetup(ctx, conf)
for layerType, layerEntry := range r.layers {
layer, err := layerEntry.setup(conf)
if err != nil {
return nil, errors.Wrapf(err, "could not create layer '%s'", layerType)
}
@ -52,6 +75,6 @@ func (r *Registry) GetLayerTypes() []store.LayerType {
func NewRegistry() *Registry {
return &Registry{
layers: make(map[store.LayerType]LayerSetupFunc),
layers: make(map[store.LayerType]layerEntry),
}
}