1 package authproxy
2
3 import (
4 "bytes"
5 "context"
6 "encoding/gob"
7 "io"
8 "net/http"
9 "net/http/httputil"
10 "net/url"
11 "os"
12 "regexp"
13 "strconv"
14 "strings"
15 "time"
16
17 "github.com/gin-contrib/cors"
18 "github.com/gin-contrib/requestid"
19 "github.com/gin-contrib/sessions"
20
21 "github.com/gin-gonic/gin"
22 "github.com/go-logr/logr"
23
24 "edge-infra.dev/pkg/edge/api/middleware"
25 "edge-infra.dev/pkg/edge/audit"
26 "edge-infra.dev/pkg/edge/auth-proxy/handlers"
27 "edge-infra.dev/pkg/edge/auth-proxy/interceptor"
28
29 edgestore "edge-infra.dev/pkg/edge/auth-proxy/store"
30 "edge-infra.dev/pkg/edge/auth-proxy/types"
31 logger "edge-infra.dev/pkg/lib/logging"
32 "edge-infra.dev/pkg/lib/runtime/manager"
33 "edge-infra.dev/pkg/lib/uuid"
34 "edge-infra.dev/pkg/x/tonic"
35 )
36
37 const (
38 accessControlAllowOriginHeader = "Access-Control-Allow-Origin"
39 credentialsHeader = "Credentials"
40 authorizationHeader = "Authorization"
41 setCookieHeader = "Set-Cookie"
42 emergencyAccessPathRegex = `^/ea/`
43 )
44
45 func init() {
46
47 gob.Register(time.Time{})
48 }
49
50 func RunManager() {
51 log := logger.NewLogger().Logger.WithName("auth-proxy")
52
53 cfg, err := NewConfig(os.Args[1:])
54 if err != nil {
55 log.Error(err, "failed to parse startup configuration")
56 os.Exit(1)
57 }
58
59 db, err := cfg.connectDatabase()
60 if err != nil {
61 log.Error(err, "failed to parse startup configuration")
62 os.Exit(1)
63 }
64
65 mgr, err := manager.New(manager.Options{MetricsBindAddress: cfg.MetricsPort})
66 if err != nil {
67 log.Error(err, "failed to create a new manager for auth-proxy")
68 os.Exit(1)
69 }
70
71 auditLog := audit.New("auth-proxy")
72
73 pgstore, err := edgestore.NewStore(db, log, auditLog, []byte(cfg.SessionSecret))
74 if err != nil {
75 log.Error(err, "failed to create a new manager for auth-proxy")
76 os.Exit(1)
77 }
78
79 sameSite := http.SameSiteNoneMode
80 if cfg.Mode == gin.ReleaseMode {
81 sameSite = http.SameSiteStrictMode
82 }
83
84
85 if err := pgstore.MaxLength(cfg.SessionLength); err != nil {
86 log.Error(err, "failed to set session value maximum length")
87 os.Exit(1)
88 }
89
90 pgstore.Options(sessions.Options{
91 Path: "/",
92 HttpOnly: true,
93 SameSite: sameSite,
94 Secure: true,
95 MaxAge: 60 * 14,
96 })
97
98
99 quit, done := pgstore.Cleanup(10 * time.Minute)
100 defer pgstore.StopCleanup(quit, done)
101
102 uiProxyServer := setupServer(log, auditLog, cfg, pgstore)
103
104 if err := mgr.Add(uiProxyServer); err != nil {
105 log.Error(err, "failed to add ui proxy server")
106 os.Exit(1)
107 }
108
109 if err := mgr.Start(context.Background()); err != nil {
110 log.Error(err, "auth-proxy mgr start failed")
111 os.Exit(1)
112 }
113 }
114
115 func corsMiddleware(allowedOrigins []string) gin.HandlerFunc {
116 corsConfig := cors.DefaultConfig()
117 corsConfig.AllowAllOrigins = false
118 corsConfig.ExposeHeaders = append(corsConfig.AllowHeaders, accessControlAllowOriginHeader, credentialsHeader, authorizationHeader, setCookieHeader)
119 corsConfig.AllowOrigins = allowedOrigins
120 corsConfig.AllowHeaders = append(corsConfig.AllowHeaders, accessControlAllowOriginHeader, credentialsHeader, authorizationHeader, setCookieHeader)
121 corsConfig.AllowCredentials = true
122 return cors.New(corsConfig)
123 }
124
125 func setupServer(log logr.Logger, auditLog *audit.Sink, cfg *ProxyConfig, store edgestore.Store) *tonic.Server {
126 allowedOrigins := cfg.allowedOrigins()
127 log.Info("configuring server with these allowed origins", "origins", allowedOrigins)
128 uiProxyServer := tonic.NewWithOptions(cfg.UIProxyPort, cfg.Mode).
129 SetLogger(log).
130 SetMiddlewares(middleware.SetRequestContext(), requestid.New(), middleware.RequestLogger(log), gin.Recovery(), corsMiddleware(allowedOrigins), sessions.Sessions(edgestore.SessionIdentifier, store)).
131 SetRoutes(tonic.Route{
132 Path: "/*proxyPath",
133 Action: tonic.MethodAny,
134 Handlers: []gin.HandlerFunc{proxyHandler(log, auditLog, cfg)},
135 }).With404Route()
136
137 return uiProxyServer
138 }
139
140 func proxyHandler(log logr.Logger, auditLog *audit.Sink, cfg *ProxyConfig) gin.HandlerFunc {
141 return func(c *gin.Context) {
142 proxy(c, log, auditLog, cfg)
143 }
144 }
145
146 func proxy(c *gin.Context, log logr.Logger, auditLog *audit.Sink, cfg *ProxyConfig) {
147 session := sessions.Default(c)
148 apiEndpoint := getAPIEndpoint(c, log, cfg)
149 correlationID := uuid.New().UUID
150 auditLog.SetCorrelationID(correlationID)
151
152 if filterEmergencyAccessURL(apiEndpoint.Path) && !validSession(session) {
153
154
155 log.Info("Abandoning emergencyaccess request due to invalid session", "url", apiEndpoint)
156 c.AbortWithStatus(http.StatusForbidden)
157 return
158 }
159
160 log.Info("proxying request...", "url", apiEndpoint)
161 handler := handlers.New(c, log, session, cfg.TokenSecret, cfg.SessionDuration, false, correlationID)
162 proxy := httputil.NewSingleHostReverseProxy(apiEndpoint)
163 proxy.Rewrite = func(r *httputil.ProxyRequest) {
164 r.In.Host = apiEndpoint.Host
165 r.Out.URL = apiEndpoint
166 r.Out.RequestURI = r.Out.URL.String()
167 r.Out.RemoteAddr = r.In.RemoteAddr
168 r.Out.Header.Set(audit.AuditIdentifierHeader, correlationID)
169
170
171 if r.Out.Body != nil {
172 body, err := io.ReadAll(r.Out.Body)
173 if err != nil {
174 log.Error(err, "error reading proxied response")
175 return
176 }
177 if err := r.Out.Body.Close(); err != nil {
178 log.Error(err, "error closing proxy response body")
179 return
180 }
181 r.Out.Body = io.NopCloser((bytes.NewBuffer(body)))
182 intercpt := interceptor.New(c.ClientIP(), correlationID, session)
183 intercpt.Query("schemaIntrospection", handler.Default, `\{"__schema":\{`, true)
184 intercpt.Query("typeIntrospection", handler.Default, `\{"__type":\{`, true)
185 intercpt.Query("typenameIntrospection", handler.Default, `\{"__typename":\{`, true)
186 intercpt.Mutation("login", handler.Login, `\{"login":\{`, true)
187 intercpt.Mutation("loginWithOkta", handler.LoginWithOkta, `\{"loginWithOkta":\{`, true)
188 intercpt.Query("tenantsForOktaToken", handler.Default, `\{"tenantsForOktaToken":\{`, true)
189 intercpt.Mutation("logout", handler.Logout, `\{"logout":true\}`, true)
190 intercpt.Mutation("sessionRefresh", handler.SessionRefresh, `\{"sessionRefresh":`, validSession(session))
191 intercpt.Query("sessionUserEdgeRole", handler.SessionDefault, `\{"sessionUserEdgeRole":`, validSession(session))
192 intercpt.Path("sessionUserDetails", handler.SessionUserDetails, emergencyAccessPathRegex, true)
193 intercpt.Default(handler.SessionDefault, true)
194 req, _, err := intercpt.Intercept(r.Out, body, true)
195 if err != nil {
196 log.Error(err, "an error occurred intercepting request body")
197 return
198 }
199 r.Out = req
200 }
201 }
202 proxy.Transport = New(apiEndpoint)
203 proxy.Director = nil
204 proxy.ModifyResponse = func(r *http.Response) error {
205
206
207
208 if filterEmergencyAccessURL(r.Request.URL.Path) {
209 return nil
210 }
211
212 r.Header.Del(accessControlAllowOriginHeader)
213 body, err := io.ReadAll(r.Body)
214 if err != nil {
215 log.Error(err, "error reading proxied response")
216 return err
217 }
218 if err := r.Body.Close(); err != nil {
219 log.Error(err, "error closing proxy response body")
220 return err
221 }
222 intercpt := interceptor.New(c.ClientIP(), correlationID, session)
223 intercpt.Query("schemaIntrospection", handler.Default, `\{"__schema":\{`, true)
224 intercpt.Query("typeIntrospection", handler.Default, `\{"__type":\{`, true)
225 intercpt.Query("typenameIntrospection", handler.Default, `\{"__typename":\{`, true)
226 intercpt.Mutation("login", handler.Login, `\{"login":\{`, true)
227 intercpt.Query("tenantsForOktaToken", handler.Default, `\{"tenantsForOktaToken":\{`, true)
228 intercpt.Mutation("loginWithOkta", handler.LoginWithOkta, `\{"loginWithOkta":\{`, true)
229 intercpt.Mutation("logout", handler.Logout, `\{"logout":true\}`, true)
230 intercpt.Mutation("sessionRefresh", handler.SessionRefresh, `\{"sessionRefresh":`, validSession(session))
231 intercpt.Query("sessionUserEdgeRole", handler.SessionUserEdgeRole, `\{"sessionUserEdgeRole":`, validSession(session))
232 intercpt.Default(handler.SessionDefault, true)
233 reqHeader := r.Request.Clone(r.Request.Context())
234 hdr, responseBody, err := intercpt.Intercept(reqHeader, body, false)
235 if err != nil {
236 return err
237 }
238 r.Request = hdr
239 r.Body = io.NopCloser(bytes.NewReader(responseBody))
240 r.ContentLength = int64(len(responseBody))
241 r.Header.Set("Content-Length", strconv.Itoa(len(responseBody)))
242 return err
243 }
244 proxy.ServeHTTP(c.Writer, c.Request)
245 }
246
247 func filterEmergencyAccessURL(path string) bool {
248 re := regexp.MustCompile(emergencyAccessPathRegex)
249 return re.MatchString(path)
250 }
251
252 func validSession(session sessions.Session) bool {
253 expiresAt := session.Get(types.SessionExpirationField)
254 if expiresAt != nil {
255 expirationTime := expiresAt.(time.Time)
256 return !expirationTime.Before(time.Now().UTC()) && session.Get(types.SessionIDField) != nil
257 }
258 return false
259 }
260
261 func getAPIEndpoint(c *gin.Context, log logr.Logger, cfg *ProxyConfig) *url.URL {
262 cfgEndpoint := cfg.BffEndpoint
263 path := c.Request.URL.Path
264
265 if strings.HasPrefix(c.Request.URL.Path, "/api/ea/") {
266 cfgEndpoint = cfg.EAGatewayEndpoint
267 path = strings.TrimPrefix(path, "/api/")
268 }
269 apiEndpointString, err := url.JoinPath(cfgEndpoint, path)
270 if err != nil {
271 log.Error(err, "Error joining url and path for reverse proxy", "url", cfgEndpoint, "path", path)
272 return nil
273 }
274 apiEndpoint, err := url.Parse(apiEndpointString)
275 if err != nil {
276 log.Error(err, "Error parsing Edge API target URL for reverse proxy", "url", apiEndpoint, "path", path)
277 return nil
278 }
279 return apiEndpoint
280 }
281
View as plain text