package authn import ( "bytes" "html/template" "io" "net/http" "path/filepath" "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/proxy/director/layer/util" "forge.cadoles.com/cadoles/bouncer/internal/rule" ruleHTTP "forge.cadoles.com/cadoles/bouncer/internal/rule/http" "forge.cadoles.com/cadoles/bouncer/internal/store" "github.com/Masterminds/sprig/v3" "github.com/pkg/errors" "gitlab.com/wpetit/goweb/logger" ) type Layer struct { layerType store.LayerType auth Authenticator debug bool ruleEngineCache *util.RuleEngineCache[*Vars, *LayerOptions] templateDir string } 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 { director.HandleError(ctx, w, r, http.StatusInternalServerError, errors.Wrap(err, "could not parse layer options")) return } if preAuth, ok := l.auth.(PreAuthentication); ok { if err := preAuth.PreAuthentication(w, r, layer); err != nil { if errors.Is(err, ErrSkipRequest) { return } err = errors.WithStack(err) logger.Error(ctx, "could not execute pre-auth hook", logger.CapturedE(err)) l.renderErrorPage(w, r, layer, options, err) return } } matches := wildcard.MatchAny(r.URL.String(), options.MatchURLs...) if !matches { next.ServeHTTP(w, r) return } user, err := l.auth.Authenticate(w, r, layer) if err != nil { if errors.Is(err, ErrSkipRequest) { return } if errors.Is(err, ErrForbidden) { l.renderForbiddenPage(w, r, layer, options, user) return } err = errors.WithStack(err) logger.Error(ctx, "could not authenticate user", logger.CapturedE(err)) l.renderErrorPage(w, r, layer, options, err) return } if err := l.applyRules(ctx, r, layer, options, user); err != nil { if errors.Is(err, ErrForbidden) { l.renderForbiddenPage(w, r, layer, options, user) return } err = errors.WithStack(err) logger.Error(ctx, "could not apply rules", logger.CapturedE(err)) l.renderErrorPage(w, r, layer, options, err) return } if postAuth, ok := l.auth.(PostAuthentication); ok { if err := postAuth.PostAuthentication(w, r, layer, user); err != nil { if errors.Is(err, ErrSkipRequest) { return } if errors.Is(err, ErrForbidden) { l.renderForbiddenPage(w, r, layer, options, user) return } err = errors.WithStack(err) logger.Error(ctx, "could not execute post-auth hook", logger.CapturedE(err)) l.renderErrorPage(w, r, layer, options, err) return } } next.ServeHTTP(w, r) } return http.HandlerFunc(fn) } } type baseTemplateData struct { Layer *store.Layer Debug bool Request *http.Request } func (l *Layer) renderForbiddenPage(w http.ResponseWriter, r *http.Request, layer *store.Layer, options *LayerOptions, user *User) { templateData := struct { baseTemplateData User *User }{ baseTemplateData: baseTemplateData{ Layer: layer, Debug: l.debug, Request: r, }, User: user, } w.WriteHeader(http.StatusForbidden) l.renderPage(w, r, "forbidden", options.Templates.Forbidden.Block, templateData) } func (l *Layer) renderErrorPage(w http.ResponseWriter, r *http.Request, layer *store.Layer, options *LayerOptions, err error) { templateData := struct { baseTemplateData Err error }{ baseTemplateData: baseTemplateData{ Layer: layer, Debug: l.debug, Request: r, }, Err: err, } w.WriteHeader(http.StatusInternalServerError) l.renderPage(w, r, "error", options.Templates.Error.Block, templateData) } func (l *Layer) renderPage(w http.ResponseWriter, r *http.Request, page string, block string, templateData any) { ctx := r.Context() pattern := filepath.Join(l.templateDir, page+".gohtml") logger.Info(ctx, "loading authn templates", logger.F("pattern", pattern)) tmpl, err := template.New("").Funcs(sprig.FuncMap()).ParseGlob(pattern) if err != nil { director.HandleError(ctx, w, r, http.StatusInternalServerError, errors.Wrap(err, "could not load authn templates")) return } w.Header().Add("Cache-Control", "no-cache") var buf bytes.Buffer if err := tmpl.ExecuteTemplate(w, block, templateData); err != nil { director.HandleError(ctx, w, r, http.StatusInternalServerError, errors.Wrap(err, "could not render authn page")) return } if _, err := io.Copy(w, &buf); err != nil { logger.Error(ctx, "could not write authn page", logger.CapturedE(errors.WithStack(err))) } } // LayerType implements director.MiddlewareLayer func (l *Layer) LayerType() store.LayerType { return l.layerType } func NewLayer(layerType store.LayerType, auth Authenticator, funcs ...OptionFunc) *Layer { opts := NewOptions(funcs...) return &Layer{ ruleEngineCache: util.NewInMemoryRuleEngineCache[*Vars, *LayerOptions](func(options *LayerOptions) (*rule.Engine[*Vars], error) { engine, err := rule.NewEngine[*Vars]( rule.WithRules(options.Rules...), rule.WithExpr(getAuthnAPI()...), ruleHTTP.WithRequestFuncs(), ) if err != nil { return nil, errors.WithStack(err) } return engine, nil }), layerType: layerType, auth: auth, templateDir: opts.TemplateDir, debug: opts.Debug, } } var _ director.MiddlewareLayer = &Layer{}