mirror of
https://github.com/yusing/godoxy.git
synced 2026-04-22 08:18:29 +02:00
Feat/OIDC middleware (#50)
* implement OIDC middleware * auth code cleanup * allow override allowed_user in middleware, fix typos * fix tests and callbackURL * update next release docs * fix OIDC middleware not working with Authentik * feat: add groups support for OIDC claims (#41) Allow users to specify allowed groups in the env and use it to inspect the claims. This performs a logical AND of users and groups (additive). * merge feat/oidc-middleware (#49) * api: enrich provider statistifcs * fix: docker monitor now uses container status * Feat/auto schemas (#48) * use auto generated schemas * go version bump and dependencies upgrade * clarify some error messages --------- Co-authored-by: yusing <yusing@6uo.me> * cleanup some loadbalancer code * api: cleanup websocket code * api: add /v1/health/ws for health bubbles on dashboard * feat: experimental memory logger and logs api for WebUI --------- Co-authored-by: yusing <yusing@6uo.me> --------- Co-authored-by: yusing <yusing@6uo.me> Co-authored-by: Peter Olds <peter@olds.co>
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
@@ -26,28 +27,50 @@ type (
|
||||
name string
|
||||
construct ImplNewFunc
|
||||
impl any
|
||||
// priority is only applied for ReverseProxy.
|
||||
//
|
||||
// Middleware compose follows the order of the slice
|
||||
//
|
||||
// Default is 10, 0 is the highest
|
||||
priority int
|
||||
}
|
||||
ByPriority []*Middleware
|
||||
|
||||
RequestModifier interface {
|
||||
before(w http.ResponseWriter, r *http.Request) (proceed bool)
|
||||
}
|
||||
ResponseModifier interface{ modifyResponse(r *http.Response) error }
|
||||
MiddlewareWithSetup interface{ setup() }
|
||||
MiddlewareFinalizer interface{ finalize() }
|
||||
ResponseModifier interface{ modifyResponse(r *http.Response) error }
|
||||
MiddlewareWithSetup interface{ setup() }
|
||||
MiddlewareFinalizer interface{ finalize() }
|
||||
MiddlewareFinalizerWithError interface {
|
||||
finalize() error
|
||||
}
|
||||
MiddlewareWithTracer interface {
|
||||
enableTrace()
|
||||
getTracer() *Tracer
|
||||
}
|
||||
)
|
||||
|
||||
const DefaultPriority = 10
|
||||
|
||||
func (m ByPriority) Len() int { return len(m) }
|
||||
func (m ByPriority) Swap(i, j int) { m[i], m[j] = m[j], m[i] }
|
||||
func (m ByPriority) Less(i, j int) bool { return m[i].priority < m[j].priority }
|
||||
|
||||
func NewMiddleware[ImplType any]() *Middleware {
|
||||
// type check
|
||||
switch any(new(ImplType)).(type) {
|
||||
t := any(new(ImplType))
|
||||
switch t.(type) {
|
||||
case RequestModifier:
|
||||
case ResponseModifier:
|
||||
default:
|
||||
panic("must implement RequestModifier or ResponseModifier")
|
||||
}
|
||||
_, hasFinializer := t.(MiddlewareFinalizer)
|
||||
_, hasFinializerWithError := t.(MiddlewareFinalizerWithError)
|
||||
if hasFinializer && hasFinializerWithError {
|
||||
panic("MiddlewareFinalizer and MiddlewareFinalizerWithError are mutually exclusive")
|
||||
}
|
||||
return &Middleware{
|
||||
name: strings.ToLower(reflect.TypeFor[ImplType]().Name()),
|
||||
construct: func() any { return new(ImplType) },
|
||||
@@ -84,13 +107,29 @@ func (m *Middleware) apply(optsRaw OptionsRaw) E.Error {
|
||||
if len(optsRaw) == 0 {
|
||||
return nil
|
||||
}
|
||||
priority, ok := optsRaw["priority"].(int)
|
||||
if ok {
|
||||
m.priority = priority
|
||||
// remove priority for deserialization, restore later
|
||||
delete(optsRaw, "priority")
|
||||
defer func() {
|
||||
optsRaw["priority"] = priority
|
||||
}()
|
||||
} else {
|
||||
m.priority = DefaultPriority
|
||||
}
|
||||
return utils.Deserialize(optsRaw, m.impl)
|
||||
}
|
||||
|
||||
func (m *Middleware) finalize() {
|
||||
func (m *Middleware) finalize() error {
|
||||
if finalizer, ok := m.impl.(MiddlewareFinalizer); ok {
|
||||
finalizer.finalize()
|
||||
return nil
|
||||
}
|
||||
if finalizer, ok := m.impl.(MiddlewareFinalizerWithError); ok {
|
||||
return finalizer.finalize()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Middleware) New(optsRaw OptionsRaw) (*Middleware, E.Error) {
|
||||
@@ -105,7 +144,9 @@ func (m *Middleware) New(optsRaw OptionsRaw) (*Middleware, E.Error) {
|
||||
if err := mid.apply(optsRaw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mid.finalize()
|
||||
if err := mid.finalize(); err != nil {
|
||||
return nil, E.From(err)
|
||||
}
|
||||
return mid, nil
|
||||
}
|
||||
|
||||
@@ -119,8 +160,9 @@ func (m *Middleware) String() string {
|
||||
|
||||
func (m *Middleware) MarshalJSON() ([]byte, error) {
|
||||
return json.MarshalIndent(map[string]any{
|
||||
"name": m.name,
|
||||
"options": m.impl,
|
||||
"name": m.name,
|
||||
"options": m.impl,
|
||||
"priority": m.priority,
|
||||
}, "", " ")
|
||||
}
|
||||
|
||||
@@ -193,6 +235,7 @@ func PatchReverseProxy(rp *ReverseProxy, middlewaresMap map[string]OptionsRaw) (
|
||||
}
|
||||
|
||||
func patchReverseProxy(rp *ReverseProxy, middlewares []*Middleware) {
|
||||
sort.Sort(ByPriority(middlewares))
|
||||
middlewares = append([]*Middleware{newSetUpstreamHeaders(rp)}, middlewares...)
|
||||
|
||||
mid := NewMiddlewareChain(rp.TargetName, middlewares)
|
||||
|
||||
37
internal/net/http/middleware/middleware_test.go
Normal file
37
internal/net/http/middleware/middleware_test.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
. "github.com/yusing/go-proxy/internal/utils/testing"
|
||||
)
|
||||
|
||||
type testPriority struct {
|
||||
Value int `json:"value"`
|
||||
}
|
||||
|
||||
var test = NewMiddleware[testPriority]()
|
||||
|
||||
func (t testPriority) before(w http.ResponseWriter, r *http.Request) bool {
|
||||
w.Header().Add("Test-Value", strconv.Itoa(t.Value))
|
||||
return true
|
||||
}
|
||||
|
||||
func TestMiddlewarePriority(t *testing.T) {
|
||||
priorities := []int{4, 7, 9, 0}
|
||||
chain := make([]*Middleware, len(priorities))
|
||||
for i, p := range priorities {
|
||||
mid, err := test.New(OptionsRaw{
|
||||
"priority": p,
|
||||
"value": i,
|
||||
})
|
||||
ExpectNoError(t, err)
|
||||
chain[i] = mid
|
||||
}
|
||||
res, err := newMiddlewaresTest(chain, nil)
|
||||
ExpectNoError(t, err)
|
||||
ExpectEqual(t, strings.Join(res.ResponseHeaders["Test-Value"], ","), "3,0,1,2")
|
||||
}
|
||||
@@ -14,6 +14,8 @@ import (
|
||||
var allMiddlewares = map[string]*Middleware{
|
||||
"redirecthttp": RedirectHTTP,
|
||||
|
||||
"oidc": OIDC,
|
||||
|
||||
"request": ModifyRequest,
|
||||
"modifyrequest": ModifyRequest,
|
||||
"response": ModifyResponse,
|
||||
|
||||
59
internal/net/http/middleware/oidc.go
Normal file
59
internal/net/http/middleware/oidc.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/yusing/go-proxy/internal/api/v1/auth"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
)
|
||||
|
||||
type oidcMiddleware struct {
|
||||
AllowedUsers []string `json:"allowed_users"`
|
||||
AllowedGroups []string `json:"allowed_groups"`
|
||||
|
||||
auth auth.Provider
|
||||
authMux *http.ServeMux
|
||||
logoutHandler http.HandlerFunc
|
||||
}
|
||||
|
||||
var OIDC = NewMiddleware[oidcMiddleware]()
|
||||
|
||||
func (amw *oidcMiddleware) finalize() error {
|
||||
if !auth.IsOIDCEnabled() {
|
||||
return E.New("OIDC not enabled but ODIC middleware is used")
|
||||
}
|
||||
authProvider, err := auth.NewOIDCProviderFromEnv()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
authProvider.SetIsMiddleware(true)
|
||||
if len(amw.AllowedUsers) > 0 {
|
||||
authProvider.SetAllowedUsers(amw.AllowedUsers)
|
||||
}
|
||||
if len(amw.AllowedGroups) > 0 {
|
||||
authProvider.SetAllowedGroups(amw.AllowedGroups)
|
||||
}
|
||||
|
||||
amw.authMux = http.NewServeMux()
|
||||
amw.authMux.HandleFunc(auth.OIDCMiddlewareCallbackPath, authProvider.LoginCallbackHandler)
|
||||
amw.authMux.HandleFunc(auth.OIDCLogoutPath, func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
})
|
||||
amw.authMux.HandleFunc("/", authProvider.RedirectLoginPage)
|
||||
amw.logoutHandler = auth.LogoutCallbackHandler(authProvider)
|
||||
amw.auth = authProvider
|
||||
return nil
|
||||
}
|
||||
|
||||
func (amw *oidcMiddleware) before(w http.ResponseWriter, r *http.Request) (proceed bool) {
|
||||
if err := amw.auth.CheckToken(r); err != nil {
|
||||
amw.authMux.ServeHTTP(w, r)
|
||||
return false
|
||||
}
|
||||
if r.URL.Path == auth.OIDCLogoutPath {
|
||||
amw.logoutHandler(w, r)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -127,6 +127,20 @@ func newMiddlewareTest(middleware *Middleware, args *testArgs) (*TestResult, E.E
|
||||
}
|
||||
args.setDefaults()
|
||||
|
||||
mid, setOptErr := middleware.New(args.middlewareOpt)
|
||||
if setOptErr != nil {
|
||||
return nil, setOptErr
|
||||
}
|
||||
|
||||
return newMiddlewaresTest([]*Middleware{mid}, args)
|
||||
}
|
||||
|
||||
func newMiddlewaresTest(middlewares []*Middleware, args *testArgs) (*TestResult, E.Error) {
|
||||
if args == nil {
|
||||
args = new(testArgs)
|
||||
}
|
||||
args.setDefaults()
|
||||
|
||||
req := httptest.NewRequest(args.reqMethod, args.reqURL.String(), args.bodyReader())
|
||||
for k, v := range args.headers {
|
||||
req.Header[k] = v
|
||||
@@ -139,14 +153,8 @@ func newMiddlewareTest(middleware *Middleware, args *testArgs) (*TestResult, E.E
|
||||
rr.parent = http.DefaultTransport
|
||||
}
|
||||
|
||||
rp := reverseproxy.NewReverseProxy(middleware.name, args.upstreamURL, rr)
|
||||
|
||||
mid, setOptErr := middleware.New(args.middlewareOpt)
|
||||
if setOptErr != nil {
|
||||
return nil, setOptErr
|
||||
}
|
||||
|
||||
patchReverseProxy(rp, []*Middleware{mid})
|
||||
rp := reverseproxy.NewReverseProxy("test", args.upstreamURL, rr)
|
||||
patchReverseProxy(rp, middlewares)
|
||||
rp.ServeHTTP(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
|
||||
Reference in New Issue
Block a user