Skip to content

Commit 8d03e66

Browse files
author
Onur Solmaz
committed
feat(auth): add shared-host auth gateway support
1 parent 2bed0a4 commit 8d03e66

17 files changed

Lines changed: 921 additions & 63 deletions

api/instance_proxy.go

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
package main
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"net/http"
7+
"net/http/httputil"
8+
"net/url"
9+
"strings"
10+
11+
"github.com/labstack/echo/v4"
12+
apierrors "k8s.io/apimachinery/pkg/api/errors"
13+
14+
spritzv1 "spritz.sh/operator/api/v1"
15+
)
16+
17+
type instanceProxyConfig struct {
18+
enabled bool
19+
stripPrefix bool
20+
}
21+
22+
func newInstanceProxyConfig() instanceProxyConfig {
23+
return instanceProxyConfig{
24+
enabled: parseBoolEnv("SPRITZ_INSTANCE_PROXY_ENABLED", true),
25+
stripPrefix: parseBoolEnv("SPRITZ_INSTANCE_PROXY_STRIP_PREFIX", true),
26+
}
27+
}
28+
29+
func spritzRouteModelFromEnv() spritzv1.SharedHostRouteModel {
30+
return spritzv1.SharedHostRouteModelFromEnv()
31+
}
32+
33+
func (c instanceProxyConfig) pathPrefix(routeModel spritzv1.SharedHostRouteModel) string {
34+
return routeModel.InstancePathPrefix
35+
}
36+
37+
func (s *server) proxyInstanceWeb(c echo.Context) error {
38+
principal, ok := principalFromContext(c)
39+
if s.auth.enabled() && (!ok || principal.ID == "") {
40+
return writeError(c, http.StatusUnauthorized, "unauthenticated")
41+
}
42+
43+
namespace := s.requestNamespace(c)
44+
if namespace == "" {
45+
namespace = "default"
46+
}
47+
spritz, err := s.getAuthorizedSpritz(c.Request().Context(), principal, namespace, c.Param("name"))
48+
if err != nil {
49+
return s.writeInstanceProxyError(c, err)
50+
}
51+
52+
target, err := s.resolveInstanceProxyTarget(spritz)
53+
if err != nil {
54+
return writeError(c, http.StatusBadGateway, err.Error())
55+
}
56+
57+
prefix := s.instancePrefixForRequest(spritz.Name)
58+
proxy := s.newInstanceReverseProxy(target, prefix)
59+
proxy.ServeHTTP(c.Response(), c.Request())
60+
return nil
61+
}
62+
63+
func (s *server) resolveInstanceProxyTarget(spritz *spritzv1.Spritz) (*url.URL, error) {
64+
if s.instanceProxyTargetResolver != nil {
65+
return s.instanceProxyTargetResolver(spritz)
66+
}
67+
rawURL := spritzv1.WebServiceURLForSpritz(spritz)
68+
if strings.TrimSpace(rawURL) == "" {
69+
return nil, fmt.Errorf("instance web target unavailable")
70+
}
71+
target, err := url.Parse(rawURL)
72+
if err != nil {
73+
return nil, err
74+
}
75+
return target, nil
76+
}
77+
78+
func (s *server) instancePrefixForRequest(name string) string {
79+
return s.routeModel.InstancePath(name)
80+
}
81+
82+
func (s *server) newInstanceReverseProxy(target *url.URL, externalPrefix string) *httputil.ReverseProxy {
83+
proxy := &httputil.ReverseProxy{
84+
Rewrite: func(proxyReq *httputil.ProxyRequest) {
85+
proxyReq.SetURL(target)
86+
proxyReq.SetXForwarded()
87+
req := proxyReq.In
88+
proxyReq.Out.Host = target.Host
89+
proxyReq.Out.URL.Path = s.rewriteInstanceProxyPath(req.URL.Path, externalPrefix)
90+
proxyReq.Out.URL.RawPath = ""
91+
proxyReq.Out.URL.RawQuery = req.URL.RawQuery
92+
proxyReq.Out.Header.Set("X-Forwarded-Host", requestForwardedHost(req))
93+
proxyReq.Out.Header.Set("X-Forwarded-Proto", requestForwardedProto(req))
94+
proxyReq.Out.Header.Set("X-Forwarded-Prefix", externalPrefix)
95+
stripBrowserAuthHeaders(proxyReq.Out.Header, s.auth)
96+
},
97+
ErrorHandler: func(rw http.ResponseWriter, req *http.Request, err error) {
98+
http.Error(rw, err.Error(), http.StatusBadGateway)
99+
},
100+
}
101+
if s.instanceProxyTransport != nil {
102+
proxy.Transport = s.instanceProxyTransport
103+
}
104+
return proxy
105+
}
106+
107+
func (s *server) rewriteInstanceProxyPath(requestPath, externalPrefix string) string {
108+
if !s.instanceProxy.stripPrefix {
109+
if requestPath == "" {
110+
return "/"
111+
}
112+
return requestPath
113+
}
114+
trimmed := strings.TrimPrefix(requestPath, externalPrefix)
115+
if trimmed == "" {
116+
return "/"
117+
}
118+
if !strings.HasPrefix(trimmed, "/") {
119+
return "/" + trimmed
120+
}
121+
return trimmed
122+
}
123+
124+
func stripBrowserAuthHeaders(headers http.Header, auth authConfig) {
125+
headers.Del("Authorization")
126+
headers.Del("X-Auth-Request-User")
127+
headers.Del("X-Auth-Request-Email")
128+
headers.Del("X-Auth-Request-Groups")
129+
headers.Del("X-Auth-Request-Access-Token")
130+
headers.Del("X-Forwarded-Access-Token")
131+
for _, name := range []string{
132+
auth.headerID,
133+
auth.headerEmail,
134+
auth.headerTeams,
135+
auth.headerType,
136+
auth.headerScopes,
137+
} {
138+
if strings.TrimSpace(name) != "" {
139+
headers.Del(name)
140+
}
141+
}
142+
}
143+
144+
func requestForwardedHost(req *http.Request) string {
145+
if forwarded := strings.TrimSpace(req.Header.Get("X-Forwarded-Host")); forwarded != "" {
146+
return forwarded
147+
}
148+
return req.Host
149+
}
150+
151+
func requestForwardedProto(req *http.Request) string {
152+
if forwarded := strings.TrimSpace(req.Header.Get("X-Forwarded-Proto")); forwarded != "" {
153+
return forwarded
154+
}
155+
if req.TLS != nil {
156+
return "https"
157+
}
158+
if forwarded := strings.TrimSpace(req.Header.Get(echo.HeaderXForwardedProto)); forwarded != "" {
159+
return forwarded
160+
}
161+
return "http"
162+
}
163+
164+
func (s *server) writeInstanceProxyError(c echo.Context, err error) error {
165+
switch {
166+
case apierrors.IsNotFound(err):
167+
return writeError(c, http.StatusNotFound, "spritz not found")
168+
case errors.Is(err, errForbidden):
169+
return writeError(c, http.StatusForbidden, "forbidden")
170+
default:
171+
return writeError(c, http.StatusInternalServerError, err.Error())
172+
}
173+
}

0 commit comments

Comments
 (0)