Skip to content

Commit 85c90c0

Browse files
author
John Wregglesworth
committed
Add a --disable-auth flag to disable authentication. Defaults to false.
1 parent c3ceb0b commit 85c90c0

File tree

2 files changed

+171
-42
lines changed

2 files changed

+171
-42
lines changed

main.go

Lines changed: 65 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ type VICEProxy struct {
4949
checkResourceAccessBase string // The base URL for the check-resource-access service.
5050
sessionStore *sessions.CookieStore // The backend session storage.
5151
ssoClient http.Client // The HTTP client for back-channel requests to the IDP.
52+
disableAuth bool // If true, authentication and authorization are disabled.
5253
}
5354

5455
// Analysis contains the ID for the Analysis, which gets used as the resource
@@ -579,6 +580,47 @@ func (c *VICEProxy) GetFrontendHost() (string, error) {
579580
return svcURL.Host, nil
580581
}
581582

583+
// authenticateAndAuthorize validates the user's session and checks if they have permission
584+
// to access the resource. Returns the username on success, or an error on failure.
585+
func (c *VICEProxy) authenticateAndAuthorize(w http.ResponseWriter, r *http.Request) (string, error) {
586+
// Get the username from the cookie
587+
session, err := c.sessionStore.Get(r, sessionName)
588+
if err != nil {
589+
return "", errors.Wrap(err, "failed to get session")
590+
}
591+
592+
// Check if the session contains a username
593+
usernameValue, ok := session.Values[sessionKey]
594+
if !ok || usernameValue == nil {
595+
return "", errors.New("no session found")
596+
}
597+
598+
username, ok := usernameValue.(string)
599+
if !ok || username == "" {
600+
return "", errors.New("username was empty or invalid")
601+
}
602+
log.Infof("authenticated user: %s", username)
603+
604+
// Check to make sure the user can access the resource.
605+
allowed, err := c.IsAllowed(username, c.resourceName)
606+
if !allowed || err != nil {
607+
if err != nil {
608+
return "", errors.Wrap(err, "access denied")
609+
}
610+
return "", errors.New("access denied")
611+
}
612+
log.Infof("user %s authorized for resource %s", username, c.resourceName)
613+
614+
// CRITICAL: Don't reset session for WebSocket upgrades (would corrupt the upgrade handshake)
615+
if !c.isWebsocket(r) {
616+
if err = c.ResetSessionExpiration(w, r); err != nil {
617+
return "", errors.Wrap(err, "error resetting session expiration")
618+
}
619+
}
620+
621+
return username, nil
622+
}
623+
582624
// Proxy returns a handler that can support both websockets and http requests.
583625
func (c *VICEProxy) Proxy() (http.Handler, error) {
584626
rp, err := c.ReverseProxy()
@@ -594,51 +636,22 @@ func (c *VICEProxy) Proxy() (http.Handler, error) {
594636
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
595637
log.Infof("handling request for %s from remote address %s", r.URL.String(), r.RemoteAddr)
596638

597-
//Get the username from the cookie
598-
session, err := c.sessionStore.Get(r, sessionName)
599-
if err != nil {
600-
err = errors.Wrap(err, "failed to get session")
601-
log.Errorf("session error: %v", err)
602-
http.Error(w, err.Error(), http.StatusInternalServerError)
603-
return
604-
}
605-
606-
username := session.Values[sessionKey].(string)
607-
if username == "" {
608-
err = errors.Wrap(err, "username was empty")
609-
log.Errorf("authentication error: %v", err)
610-
http.Error(w, err.Error(), http.StatusForbidden)
611-
return
612-
}
613-
log.Infof("authenticated user: %s", username)
614-
615-
// Check to make sure the user can access the resource.
616-
allowed, err := c.IsAllowed(username, c.resourceName)
617-
if !allowed || err != nil {
639+
// Conditionally perform authentication and authorization
640+
if !c.disableAuth {
641+
username, err := c.authenticateAndAuthorize(w, r)
618642
if err != nil {
619-
err = errors.Wrap(err, "access denied")
620-
} else {
621-
err = errors.New("access denied")
643+
log.Errorf("auth/authz error: %v", err)
644+
http.Error(w, err.Error(), http.StatusForbidden)
645+
return
622646
}
623-
log.Errorf("authorization error for user %s: %v", username, err)
624-
http.Error(w, err.Error(), http.StatusForbidden)
625-
return
647+
log.Debugf("request authorized for user: %s", username)
648+
} else {
649+
log.Debug("authentication disabled, allowing unauthenticated access")
626650
}
627-
log.Infof("user %s authorized for resource %s", username, c.resourceName)
628651

629652
// Override the X-Forwarded-Host header.
630653
r.Header.Set("X-Forwarded-Host", frontendHost)
631654

632-
// CRITICAL: Don't reset session for WebSocket upgrades (would corrupt the upgrade handshake)
633-
if !c.isWebsocket(r) {
634-
if err = c.ResetSessionExpiration(w, r); err != nil {
635-
err = errors.Wrap(err, "error resetting session expiration")
636-
log.Errorf("session expiration error: %v", err)
637-
http.Error(w, err.Error(), http.StatusInternalServerError)
638-
return
639-
}
640-
}
641-
642655
// The reverse proxy handles both HTTP and WebSocket upgrade requests transparently
643656
log.Infof("proxying request to %s%s", c.backendURL, r.URL.Path)
644657
rp.ServeHTTP(w, r)
@@ -681,6 +694,7 @@ func main() {
681694
encodedReadTimeout = flag.String("read-timeout", "48h", "The maximum duration for reading the entire request, including the body.")
682695
encodedWriteTimeout = flag.String("write-timeout", "48h", "The maximum duration before timing out writes of the response.")
683696
encodedIdleTimeout = flag.String("idle-timeout", "5000s", "The maximum amount of time to wait for the next request when keep-alives are enabled.")
697+
disableAuth = flag.Bool("disable-auth", false, "Disable authentication and authorization. When true, allows unauthenticated access to the proxied application.")
684698
)
685699

686700
flag.Var(&corsOrigins, "allowed-origins", "List of allowed origins, separated by commas.")
@@ -721,6 +735,7 @@ func main() {
721735
log.Infof("read timeout is %s", *encodedReadTimeout)
722736
log.Infof("write timeout is %s", *encodedWriteTimeout)
723737
log.Infof("idle timeout is %s", *encodedIdleTimeout)
738+
log.Infof("authentication disabled: %v", *disableAuth)
724739

725740
for _, c := range corsOrigins {
726741
log.Infof("Origin: %s\n", c)
@@ -778,6 +793,7 @@ func main() {
778793
checkResourceAccessBase: *checkResourceAccessBase,
779794
sessionStore: sessionStore,
780795
ssoClient: *client,
796+
disableAuth: *disableAuth,
781797
}
782798

783799
resourceName, err := p.getResourceName(*externalID)
@@ -793,11 +809,18 @@ func main() {
793809

794810
r := mux.NewRouter()
795811

796-
// If the query contains a ticket in the query params, then it needs to be
797-
// validated.
812+
// Health check endpoint - always available
798813
r.PathPrefix("/url-ready").HandlerFunc(p.URLIsReady)
799-
r.PathPrefix("/").Queries("code", "").Handler(http.HandlerFunc(p.HandleAuthorizationCode))
800-
r.PathPrefix("/").MatcherFunc(p.Session).Handler(http.HandlerFunc(p.RequireKeycloakAuth))
814+
815+
// Conditionally add authentication routes based on --disable-auth flag
816+
if !*disableAuth {
817+
// If the query contains a code parameter, handle the OAuth authorization code
818+
r.PathPrefix("/").Queries("code", "").Handler(http.HandlerFunc(p.HandleAuthorizationCode))
819+
// If the request doesn't have a valid session, redirect to Keycloak for authentication
820+
r.PathPrefix("/").MatcherFunc(p.Session).Handler(http.HandlerFunc(p.RequireKeycloakAuth))
821+
}
822+
823+
// Proxy all requests to the backend
801824
r.PathPrefix("/").Handler(proxy)
802825

803826
c := cors.New(cors.Options{

main_test.go

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,23 @@
11
package main
22

33
import (
4+
"crypto/rand"
5+
"net/http"
6+
"net/http/httptest"
47
"testing"
58

9+
"github.com/gorilla/sessions"
610
"github.com/stretchr/testify/assert"
711
)
812

913
// getVICEProxy returns a VICEProxy instance with some default settnigs for testing. Some fields that aren't being used
1014
// during testing are omitted.
1115
func getVICEProxy() *VICEProxy {
16+
// Create a session store for testing
17+
authkey := make([]byte, 64)
18+
rand.Read(authkey)
19+
sessionStore := sessions.NewCookieStore(authkey)
20+
1221
return &VICEProxy{
1322
keycloakBaseURL: "https://keycloak.example.org",
1423
keycloakRealm: "example",
@@ -19,6 +28,8 @@ func getVICEProxy() *VICEProxy {
1928
wsbackendURL: "http://localhost:8888",
2029
getAnalysisIDBase: "http://get-analysis-id",
2130
checkResourceAccessBase: "http://check-resource-access",
31+
sessionStore: sessionStore,
32+
disableAuth: false,
2233
}
2334
}
2435

@@ -65,3 +76,98 @@ func TestKeycloakURL(t *testing.T) {
6576
})
6677
}
6778
}
79+
80+
func TestProxyWithAuthDisabled(t *testing.T) {
81+
assert := assert.New(t)
82+
83+
// Create a test backend server
84+
backendCalled := false
85+
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
86+
backendCalled = true
87+
w.WriteHeader(http.StatusOK)
88+
w.Write([]byte("backend response"))
89+
}))
90+
defer backend.Close()
91+
92+
// Create proxy with auth disabled
93+
proxy := getVICEProxy()
94+
proxy.disableAuth = true
95+
proxy.backendURL = backend.URL
96+
97+
// Get the proxy handler
98+
proxyHandler, err := proxy.Proxy()
99+
assert.NoError(err, "creating proxy handler should not error")
100+
101+
// Create a test request without authentication
102+
req := httptest.NewRequest("GET", "http://example.com/test", nil)
103+
w := httptest.NewRecorder()
104+
105+
// Execute the request
106+
proxyHandler.ServeHTTP(w, req)
107+
108+
// Verify the backend was called and request succeeded
109+
assert.True(backendCalled, "backend should have been called")
110+
assert.Equal(http.StatusOK, w.Code, "request should succeed without authentication")
111+
}
112+
113+
func TestProxyWithAuthEnabled(t *testing.T) {
114+
assert := assert.New(t)
115+
116+
// Create a test backend server
117+
backendCalled := false
118+
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
119+
backendCalled = true
120+
w.WriteHeader(http.StatusOK)
121+
w.Write([]byte("backend response"))
122+
}))
123+
defer backend.Close()
124+
125+
// Create proxy with auth enabled (default)
126+
proxy := getVICEProxy()
127+
proxy.disableAuth = false
128+
proxy.backendURL = backend.URL
129+
130+
// Get the proxy handler
131+
proxyHandler, err := proxy.Proxy()
132+
assert.NoError(err, "creating proxy handler should not error")
133+
134+
// Create a test request without authentication
135+
req := httptest.NewRequest("GET", "http://example.com/test", nil)
136+
w := httptest.NewRecorder()
137+
138+
// Execute the request
139+
proxyHandler.ServeHTTP(w, req)
140+
141+
// Verify the backend was NOT called and request was rejected
142+
assert.False(backendCalled, "backend should not have been called without authentication")
143+
assert.Equal(http.StatusForbidden, w.Code, "request should be rejected without authentication")
144+
}
145+
146+
func TestAuthenticateAndAuthorizeWithoutSession(t *testing.T) {
147+
assert := assert.New(t)
148+
149+
proxy := getVICEProxy()
150+
proxy.resourceName = "test-resource"
151+
152+
req := httptest.NewRequest("GET", "http://example.com/test", nil)
153+
w := httptest.NewRecorder()
154+
155+
// Attempt authentication without a valid session
156+
username, err := proxy.authenticateAndAuthorize(w, req)
157+
158+
// Should fail with empty username and error
159+
assert.Empty(username, "username should be empty without a valid session")
160+
assert.Error(err, "should return an error without a valid session")
161+
}
162+
163+
func TestDisableAuthFlag(t *testing.T) {
164+
assert := assert.New(t)
165+
166+
// Test that disableAuth field defaults to false
167+
proxy := getVICEProxy()
168+
assert.False(proxy.disableAuth, "disableAuth should default to false")
169+
170+
// Test that disableAuth can be set to true
171+
proxy.disableAuth = true
172+
assert.True(proxy.disableAuth, "disableAuth should be settable to true")
173+
}

0 commit comments

Comments
 (0)