Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
Matovidlo committed Jun 21, 2024
1 parent 04ce97d commit 10bb411
Show file tree
Hide file tree
Showing 19 changed files with 847 additions and 239 deletions.
2 changes: 1 addition & 1 deletion initializer.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
{
"httpRequest": {
"method": "GET",
"path": "/apps/app/proxy-config"
"path": "/apps/123/proxy-config"
},
"httpResponse": {
"body": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,7 @@ type Basic struct {
Base
Password string `json:"password"`
}

func (p *Basic) IsAuthorized(password string) bool {
return p.Password == password
}
12 changes: 6 additions & 6 deletions internal/pkg/service/appsproxy/dependencies/dependencies.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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 {
Expand Down
11 changes: 5 additions & 6 deletions internal/pkg/service/appsproxy/proxy/apphandler/apphandler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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")

Check failure on line 68 in internal/pkg/service/appsproxy/proxy/apphandler/authproxy/basicauth/handler.go

View workflow job for this annotation

GitHub Actions / Lint / lint

commentFormatting: put a space between `//` and comment text (gocritic)
}

// 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)
}
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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,
},
},
},
}
}
Loading

0 comments on commit 10bb411

Please sign in to comment.