diff --git a/initializer.json b/initializer.json index 8fbfcd009b..cc4aa3e3d1 100644 --- a/initializer.json +++ b/initializer.json @@ -2,7 +2,7 @@ { "httpRequest": { "method": "GET", - "path": "/apps/app/proxy-config" + "path": "/apps/123/proxy-config" }, "httpResponse": { "body": { diff --git a/internal/pkg/service/appsproxy/dataapps/auth/provider/basic.go b/internal/pkg/service/appsproxy/dataapps/auth/provider/basic.go index 9ec7f764fa..b3f4a8bb0e 100644 --- a/internal/pkg/service/appsproxy/dataapps/auth/provider/basic.go +++ b/internal/pkg/service/appsproxy/dataapps/auth/provider/basic.go @@ -4,3 +4,7 @@ type Basic struct { Base Password string `json:"password"` } + +func (p *Basic) IsAuthorized(password string) bool { + return p.Password == password +} diff --git a/internal/pkg/service/appsproxy/dependencies/dependencies.go b/internal/pkg/service/appsproxy/dependencies/dependencies.go index 9d8a195882..801adb3023 100644 --- a/internal/pkg/service/appsproxy/dependencies/dependencies.go +++ b/internal/pkg/service/appsproxy/dependencies/dependencies.go @@ -28,7 +28,7 @@ import ( "github.com/keboola/keboola-as-code/internal/pkg/service/appsproxy/dataapps/notify" "github.com/keboola/keboola-as-code/internal/pkg/service/appsproxy/dataapps/wakeup" "github.com/keboola/keboola-as-code/internal/pkg/service/appsproxy/proxy/apphandler" - "github.com/keboola/keboola-as-code/internal/pkg/service/appsproxy/proxy/apphandler/oidcproxy" + "github.com/keboola/keboola-as-code/internal/pkg/service/appsproxy/proxy/apphandler/authproxy" "github.com/keboola/keboola-as-code/internal/pkg/service/appsproxy/proxy/apphandler/upstream" "github.com/keboola/keboola-as-code/internal/pkg/service/appsproxy/proxy/pagewriter" "github.com/keboola/keboola-as-code/internal/pkg/service/appsproxy/proxy/transport" @@ -53,7 +53,7 @@ type ServiceScope interface { AppConfigLoader() *appconfig.Loader UpstreamTransport() http.RoundTripper UpstreamManager() *upstream.Manager - OidcProxyManager() *oidcproxy.Manager + AuthProxyManager() *authproxy.Manager PageWriter() *pagewriter.Writer NotifyManager() *notify.Manager WakeupManager() *wakeup.Manager @@ -73,7 +73,7 @@ type serviceScope struct { appHandlers *apphandler.Manager upstreamTransport http.RoundTripper upstreamManager *upstream.Manager - oidcProxyManager *oidcproxy.Manager + authProxyManager *authproxy.Manager pageWriter *pagewriter.Writer appConfigLoader *appconfig.Loader notifyManager *notify.Manager @@ -154,7 +154,7 @@ func newServiceScope(ctx context.Context, parentScp parentScopes, cfg config.Con d.appConfigLoader = appconfig.NewLoader(d) d.notifyManager = notify.NewManager(d) d.wakeupManager = wakeup.NewManager(d) - d.oidcProxyManager = oidcproxy.NewManager(d) + d.authProxyManager = authproxy.NewManager(d) d.upstreamManager = upstream.NewManager(d) d.appHandlers = apphandler.NewManager(d) @@ -185,8 +185,8 @@ func (v *serviceScope) UpstreamManager() *upstream.Manager { return v.upstreamManager } -func (v *serviceScope) OidcProxyManager() *oidcproxy.Manager { - return v.oidcProxyManager +func (v *serviceScope) AuthProxyManager() *authproxy.Manager { + return v.authProxyManager } func (v *serviceScope) PageWriter() *pagewriter.Writer { diff --git a/internal/pkg/service/appsproxy/proxy/apphandler/apphandler.go b/internal/pkg/service/appsproxy/proxy/apphandler/apphandler.go index 0e25976c5b..5c2ec57699 100644 --- a/internal/pkg/service/appsproxy/proxy/apphandler/apphandler.go +++ b/internal/pkg/service/appsproxy/proxy/apphandler/apphandler.go @@ -13,8 +13,8 @@ import ( "github.com/keboola/keboola-as-code/internal/pkg/service/appsproxy/config" "github.com/keboola/keboola-as-code/internal/pkg/service/appsproxy/dataapps/api" "github.com/keboola/keboola-as-code/internal/pkg/service/appsproxy/dataapps/auth/provider" + "github.com/keboola/keboola-as-code/internal/pkg/service/appsproxy/proxy/apphandler/authproxy/selector" "github.com/keboola/keboola-as-code/internal/pkg/service/appsproxy/proxy/apphandler/chain" - "github.com/keboola/keboola-as-code/internal/pkg/service/appsproxy/proxy/apphandler/oidcproxy" "github.com/keboola/keboola-as-code/internal/pkg/service/common/ctxattr" svcErrors "github.com/keboola/keboola-as-code/internal/pkg/service/common/errors" "github.com/keboola/keboola-as-code/internal/pkg/service/common/httpserver/middleware" @@ -33,7 +33,7 @@ type appHandler struct { type ruleIndex int -func newAppHandler(manager *Manager, app api.AppConfig, appUpstream chain.Handler, authHandlers map[provider.ID]*oidcproxy.Handler) (http.Handler, error) { +func newAppHandler(manager *Manager, app api.AppConfig, appUpstream chain.Handler, authHandlers map[provider.ID]selector.Handler) (http.Handler, error) { handler := &appHandler{ manager: manager, app: app, @@ -45,7 +45,7 @@ func newAppHandler(manager *Manager, app api.AppConfig, appUpstream chain.Handle // Create handler with all auth handlers, to route internal URLs if len(authHandlers) > 0 { - if h, err := manager.oidcProxyManager.ProviderSelector().For(app, authHandlers); err == nil { + if h, err := manager.authProxyManager.ProviderSelector().For(app, authHandlers); err == nil { handler.allAuthHandlers = h } else { return nil, err @@ -78,18 +78,17 @@ func newAppHandler(manager *Manager, app api.AppConfig, appUpstream chain.Handle } // Filter authentication handlers - authHandlersPerRule := make(map[provider.ID]*oidcproxy.Handler) + authHandlersPerRule := make(map[provider.ID]selector.Handler) for _, providerID := range rule.Auth { if authHandler, found := authHandlers[providerID]; found { authHandlersPerRule[providerID] = authHandler } else { - // TODO: add basic auth provider return nil, errors.Errorf(`authentication provider "%s" not found for "%s"`, providerID.String(), rule.Value) } } // Merge authentication handlers for the rule to one selector handler - if h, err := manager.oidcProxyManager.ProviderSelector().For(app, authHandlersPerRule); err == nil { + if h, err := manager.authProxyManager.ProviderSelector().For(app, authHandlersPerRule); err == nil { handler.authHandlerPerRule[index] = h } else { return nil, err diff --git a/internal/pkg/service/appsproxy/proxy/apphandler/authproxy/basicauth/handler.go b/internal/pkg/service/appsproxy/proxy/apphandler/authproxy/basicauth/handler.go new file mode 100644 index 0000000000..728cb4808a --- /dev/null +++ b/internal/pkg/service/appsproxy/proxy/apphandler/authproxy/basicauth/handler.go @@ -0,0 +1,106 @@ +package basicauth + +import ( + "net/http" + "time" + + "github.com/benbjohnson/clock" + + "github.com/keboola/keboola-as-code/internal/pkg/service/appsproxy/config" + "github.com/keboola/keboola-as-code/internal/pkg/service/appsproxy/dataapps/api" + "github.com/keboola/keboola-as-code/internal/pkg/service/appsproxy/dataapps/auth/provider" + "github.com/keboola/keboola-as-code/internal/pkg/service/appsproxy/proxy/apphandler/chain" + "github.com/keboola/keboola-as-code/internal/pkg/service/appsproxy/proxy/pagewriter" + "github.com/keboola/keboola-as-code/internal/pkg/utils/errors" + "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/util" +) + +const ( + basicAuthCookie = "proxyBasicAuth" + formPagePath = config.InternalPrefix + "/form" +) + +type Handler struct { + basicAuth provider.Basic + upstream chain.Handler + app api.AppConfig + pageWriter *pagewriter.Writer + clock clock.Clock +} + +func NewHandler( + auth provider.Basic, + app api.AppConfig, + upstream chain.Handler, + pageWriter *pagewriter.Writer, + clock clock.Clock, +) *Handler { + return &Handler{ + basicAuth: auth, + app: app, + upstream: upstream, + pageWriter: pageWriter, + clock: clock, + } +} + +func (h *Handler) Name() string { + return h.basicAuth.Name() +} + +func (h *Handler) SignInPath() string { + return "/form" +} + +func (h *Handler) CookieExpiration() time.Duration { + return 5 * time.Minute +} + +func (h *Handler) ServeHTTPOrError(w http.ResponseWriter, req *http.Request) error { + cookie, _ := req.Cookie(basicAuthCookie) + if _, ok := req.Form["password"]; !ok || cookie == nil { + h.pageWriter.WriteFormPage(w, req, http.StatusOK) + return nil + } + + if req.URL.Path != "_proxy/form" { + return nil + //return errors.New("404") + } + + // TODO: check post body for form filled value + // Set cookies with `CookieExpiration`. Cookie "proxyBasicAuth": "sha1(value, salt from config)" + // Test cases: 1. Wrong password, Wrong cookie, Correct password, Correct cookie, sign_out with deletion of cookie + if !h.basicAuth.IsAuthorized("") { + // TODO: here should be pagewriter injected? + // TODO: specific error catch on pagewriter + return errors.New("wrong password prompted") + } + + // TODO: check cookie before setting it + host, _ := util.SplitHostPort(req.Host) + if host == "" { + panic(errors.New("host cannot be empty")) + } + + v := &http.Cookie{ + Name: basicAuthCookie, + // Value: value, sha1 + Path: "/", + Domain: host, + Secure: true, + HttpOnly: true, + SameSite: http.SameSiteStrictMode, + } + + expires := h.CookieExpiration() + if expires > 0 { + // If there is an expiration, set it + v.Expires = h.clock.Now().Add(expires) + } else { + // Otherwise clear the cookie + v.MaxAge = -1 + } + + return h.upstream.ServeHTTPOrError(w, req) +} diff --git a/internal/pkg/service/appsproxy/proxy/apphandler/authproxy/manager.go b/internal/pkg/service/appsproxy/proxy/apphandler/authproxy/manager.go new file mode 100644 index 0000000000..b8489fdcb2 --- /dev/null +++ b/internal/pkg/service/appsproxy/proxy/apphandler/authproxy/manager.go @@ -0,0 +1,62 @@ +package authproxy + +import ( + "github.com/benbjohnson/clock" + + "github.com/keboola/keboola-as-code/internal/pkg/log" + "github.com/keboola/keboola-as-code/internal/pkg/service/appsproxy/config" + "github.com/keboola/keboola-as-code/internal/pkg/service/appsproxy/dataapps/api" + "github.com/keboola/keboola-as-code/internal/pkg/service/appsproxy/dataapps/auth/provider" + "github.com/keboola/keboola-as-code/internal/pkg/service/appsproxy/proxy/apphandler/authproxy/basicauth" + "github.com/keboola/keboola-as-code/internal/pkg/service/appsproxy/proxy/apphandler/authproxy/oidcproxy" + "github.com/keboola/keboola-as-code/internal/pkg/service/appsproxy/proxy/apphandler/authproxy/selector" + "github.com/keboola/keboola-as-code/internal/pkg/service/appsproxy/proxy/apphandler/chain" + "github.com/keboola/keboola-as-code/internal/pkg/service/appsproxy/proxy/pagewriter" +) + +type Manager struct { + logger log.Logger + config config.Config + pageWriter *pagewriter.Writer + clock clock.Clock + providerSelector *selector.Selector +} + +type dependencies interface { + Logger() log.Logger + Clock() clock.Clock + Config() config.Config + PageWriter() *pagewriter.Writer +} + +func NewManager(d dependencies) *Manager { + return &Manager{ + logger: d.Logger(), + config: d.Config(), + pageWriter: d.PageWriter(), + clock: d.Clock(), + providerSelector: selector.New(d), + } +} + +func (m *Manager) ProviderSelector() *selector.Selector { + return m.providerSelector +} + +func (m *Manager) NewHandlers(app api.AppConfig, upstream chain.Handler) map[provider.ID]selector.Handler { + authHandlers := make(map[provider.ID]selector.Handler, len(app.AuthProviders)) + // TODO: factory method + for _, auth := range app.AuthProviders { + switch p := auth.(type) { + case provider.OIDC: + authHandlers[auth.ID()] = oidcproxy.NewHandler(m.logger, m.config, m.providerSelector, m.pageWriter, app, p, upstream) + + case provider.Basic: + authHandlers[auth.ID()] = basicauth.NewHandler(p, app, upstream, m.pageWriter, m.clock) + + default: + panic("unknown auth provider type") + } + } + return authHandlers +} diff --git a/internal/pkg/service/appsproxy/proxy/apphandler/authproxy/oidcproxy/config.go b/internal/pkg/service/appsproxy/proxy/apphandler/authproxy/oidcproxy/config.go new file mode 100644 index 0000000000..7725d168eb --- /dev/null +++ b/internal/pkg/service/appsproxy/proxy/apphandler/authproxy/oidcproxy/config.go @@ -0,0 +1,116 @@ +package oidcproxy + +import ( + "crypto/sha256" + "fmt" + "net/http" + "strings" + + "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options" + "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/validation" + + "github.com/keboola/keboola-as-code/internal/pkg/service/appsproxy/config" + "github.com/keboola/keboola-as-code/internal/pkg/service/appsproxy/dataapps/api" + "github.com/keboola/keboola-as-code/internal/pkg/service/appsproxy/dataapps/auth/provider" + "github.com/keboola/keboola-as-code/internal/pkg/service/appsproxy/proxy/apphandler/authproxy/selector" + "github.com/keboola/keboola-as-code/internal/pkg/service/appsproxy/proxy/apphandler/chain" + "github.com/keboola/keboola-as-code/internal/pkg/service/appsproxy/proxy/pagewriter" + "github.com/keboola/keboola-as-code/internal/pkg/utils/errors" +) + +func proxyConfig( + cfg config.Config, + selector *selector.Selector, + pageWriter *pagewriter.Writer, + app api.AppConfig, + authProvider provider.OIDC, + upstream chain.Handler, +) (*options.Options, error) { + // Generate unique cookies secret + secret, err := generateCookieSecret(cfg, app, authProvider.ID()) + if err != nil { + return nil, err + } + + proxyProvider, err := authProvider.ProxyProviderOptions() + if err != nil { + return nil, err + } + + v := options.NewOptions() + + // Connect to the app upstream + v.UpstreamHandler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + if err := upstream.ServeHTTPOrError(w, req); err != nil { + pageWriter.WriteError(w, req, &app, err) + } + }) + + // Render the selector page, if login is needed, it is not an internal URL + v.OnNeedsLogin = func(rw http.ResponseWriter, req *http.Request) (stop bool) { + // Bypass internal paths + if strings.HasPrefix(req.URL.Path, v.ProxyPrefix) { + return false + } + + return selector.OnNeedsLogin(&app, pageWriter.WriteError)(rw, req) + } + // Setup + domain := app.CookieDomain(cfg.API.PublicURL) + redirectURL := cfg.API.PublicURL.Scheme + "://" + domain + config.InternalPrefix + "/callback" + v.Logging.RequestIDHeader = config.RequestIDHeader + v.Logging.RequestEnabled = false // we have log middleware for all requests + v.Cookie.Secret = secret + v.Cookie.Domains = []string{domain} + v.Cookie.SameSite = "strict" + v.ProxyPrefix = config.InternalPrefix + v.RawRedirectURL = redirectURL + v.Providers = options.Providers{proxyProvider} + v.SkipProviderButton = true + v.Session = options.SessionOptions{Type: options.CookieSessionStoreType} + v.EmailDomains = []string{"*"} + v.InjectRequestHeaders = []options.Header{ + headerFromClaim("X-Kbc-User-Name", "name"), + headerFromClaim("X-Kbc-User-Email", options.OIDCEmailClaim), + headerFromClaim("X-Kbc-User-Roles", options.OIDCGroupsClaim), + } + + // Cannot separate errors from info because when ErrToInfo is false (default), + // oauthproxy keeps forcibly setting its global error writer to os.Stderr whenever a new proxy instance is created. + v.Logging.ErrToInfo = true + + if err := validation.Validate(v); err != nil { + return nil, err + } + + return v, nil +} + +// generateCookieSecret creates a unique cookie secret for each app and provider. +// This is necessary because otherwise cookies created by provider A would also be valid in a section that requires provider B but not A. +// To solve this we use the combination of the provider id and our salt. +func generateCookieSecret(cfg config.Config, app api.AppConfig, providerID provider.ID) (string, error) { + if cfg.CookieSecretSalt == "" { + return "", errors.New("missing cookie secret salt") + } + + h := sha256.New() + h.Write([]byte(app.ID.String() + "/" + providerID.String() + "/" + cfg.CookieSecretSalt)) + bs := h.Sum(nil) + + // Result must be 32 chars, 2 hex chars for each byte + return fmt.Sprintf("%x", bs[:16]), nil +} + +func headerFromClaim(header, claim string) options.Header { + return options.Header{ + Name: header, + Values: []options.HeaderValue{ + { + ClaimSource: &options.ClaimSource{ + Claim: claim, + }, + }, + }, + } +} diff --git a/internal/pkg/service/appsproxy/proxy/apphandler/authproxy/oidcproxy/doc.go b/internal/pkg/service/appsproxy/proxy/apphandler/authproxy/oidcproxy/doc.go new file mode 100644 index 0000000000..f1acc74ee2 --- /dev/null +++ b/internal/pkg/service/appsproxy/proxy/apphandler/authproxy/oidcproxy/doc.go @@ -0,0 +1,3 @@ +// Package oidcproxy provides factory for OAuth2Proxy library. +// The logic is inserted before the handler from the "upstream" package. +package oidcproxy diff --git a/internal/pkg/service/appsproxy/proxy/apphandler/authproxy/oidcproxy/handler.go b/internal/pkg/service/appsproxy/proxy/apphandler/authproxy/oidcproxy/handler.go new file mode 100644 index 0000000000..7b0e18180a --- /dev/null +++ b/internal/pkg/service/appsproxy/proxy/apphandler/authproxy/oidcproxy/handler.go @@ -0,0 +1,98 @@ +package oidcproxy + +import ( + "fmt" + "net/http" + "time" + + oauthproxy "github.com/oauth2-proxy/oauth2-proxy/v7" + proxyOptions "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options" + + "github.com/keboola/keboola-as-code/internal/pkg/log" + "github.com/keboola/keboola-as-code/internal/pkg/service/appsproxy/config" + "github.com/keboola/keboola-as-code/internal/pkg/service/appsproxy/dataapps/api" + "github.com/keboola/keboola-as-code/internal/pkg/service/appsproxy/dataapps/auth/provider" + "github.com/keboola/keboola-as-code/internal/pkg/service/appsproxy/proxy/apphandler/authproxy/selector" + "github.com/keboola/keboola-as-code/internal/pkg/service/appsproxy/proxy/apphandler/chain" + "github.com/keboola/keboola-as-code/internal/pkg/service/appsproxy/proxy/pagewriter" + svcErrors "github.com/keboola/keboola-as-code/internal/pkg/service/common/errors" + "github.com/keboola/keboola-as-code/internal/pkg/utils/errors" +) + +type Handler struct { + providerName string + proxyConfig *proxyOptions.Options + proxyHandler *oauthproxy.OAuthProxy + initErr error +} + +func NewHandler( + logger log.Logger, + cfg config.Config, + selector *selector.Selector, + pw *pagewriter.Writer, + app api.AppConfig, + auth provider.OIDC, + upstream chain.Handler, +) *Handler { + var err error + handler := &Handler{providerName: auth.Name()} + + // Create proxy configuration + handler.proxyConfig, err = proxyConfig(cfg, selector, pw, app, auth, upstream) + if err != nil { + handler.initErr = wrapHandlerInitErr(app, auth, err) + return handler + } + + // Create proxy page writer adapter + pageWriter, err := newPageWriter(logger, pw, app, auth, handler.proxyConfig) + if err != nil { + handler.initErr = wrapHandlerInitErr(app, auth, err) + return handler + } + + // Create proxy HTTP handler + authValidator := func(email string) bool { return true } // there is no need to verify individual users + handler.proxyHandler, err = oauthproxy.NewOAuthProxyWithPageWriter(handler.proxyConfig, authValidator, pageWriter) + if err != nil { + handler.initErr = wrapHandlerInitErr(app, auth, err) + return handler + } + + return handler +} + +func (h *Handler) Name() string { + return h.providerName +} + +func (h *Handler) CookieExpiration() time.Duration { + if h.initErr != nil { + return 5 * time.Minute + } + return h.proxyConfig.Cookie.Expire +} + +func (h *Handler) SignInPath() string { + if h.initErr != nil { + return "/error" + } + return h.proxyHandler.SignInPath +} + +func (h *Handler) ServeHTTPOrError(w http.ResponseWriter, req *http.Request) error { + if h.initErr != nil { + return h.initErr + } + + // Pass request to OAuth2Proxy + h.proxyHandler.ServeHTTP(w, req) // errors are handled by the page writer + return nil +} + +func wrapHandlerInitErr(app api.AppConfig, auth provider.Provider, err error) error { + return svcErrors. + NewServiceUnavailableError(errors.PrefixErrorf(err, `application "%s" has invalid configuration for authentication provider "%s"`, app.IdAndName(), auth.ID())). + WithUserMessage(fmt.Sprintf(`Application "%s" has invalid configuration for authentication provider "%s".`, app.IdAndName(), auth.ID())) +} diff --git a/internal/pkg/service/appsproxy/proxy/apphandler/authproxy/oidcproxy/logging/writer.go b/internal/pkg/service/appsproxy/proxy/apphandler/authproxy/oidcproxy/logging/writer.go new file mode 100644 index 0000000000..ed451eeb4c --- /dev/null +++ b/internal/pkg/service/appsproxy/proxy/apphandler/authproxy/oidcproxy/logging/writer.go @@ -0,0 +1,35 @@ +package logging + +import ( + "context" + "io" + + "github.com/keboola/keboola-as-code/internal/pkg/log" +) + +type loggerWriter struct { + logger log.Logger + level string + buffer []byte +} + +const newLine = byte(10) + +func (w *loggerWriter) Write(p []byte) (n int, err error) { + w.buffer = append(w.buffer, p...) + + if len(p) > 0 && p[len(p)-1] == newLine { + w.logger.Log(context.Background(), w.level, string(w.buffer)) + w.buffer = []byte{} + return 1, nil + } + + return len(p), nil +} + +func NewLoggerWriter(logger log.Logger, level string) io.Writer { + return &loggerWriter{ + logger: logger, + level: level, + } +} diff --git a/internal/pkg/service/appsproxy/proxy/apphandler/authproxy/oidcproxy/pagewriter.go b/internal/pkg/service/appsproxy/proxy/apphandler/authproxy/oidcproxy/pagewriter.go new file mode 100644 index 0000000000..df0248f088 --- /dev/null +++ b/internal/pkg/service/appsproxy/proxy/apphandler/authproxy/oidcproxy/pagewriter.go @@ -0,0 +1,107 @@ +package oidcproxy + +import ( + "net/http" + "strings" + + oauthproxy "github.com/oauth2-proxy/oauth2-proxy/v7" + "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options" + proxypw "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/app/pagewriter" + "github.com/spf13/cast" + "go.opentelemetry.io/otel/attribute" + semconv "go.opentelemetry.io/otel/semconv/v1.17.0" + + "github.com/keboola/keboola-as-code/internal/pkg/log" + "github.com/keboola/keboola-as-code/internal/pkg/service/appsproxy/dataapps/api" + "github.com/keboola/keboola-as-code/internal/pkg/service/appsproxy/dataapps/auth/provider" + "github.com/keboola/keboola-as-code/internal/pkg/service/appsproxy/proxy/pagewriter" + "github.com/keboola/keboola-as-code/internal/pkg/service/common/ctxattr" +) + +type proxyPageWriter proxypw.Writer + +// pageWriter is adapter between common page writer and OAuth2Proxy specific page writer. +type pageWriter struct { + proxyPageWriter + logger log.Logger + app api.AppConfig + authProvider provider.Provider + pageWriter *pagewriter.Writer +} + +func newPageWriter(logger log.Logger, pw *pagewriter.Writer, app api.AppConfig, authProvider provider.Provider, opts *options.Options) (*pageWriter, error) { + parent, err := proxypw.NewWriter( + proxypw.Opts{ + TemplatesPath: opts.Templates.Path, + CustomLogo: opts.Templates.CustomLogo, + ProxyPrefix: opts.ProxyPrefix, + Footer: opts.Templates.Footer, + Version: oauthproxy.VERSION, + Debug: opts.Templates.Debug, + ProviderName: opts.Providers[0].Name, + SignInMessage: opts.Templates.Banner, + DisplayLoginForm: opts.Templates.DisplayLoginForm, + }, + ) + if err != nil { + return nil, err + } + + return &pageWriter{ + logger: logger.WithComponent("oauth2proxy.pw"), + proxyPageWriter: parent, + app: app, + authProvider: authProvider, + pageWriter: pw, + }, nil +} + +func (pw *pageWriter) WriteErrorPage(w http.ResponseWriter, req *http.Request, opts proxypw.ErrorPageOpts) { + // Convert messages to string + var messages []string + for _, msg := range opts.Messages { + if str := cast.ToString(msg); str != "" { + messages = append(messages, str) + } + } + + // Default messages + if len(messages) == 0 { + switch opts.Status { + case http.StatusUnauthorized: + messages = []string{"You need to be logged in to access this resource."} + case http.StatusForbidden: + messages = []string{"You do not have permission to access this resource."} + case http.StatusInternalServerError: + messages = []string{"Oops! Something went wrong."} + default: + messages = []string{opts.AppError} + } + } + + // Exception ID + exceptionID := pagewriter.ExceptionIDPrefix + opts.RequestID + + joinedMessage := strings.Join(messages, "\n") + // Add attributes + req = req.WithContext(ctxattr.ContextWith( + req.Context(), + semconv.HTTPStatusCode(opts.Status), + attribute.String("exceptionId", exceptionID), + attribute.String("error.userMessages", joinedMessage), + attribute.String("error.details", opts.AppError), + )) + + // Log warning + pw.logger.Warn(req.Context(), strings.Join(messages, "\n")) //nolint:contextcheck // false positive + + pw.pageWriter.WriteErrorPage(w, req, &pw.app, opts.Status, joinedMessage+opts.AppError, exceptionID) +} + +func (pw *pageWriter) ProxyErrorHandler(w http.ResponseWriter, req *http.Request, err error) { + pw.pageWriter.ProxyErrorHandler(w, req, pw.app, err) +} + +func (pw *pageWriter) WriteRobotsTxt(w http.ResponseWriter, req *http.Request) { + pw.pageWriter.WriteRobotsTxt(w, req) +} diff --git a/internal/pkg/service/appsproxy/proxy/apphandler/authproxy/selector/handler.go b/internal/pkg/service/appsproxy/proxy/apphandler/authproxy/selector/handler.go new file mode 100644 index 0000000000..dfa95e8fed --- /dev/null +++ b/internal/pkg/service/appsproxy/proxy/apphandler/authproxy/selector/handler.go @@ -0,0 +1,13 @@ +package selector + +import ( + "net/http" + "time" +) + +type Handler interface { + Name() string + ServeHTTPOrError(w http.ResponseWriter, req *http.Request) error + CookieExpiration() time.Duration // TODO: configurable from API Provider base? + SignInPath() string +} diff --git a/internal/pkg/service/appsproxy/proxy/apphandler/authproxy/selector/selector.go b/internal/pkg/service/appsproxy/proxy/apphandler/authproxy/selector/selector.go new file mode 100644 index 0000000000..ea408064e1 --- /dev/null +++ b/internal/pkg/service/appsproxy/proxy/apphandler/authproxy/selector/selector.go @@ -0,0 +1,273 @@ +package selector + +import ( + "context" + "net/http" + "net/url" + "slices" + "strings" + "time" + + "github.com/benbjohnson/clock" + "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/util" + + "github.com/keboola/keboola-as-code/internal/pkg/service/appsproxy/config" + "github.com/keboola/keboola-as-code/internal/pkg/service/appsproxy/dataapps/api" + "github.com/keboola/keboola-as-code/internal/pkg/service/appsproxy/dataapps/auth/provider" + "github.com/keboola/keboola-as-code/internal/pkg/service/appsproxy/proxy/pagewriter" + svcErrors "github.com/keboola/keboola-as-code/internal/pkg/service/common/errors" + "github.com/keboola/keboola-as-code/internal/pkg/utils/errors" +) + +const ( + providerCookie = "_oauth2_provider" + providerQueryParam = "provider" + continueAuthQueryParam = "continue_auth" + callbackQueryParam = "rd" // value match OAuth2Proxy internals and shouldn't be modified (see AppDirector there) + selectionPagePath = config.InternalPrefix + "/selection" + signOutPath = config.InternalPrefix + "/sign_out" + proxyCallbackPath = config.InternalPrefix + "/callback" + ignoreProviderCookieCtxKey = ctxKey("ignoreProviderCookieCtxKey") + selectorHandlerCtxKey = ctxKey("selectorHandlerCtxKey") +) + +type ctxKey string + +type Selector struct { + clock clock.Clock + config config.Config + pageWriter *pagewriter.Writer +} + +type SelectorForAppRule struct { + *Selector + app api.AppConfig + handlers map[provider.ID]Handler +} + +type dependencies interface { + Clock() clock.Clock + Config() config.Config + PageWriter() *pagewriter.Writer +} + +func New(d dependencies) *Selector { + return &Selector{ + clock: d.Clock(), + config: d.Config(), + pageWriter: d.PageWriter(), + } +} + +func (s *Selector) For(app api.AppConfig, handlers map[provider.ID]Handler) (*SelectorForAppRule, error) { + // Validate handlers count + if len(handlers) == 0 { + return nil, svcErrors.NewServiceUnavailableError(errors.New(`no authentication provider found`)) + } + + return &SelectorForAppRule{Selector: s, app: app, handlers: handlers}, nil +} + +// ServeHTTPOrError renders selector page if there is more than one authentication handler, +// and no handler is selected, or the selected handler is not allowed for the requested path (see api.AuthRule). +// +// The selector page is rendered: +// 1. If it is accessed directly using selectionPagePath, the status code is StatusOK. +// 2. If no handler is selected and the path requires authorization, the status code is StatusUnauthorized. +func (s *SelectorForAppRule) ServeHTTPOrError(w http.ResponseWriter, req *http.Request) error { + // To make the same site strict cookie work we need to replace the redirect from the auth provider with a page that does the redirect. + if req.URL.Path == proxyCallbackPath { + query := req.URL.Query() + if query.Get(continueAuthQueryParam) != "true" { + query.Set(continueAuthQueryParam, "true") + baseURL := s.app.BaseURL(s.config.API.PublicURL) + redirectURL := baseURL.ResolveReference(&url.URL{Path: req.URL.Path, RawQuery: query.Encode()}) + s.pageWriter.WriteRedirectPage(w, req, http.StatusOK, redirectURL.String()) + return nil + } + } + + // Store the selector to the context. + // It is used by the OnNeedsLogin callback, to render the selector page, if the provider needs login. + // Internal paths (it includes sing in) are bypassed, see Manager.proxyConfig for details. + req = req.WithContext(context.WithValue(req.Context(), selectorHandlerCtxKey, s)) + + // Clear cookie on logout + if req.URL.Path == signOutPath { + // This clears provider selection cookie while oauth2-proxy clears the session cookie. + // The user isn't logged out on the provider's side, but when redirected to the provider they're + // forced to select their account again because of the "select_account" flag in LoginURLParameters. + s.clearCookie(w, req) + } + + // Render selector page, if it is accessed directly + if req.URL.Path == selectionPagePath { + return s.writeSelectorPage(w, req, http.StatusOK) + } + + // Skip selector page, if there is only one provider + if len(s.handlers) == 1 { + // The handlers variable is a map, use the first handler via a for cycle + for id, handler := range s.handlers { + // Set cookie if needed + if providerID := s.providerIDFromCookie(req); providerID != id { + s.setCookie(w, req, id, handler) + } + return handler.ServeHTTPOrError(w, req) + } + } + + // Ignore cookie if we have already tried this provider, but the provider requires login. + providerID := s.providerIDFromCookie(req) + if ignore, _ := req.Context().Value(ignoreProviderCookieCtxKey).(bool); ignore { + providerID = "" + } + + // Identify the chosen provider by the cookie + if handler := s.handlers[providerID]; handler != nil { + return handler.ServeHTTPOrError(w, req) + } + + // No matching handler found + return s.writeSelectorPage(w, req, http.StatusUnauthorized) +} + +func (s *SelectorForAppRule) writeSelectorPage(w http.ResponseWriter, req *http.Request, status int) error { + // Mark provider selected + id := provider.ID(req.URL.Query().Get(providerQueryParam)) + if selected, found := s.handlers[id]; found { + // Set cookie with the same expiration as other provider cookies + s.setCookie(w, req, id, selected) + + // Get path for redirect after sign in, it must not refer to an external URL + query := make(url.Values) + callback := req.URL.Query().Get(callbackQueryParam) + if isAcceptedCallbackURL(callback) { + query.Set(callbackQueryParam, callback) + } + + // Render sign in page, set callback after login + s.redirect(w, req, selected.SignInPath(), query) + return nil + } + + // Render the page, if there is no cookie or the value is invalid + s.pageWriter.WriteSelectorPage(w, req, status, s.selectorPageData(req)) + return nil +} + +func (s *SelectorForAppRule) selectorPageData(req *http.Request) *pagewriter.SelectorPageData { + // Pass link back to the current page, if reasonable, otherwise the user will be redirected to / + var callback string + if req.Method == http.MethodGet { + callback = req.URL.Path + } + + // Base URL for all providers + pageURL := s.url(req, selectionPagePath, nil) + + // Generate link for each providers + data := &pagewriter.SelectorPageData{App: pagewriter.NewAppData(&s.app)} + for id, handler := range s.handlers { + query := make(url.Values) + query.Set(providerQueryParam, id.String()) + if isAcceptedCallbackURL(callback) { + query.Set(callbackQueryParam, callback) + } + data.Providers = append(data.Providers, pagewriter.ProviderData{ + Name: handler.Name(), + URL: pageURL.ResolveReference(&url.URL{RawQuery: query.Encode()}).String(), + }) + } + + // Sort items + slices.SortStableFunc(data.Providers, func(a, b pagewriter.ProviderData) int { + return strings.Compare(a.Name, b.Name) + }) + + return data +} + +func (s *Selector) OnNeedsLogin( + app *api.AppConfig, + writeErrorPage func(rw http.ResponseWriter, req *http.Request, app *api.AppConfig, err error), +) func(rw http.ResponseWriter, req *http.Request) bool { + return func(w http.ResponseWriter, req *http.Request) (stop bool) { + // Determine, if we should render the selector page using the selector instance from the context + if selector, ok := req.Context().Value(selectorHandlerCtxKey).(*SelectorForAppRule); ok { + // If there is only one provider, continue to the sing in page + if len(selector.handlers) <= 1 { + return false + } + + // Go back and render the selector page, ignore the cookie value + req = req.WithContext(context.WithValue(req.Context(), ignoreProviderCookieCtxKey, true)) + if err := selector.ServeHTTPOrError(w, req); err != nil { + writeErrorPage(w, req, app, err) + } + return true + } + + // Fallback, the selector instance is not found, it shouldn't happen. + // Clear the cookie and redirect to the same path, so the selector page is rendered. + s.clearCookie(w, req) + w.Header().Set("Location", req.URL.Path) + w.WriteHeader(http.StatusFound) + return true + } +} + +func (s *Selector) redirect(w http.ResponseWriter, req *http.Request, path string, query url.Values) { + w.Header().Set("Location", s.url(req, path, query).String()) + w.WriteHeader(http.StatusFound) +} + +func (s *Selector) url(req *http.Request, path string, query url.Values) *url.URL { + return &url.URL{Scheme: s.config.API.PublicURL.Scheme, Host: req.Host, Path: path, RawQuery: query.Encode()} +} + +func (s *Selector) providerIDFromCookie(req *http.Request) provider.ID { + if cookie, _ := req.Cookie(providerCookie); cookie != nil && cookie.Value != "" { + return provider.ID(cookie.Value) + } + return "" +} + +func (s *Selector) clearCookie(w http.ResponseWriter, req *http.Request) { + http.SetCookie(w, s.cookie(req, "", -1)) +} + +func (s *Selector) setCookie(w http.ResponseWriter, req *http.Request, id provider.ID, handler Handler) { + http.SetCookie(w, s.cookie(req, id.String(), handler.CookieExpiration())) +} + +func (s *Selector) cookie(req *http.Request, value string, expires time.Duration) *http.Cookie { + host, _ := util.SplitHostPort(req.Host) + if host == "" { + panic(errors.New("host cannot be empty")) + } + + v := &http.Cookie{ + Name: providerCookie, + Value: value, + Path: "/", + Domain: host, + Secure: true, + HttpOnly: true, + SameSite: http.SameSiteStrictMode, + } + + if expires > 0 { + // If there is an expiration, set it + v.Expires = s.clock.Now().Add(expires) + } else { + // Otherwise clear the cookie + v.MaxAge = -1 + } + + return v +} + +func isAcceptedCallbackURL(callback string) bool { + return callback != "" && callback != "/" && callback != selectionPagePath && strings.HasPrefix(callback, "/") +} diff --git a/internal/pkg/service/appsproxy/proxy/apphandler/basicauth/handler.go b/internal/pkg/service/appsproxy/proxy/apphandler/basicauth/handler.go deleted file mode 100644 index e05964f0a7..0000000000 --- a/internal/pkg/service/appsproxy/proxy/apphandler/basicauth/handler.go +++ /dev/null @@ -1,19 +0,0 @@ -package basicauth - -import ( - "time" - - "github.com/keboola/keboola-as-code/internal/pkg/service/appsproxy/dataapps/auth/provider" -) - -type Handler struct { - provider provider.Provider -} - -func (h *Handler) ID() provider.ID { - return h.provider.ID() -} - -func (h *Handler) CookieExpiration() time.Duration { - return 5 * time.Minute -} diff --git a/internal/pkg/service/appsproxy/proxy/apphandler/basicauth/prompt.go b/internal/pkg/service/appsproxy/proxy/apphandler/basicauth/prompt.go deleted file mode 100644 index 3d43e76bce..0000000000 --- a/internal/pkg/service/appsproxy/proxy/apphandler/basicauth/prompt.go +++ /dev/null @@ -1,177 +0,0 @@ -package basicauth - -import ( - "context" - "errors" - "net/http" - "net/url" - "strings" - "time" - - "github.com/benbjohnson/clock" - "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/util" - - "github.com/keboola/keboola-as-code/internal/pkg/service/appsproxy/config" - "github.com/keboola/keboola-as-code/internal/pkg/service/appsproxy/dataapps/api" - "github.com/keboola/keboola-as-code/internal/pkg/service/appsproxy/dataapps/auth/provider" - "github.com/keboola/keboola-as-code/internal/pkg/service/appsproxy/proxy/pagewriter" -) - -const ( - providerCookie = "_basicauth_provider" - providerQueryParam = "provider" - promptPagePath = config.InternalPrefix + "/password" - signOutPath = config.InternalPrefix + "/sign_out" - promptHandlerCtxKey = ctxKey("promptHandlerCtxKey") -) - -type ctxKey string - -type Prompt struct { - clock clock.Clock - config config.Config - pageWriter *pagewriter.Writer - app api.AppConfig - handler *Handler -} - -/*type Prompt struct { - *Selector - app api.AppConfig - handlers map[provider.ID]*Handler -}*/ - -// ServeHTTPOrError renders selector page if there is more than one authentication handler, -// and no handler is selected, or the selected handler is not allowed for the requested path (see api.AuthRule). -// -// The selector page is rendered: -// 1. If it is accessed directly using selectionPagePath, the status code is StatusOK. -// 2. If no handler is selected and the path requires authorization, the status code is StatusUnauthorized. -func (s *Prompt) ServeHTTPOrError(w http.ResponseWriter, req *http.Request) error { - // Store the selector to the context. - // It is used by the OnNeedsLogin callback, to render the selector page, if the provider needs login. - // Internal paths (it includes sing in) are bypassed, see Manager.proxyConfig for details. - req = req.WithContext(context.WithValue(req.Context(), promptHandlerCtxKey, s)) - - // Clear cookie on logout - if req.URL.Path == signOutPath { - // This clears provider selection cookie while oauth2-proxy clears the session cookie. - // The user isn't logged out on the provider's side, but when redirected to the provider they're - // forced to select their account again because of the "select_account" flag in LoginURLParameters. - s.clearCookie(w, req) - } - - // Render selector page, if it is accessed directly - if req.URL.Path == promptPagePath { - return s.writeSelectorPage(w, req, http.StatusOK) - } - - // Set cookie if needed - /*if providerID := s.providerIDFromCookie(req); providerID != handler.Provider().ID() { - s.setCookie(w, req, handler) - } - return handler.ServeHTTPOrError(w, req)*/ - - // No matching handler found - return s.writeSelectorPage(w, req, http.StatusUnauthorized) -} - -func (s *Prompt) writeSelectorPage(w http.ResponseWriter, req *http.Request, status int) error { - // Mark provider selected - id := provider.ID(req.URL.Query().Get(providerQueryParam)) - // Set cookie with the same expiration as other provider cookies - if s.handler != nil && s.handler.ID() == id { - s.setCookie(w, req) - - // Get path for redirect after sign in, it must not refer to an external URL - query := make(url.Values) - /*callback := req.URL.Query().Get(callbackQueryParam) - if isAcceptedCallbackURL(callback) { - query.Set(callbackQueryParam, callback) - }*/ - - // Render sign in page, set callback after login - s.redirect(w, req, "/sign_in", query) - return nil - } - // Render the page, if there is no cookie or the value is invalid - return nil -} - -func (s *Prompt) promptPageData(req *http.Request) *pagewriter.PromptPageData { - // Pass link back to the current page, if reasonable, otherwise the user will be redirected to / - var callback string - if req.Method == http.MethodGet { - callback = req.URL.Path - } - - // Base URL for all providers - pageURL := s.url(req, promptPagePath, nil) - _ = pageURL - - // Generate link for each providers - data := &pagewriter.PromptPageData{App: pagewriter.NewAppData(&s.app)} - query := make(url.Values) - query.Set(providerQueryParam, s.handler.ID().String()) - _ = callback - /*if isAcceptedCallbackURL(callback) { - query.Set(callbackQueryParam, callback) - }*/ - - return data -} - -func (s *Prompt) redirect(w http.ResponseWriter, req *http.Request, path string, query url.Values) { - w.Header().Set("Location", s.url(req, path, query).String()) - w.WriteHeader(http.StatusFound) -} - -func (s *Prompt) url(req *http.Request, path string, query url.Values) *url.URL { - return &url.URL{Scheme: s.config.API.PublicURL.Scheme, Host: req.Host, Path: path, RawQuery: query.Encode()} -} - -func (s *Prompt) providerIDFromCookie(req *http.Request) provider.ID { - if cookie, _ := req.Cookie(providerCookie); cookie != nil && cookie.Value != "" { - return provider.ID(cookie.Value) - } - return "" -} - -func (s *Prompt) clearCookie(w http.ResponseWriter, req *http.Request) { - http.SetCookie(w, s.cookie(req, "", -1)) -} - -func (s *Prompt) setCookie(w http.ResponseWriter, req *http.Request) { - http.SetCookie(w, s.cookie(req, s.handler.ID().String(), s.handler.CookieExpiration())) -} - -func (s *Prompt) cookie(req *http.Request, value string, expires time.Duration) *http.Cookie { - host, _ := util.SplitHostPort(req.Host) - if host == "" { - panic(errors.New("host cannot be empty")) - } - - v := &http.Cookie{ - Name: providerCookie, - Value: value, - Path: "/", - Domain: host, - Secure: true, - HttpOnly: true, - SameSite: http.SameSiteStrictMode, - } - - if expires > 0 { - // If there is an expiration, set it - v.Expires = s.clock.Now().Add(expires) - } else { - // Otherwise clear the cookie - v.MaxAge = -1 - } - - return v -} - -func isAcceptedCallbackURL(callback string) bool { - return callback != "" && callback != "/" && callback != promptPagePath && strings.HasPrefix(callback, "/") -} diff --git a/internal/pkg/service/appsproxy/proxy/apphandler/manager.go b/internal/pkg/service/appsproxy/proxy/apphandler/manager.go index feb5b721e0..a170dec539 100644 --- a/internal/pkg/service/appsproxy/proxy/apphandler/manager.go +++ b/internal/pkg/service/appsproxy/proxy/apphandler/manager.go @@ -8,8 +8,7 @@ import ( "github.com/keboola/keboola-as-code/internal/pkg/service/appsproxy/config" "github.com/keboola/keboola-as-code/internal/pkg/service/appsproxy/dataapps/api" "github.com/keboola/keboola-as-code/internal/pkg/service/appsproxy/dataapps/appconfig" - "github.com/keboola/keboola-as-code/internal/pkg/service/appsproxy/dataapps/auth/provider" - "github.com/keboola/keboola-as-code/internal/pkg/service/appsproxy/proxy/apphandler/oidcproxy" + "github.com/keboola/keboola-as-code/internal/pkg/service/appsproxy/proxy/apphandler/authproxy" "github.com/keboola/keboola-as-code/internal/pkg/service/appsproxy/proxy/apphandler/upstream" "github.com/keboola/keboola-as-code/internal/pkg/service/appsproxy/proxy/pagewriter" "github.com/keboola/keboola-as-code/internal/pkg/service/appsproxy/syncmap" @@ -24,7 +23,7 @@ type Manager struct { telemetry telemetry.Telemetry configLoader *appconfig.Loader upstreamManager *upstream.Manager - oidcProxyManager *oidcproxy.Manager + authProxyManager *authproxy.Manager pageWriter *pagewriter.Writer handlers *syncmap.SyncMap[api.AppID, appHandlerWrapper] } @@ -39,7 +38,7 @@ type dependencies interface { Telemetry() telemetry.Telemetry PageWriter() *pagewriter.Writer UpstreamManager() *upstream.Manager - OidcProxyManager() *oidcproxy.Manager + AuthProxyManager() *authproxy.Manager AppConfigLoader() *appconfig.Loader } @@ -49,7 +48,7 @@ func NewManager(d dependencies) *Manager { telemetry: d.Telemetry(), configLoader: d.AppConfigLoader(), upstreamManager: d.UpstreamManager(), - oidcProxyManager: d.OidcProxyManager(), + authProxyManager: d.AuthProxyManager(), pageWriter: d.PageWriter(), handlers: syncmap.New[api.AppID, appHandlerWrapper](func(api.AppID) *appHandlerWrapper { return &appHandlerWrapper{lock: &sync.Mutex{}} @@ -87,18 +86,7 @@ func (m *Manager) newHandler(ctx context.Context, app api.AppConfig) http.Handle } // Create authentication handlers - authHandlers := make(map[provider.ID]*oidcproxy.Handler, len(app.AuthProviders)) - for _, auth := range app.AuthProviders { - switch p := auth.(type) { - case provider.OIDC: - authHandlers[auth.ID()] = m.oidcProxyManager.NewHandler(app, p, appUpstream) - - case provider.Basic: - - default: - panic("unknown auth provider type") - } - } + authHandlers := m.authProxyManager.NewHandlers(app, appUpstream) // Create root handler for application handler, err := newAppHandler(m, app, appUpstream, authHandlers) diff --git a/internal/pkg/service/appsproxy/proxy/pagewriter/form.go b/internal/pkg/service/appsproxy/proxy/pagewriter/form.go new file mode 100644 index 0000000000..cd53f8d108 --- /dev/null +++ b/internal/pkg/service/appsproxy/proxy/pagewriter/form.go @@ -0,0 +1,13 @@ +package pagewriter + +import "net/http" + +type FormPageData struct { + App AppData +} + +func (pw *Writer) WriteFormPage(w http.ResponseWriter, req *http.Request, status int) { + w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate;") + w.Header().Set("pragma", "no-cache") + pw.writePage(w, req, "form.gohtml", status, nil) +} diff --git a/internal/pkg/service/appsproxy/proxy/pagewriter/prompt.go b/internal/pkg/service/appsproxy/proxy/pagewriter/prompt.go deleted file mode 100644 index b85c7459cd..0000000000 --- a/internal/pkg/service/appsproxy/proxy/pagewriter/prompt.go +++ /dev/null @@ -1,13 +0,0 @@ -package pagewriter - -import "net/http" - -type PromptPageData struct { - App AppData -} - -func (pw *Writer) WritePromptPage(w http.ResponseWriter, req *http.Request, status int) { - w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate;") - w.Header().Set("pragma", "no-cache") - pw.writePage(w, req, "prompt.gohtml", status, nil) -} diff --git a/internal/pkg/service/appsproxy/proxy/pagewriter/template/prompt.gohtml b/internal/pkg/service/appsproxy/proxy/pagewriter/template/form.gohtml similarity index 100% rename from internal/pkg/service/appsproxy/proxy/pagewriter/template/prompt.gohtml rename to internal/pkg/service/appsproxy/proxy/pagewriter/template/form.gohtml