package authn import ( "fmt" "net/http" "strconv" "time" "forge.cadoles.com/Cadoles/go-proxy" "forge.cadoles.com/Cadoles/go-proxy/wildcard" "forge.cadoles.com/cadoles/bouncer/internal/proxy/director" "forge.cadoles.com/cadoles/bouncer/internal/store" "github.com/expr-lang/expr" "github.com/gorilla/sessions" "github.com/pkg/errors" "gitlab.com/wpetit/goweb/logger" ) type Layer struct { layerType store.LayerType auth Authenticator store sessions.Store } func (l *Layer) Middleware(layer *store.Layer) proxy.Middleware { return func(next http.Handler) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() options, err := fromStoreOptions(layer.Options) if err != nil { logger.Error(ctx, "could not parse layer options", logger.E(errors.WithStack(err))) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } sess, err := l.store.Get(r, options.Session.Name) if err != nil { logger.Error(ctx, "could not retrieve session", logger.E(errors.WithStack(err))) } if preAuth, ok := l.auth.(PreAuthentication); ok { if err := preAuth.PreAuthentication(w, r, layer, sess); err != nil { if errors.Is(err, ErrSkipRequest) { return } logger.Error(ctx, "could not execute pre-auth hook", logger.E(errors.WithStack(err))) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } } matches := wildcard.MatchAny(r.URL.String(), options.MatchURLs...) if !matches { next.ServeHTTP(w, r) return } user, err := l.auth.Authenticate(w, r, layer, sess) if err != nil { if errors.Is(err, ErrSkipRequest) { return } logger.Error(ctx, "could not authenticate user", logger.E(errors.WithStack(err))) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } if err = l.injectHeaders(r, options, user); err != nil { logger.Error(ctx, "could not inject header", logger.E(errors.WithStack(err))) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } if postAuth, ok := l.auth.(PostAuthentication); ok { if err := postAuth.PostAuthentication(w, r, layer, sess, user); err != nil { if errors.Is(err, ErrSkipRequest) { return } logger.Error(ctx, "could not execute post-auth hook", logger.E(errors.WithStack(err))) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } } next.ServeHTTP(w, r) } return http.HandlerFunc(fn) } } func (l *Layer) getRuleOptions(r *http.Request) []expr.Option { options := make([]expr.Option, 0) setHeader := expr.Function( "set_header", func(params ...any) (any, error) { name := params[0].(string) rawValue := params[1] var value string switch v := rawValue.(type) { case time.Time: value = strconv.FormatInt(v.UTC().Unix(), 10) case time.Duration: value = strconv.FormatInt(int64(v.Seconds()), 10) default: value = fmt.Sprintf("%v", rawValue) } r.Header.Set(name, value) return true, nil }, new(func(string, string) bool), ) options = append(options, setHeader) delHeaders := expr.Function( "del_headers", func(params ...any) (any, error) { pattern := params[0].(string) deleted := false for key := range r.Header { if !wildcard.Match(key, pattern) { continue } r.Header.Del(key) deleted = true } return deleted, nil }, new(func(string) bool), ) options = append(options, delHeaders) return options } func (l *Layer) injectHeaders(r *http.Request, options *LayerOptions, user *User) error { rules := options.Headers.Rules if len(rules) == 0 { return nil } env := map[string]any{ "user": user, } rulesOptions := l.getRuleOptions(r) for i, r := range rules { program, err := expr.Compile(r, rulesOptions...) if err != nil { return errors.Wrapf(err, "could not compile header rule #%d", i) } if _, err := expr.Run(program, env); err != nil { return errors.Wrapf(err, "could not execute header rule #%d", i) } } return nil } // LayerType implements director.MiddlewareLayer func (l *Layer) LayerType() store.LayerType { return l.layerType } func NewLayer(layerType store.LayerType, auth Authenticator, adapter StoreAdapter) *Layer { return &Layer{ layerType: layerType, auth: auth, store: NewStore(adapter), } } var _ director.MiddlewareLayer = &Layer{}