Skip to content

Commit 65535ac

Browse files
committed
backend: auth: Extract ParseClusterAndToken from headlamp.go
This change extracts the ParseClusterAndToken from headlamp.go into the new auth package.
1 parent f9dc688 commit 65535ac

File tree

4 files changed

+125
-30
lines changed

4 files changed

+125
-30
lines changed

backend/cmd/headlamp.go

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ import (
3232
"os"
3333
"path"
3434
"path/filepath"
35-
"regexp"
3635
"runtime"
3736
"strings"
3837
"time"
@@ -860,23 +859,6 @@ func createHeadlampHandler(config *HeadlampConfig) http.Handler {
860859
return r
861860
}
862861

863-
func parseClusterAndToken(r *http.Request) (string, string) {
864-
cluster := ""
865-
re := regexp.MustCompile(`^/clusters/([^/]+)/.*`)
866-
urlString := r.URL.RequestURI()
867-
868-
matches := re.FindStringSubmatch(urlString)
869-
if len(matches) > 1 {
870-
cluster = matches[1]
871-
}
872-
873-
// get token
874-
token := r.Header.Get("Authorization")
875-
token = strings.TrimPrefix(token, "Bearer ")
876-
877-
return cluster, token
878-
}
879-
880862
func getExpiryTime(payload map[string]interface{}) (time.Time, error) {
881863
exp, ok := payload["exp"].(float64)
882864
if !ok {
@@ -1123,7 +1105,7 @@ func (c *HeadlampConfig) OIDCTokenRefreshMiddleware(next http.Handler) http.Hand
11231105
}
11241106

11251107
// parse cluster and token
1126-
cluster, token := parseClusterAndToken(r)
1108+
cluster, token := auth.ParseClusterAndToken(r)
11271109
if c.shouldBypassOIDCRefresh(cluster, token, w, r, span, ctx, start, next) {
11281110
return
11291111
}

backend/cmd/headlamp_test.go

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -978,17 +978,6 @@ func TestGetOidcCallbackURL(t *testing.T) {
978978
}
979979
}
980980

981-
func TestParseClusterAndToken(t *testing.T) {
982-
ctx := context.Background()
983-
req, err := http.NewRequestWithContext(ctx, "GET", "/clusters/test-cluster/api", nil)
984-
require.NoError(t, err)
985-
req.Header.Set("Authorization", "Bearer test-token")
986-
987-
cluster, token := parseClusterAndToken(req)
988-
assert.Equal(t, "test-cluster", cluster)
989-
assert.Equal(t, "test-token", token)
990-
}
991-
992981
func TestIsTokenAboutToExpire(t *testing.T) {
993982
// Token that expires in 4 minutes
994983
header := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9."

backend/pkg/auth/auth.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ package auth
1919
import (
2020
"encoding/base64"
2121
"encoding/json"
22+
"net/http"
23+
"regexp"
24+
"strings"
2225
)
2326

2427
// DecodeBase64JSON decodes a base64 URL-encoded JSON string into a map.
@@ -35,3 +38,40 @@ func DecodeBase64JSON(base64JSON string) (map[string]interface{}, error) {
3538

3639
return payloadMap, nil
3740
}
41+
42+
// clusterPathRegex matches /clusters/<cluster>/...
43+
var clusterPathRegex = regexp.MustCompile(`^/clusters/([^/]+)/.*`)
44+
45+
// kubeContextNameRegex matches valid context names following the RFC 1123 standard.
46+
var kubeContextNameRegex = regexp.MustCompile(`^[a-zA-Z0-9._-]+$`)
47+
48+
// bearerTokenRegex matches valid bearer tokens as specified by RFC 6750:
49+
// https://datatracker.ietf.org/doc/html/rfc6750#section-2.1
50+
var bearerTokenRegex = regexp.MustCompile(`^[\x21-\x7E]+$`)
51+
52+
// ParseClusterAndToken extracts the cluster name from the URL path and
53+
// the Bearer token from the Authorization header of the HTTP request.
54+
func ParseClusterAndToken(r *http.Request) (string, string) {
55+
cluster := ""
56+
matches := clusterPathRegex.FindStringSubmatch(r.URL.Path)
57+
58+
if len(matches) > 1 && kubeContextNameRegex.MatchString(matches[1]) {
59+
cluster = matches[1]
60+
}
61+
62+
token := strings.TrimSpace(r.Header.Get("Authorization"))
63+
if strings.Contains(token, ",") {
64+
return cluster, ""
65+
}
66+
67+
const bearerPrefix = "Bearer "
68+
if strings.HasPrefix(strings.ToLower(token), strings.ToLower(bearerPrefix)) {
69+
token = strings.TrimSpace(token[len(bearerPrefix):])
70+
}
71+
72+
if token != "" && !bearerTokenRegex.MatchString(token) {
73+
return cluster, ""
74+
}
75+
76+
return cluster, token
77+
}

backend/pkg/auth/auth_test.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ limitations under the License.
1717
package auth_test
1818

1919
import (
20+
"context"
21+
"net/http"
2022
"reflect"
2123
"testing"
2224

@@ -89,3 +91,85 @@ func TestDecodeBase64JSON(t *testing.T) {
8991
})
9092
}
9193
}
94+
95+
var parseClusterAndTokenTests = []struct {
96+
name string
97+
url string
98+
authHeader string
99+
wantCluster string
100+
wantToken string
101+
}{
102+
{
103+
name: "standard case",
104+
url: "/clusters/test-cluster/api",
105+
authHeader: "Bearer test-token",
106+
wantCluster: "test-cluster",
107+
wantToken: "test-token",
108+
},
109+
{
110+
name: "lowercase bearer",
111+
url: "/clusters/abc/api",
112+
authHeader: "bearer token-lowercase",
113+
wantCluster: "abc",
114+
wantToken: "token-lowercase",
115+
},
116+
{
117+
name: "uppercase bearer",
118+
url: "/clusters/xyz/api",
119+
authHeader: "BEARER token-upper",
120+
wantCluster: "xyz",
121+
wantToken: "token-upper",
122+
},
123+
{
124+
name: "extra spaces before bearer",
125+
url: "/clusters/extra/api",
126+
authHeader: " Bearer spaced-token",
127+
wantCluster: "extra",
128+
wantToken: "spaced-token",
129+
},
130+
{
131+
name: "not a clusters path",
132+
url: "/no-clusters-prefix/api",
133+
authHeader: "Bearer test-token",
134+
wantCluster: "",
135+
wantToken: "test-token",
136+
},
137+
{
138+
name: "multiple bearer tokens",
139+
url: "/clusters/test/api",
140+
authHeader: "Bearer xxx, Bearer yyy",
141+
wantCluster: "test",
142+
wantToken: "",
143+
},
144+
{
145+
name: "no cluster in path",
146+
url: "/clusters/",
147+
authHeader: "Bearer some-token",
148+
wantCluster: "",
149+
wantToken: "some-token",
150+
},
151+
}
152+
153+
func TestParseClusterAndToken(t *testing.T) {
154+
for _, tt := range parseClusterAndTokenTests {
155+
t.Run(tt.name, func(t *testing.T) {
156+
req, err := http.NewRequestWithContext(context.Background(), "GET", tt.url, nil)
157+
if err != nil {
158+
t.Fatalf("ParseClusterAndToken() error = %v", err)
159+
}
160+
161+
if tt.authHeader != "" {
162+
req.Header.Set("Authorization", tt.authHeader)
163+
}
164+
165+
cluster, token := auth.ParseClusterAndToken(req)
166+
if cluster != tt.wantCluster {
167+
t.Errorf("ParseClusterAndToken() got cluster %q, want %q", cluster, tt.wantCluster)
168+
}
169+
170+
if token != tt.wantToken {
171+
t.Errorf("ParseClusterAndToken() got token = %q, want %q", token, tt.wantToken)
172+
}
173+
})
174+
}
175+
}

0 commit comments

Comments
 (0)