Skip to content

Commit 4f2010e

Browse files
Merge pull request #25 from ls1intum/add-authorization-support
Switch to reader and writer auth
2 parents 2708c59 + 66fbf2b commit 4f2010e

File tree

9 files changed

+87
-50
lines changed

9 files changed

+87
-50
lines changed

chart/Chart.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ description: A Gradle Build Cache server with Redis backend for Theia IDE deploy
55
type: application
66

77
# Chart version - bump for breaking changes
8-
version: 0.3.0
8+
version: 0.3.1
99

1010
# Application version - matches the cache server version
1111
appVersion: "0.1.0"

chart/templates/auth-secrets.yaml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{{- if and .Values.enabled .Values.auth.enabled }}
2+
# Read-only cache credentials (for Theia IDE / students)
3+
apiVersion: v1
4+
kind: Secret
5+
metadata:
6+
name: {{ .Release.Name }}-cache-reader
7+
labels:
8+
app: {{ .Release.Name }}
9+
role: reader
10+
type: kubernetes.io/basic-auth
11+
data:
12+
username: {{ .Values.auth.reader.username | b64enc | quote }}
13+
password: {{ .Values.auth.reader.password | b64enc | quote }}
14+
---
15+
# Read-write cache credentials (for CI/CD, admin, cache pre-warming)
16+
apiVersion: v1
17+
kind: Secret
18+
metadata:
19+
name: {{ .Release.Name }}-cache-writer
20+
labels:
21+
app: {{ .Release.Name }}
22+
role: writer
23+
type: kubernetes.io/basic-auth
24+
data:
25+
username: {{ .Values.auth.writer.username | b64enc | quote }}
26+
password: {{ .Values.auth.writer.password | b64enc | quote }}
27+
{{- end }}

chart/templates/configmap.yaml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,10 @@ data:
2828
auth:
2929
enabled: {{ .Values.auth.enabled }}
3030
{{- if .Values.auth.enabled }}
31-
users:
32-
- username: {{ .Values.auth.username | quote }}
31+
reader:
32+
username: {{ .Values.auth.reader.username | quote }}
33+
writer:
34+
username: {{ .Values.auth.writer.username | quote }}
3335
{{- end }}
3436
3537
logging:

chart/templates/deployment.yaml

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,16 @@ spec:
4949
name: {{ .Release.Name }}-redis-secret
5050
key: redis-password
5151
{{- if .Values.auth.enabled }}
52-
- name: CACHE_PASSWORD
52+
- name: CACHE_READER_PASSWORD
5353
valueFrom:
5454
secretKeyRef:
55-
name: {{ .Release.Name }}-redis-secret
56-
key: cache-password
55+
name: {{ .Release.Name }}-cache-reader
56+
key: password
57+
- name: CACHE_WRITER_PASSWORD
58+
valueFrom:
59+
secretKeyRef:
60+
name: {{ .Release.Name }}-cache-writer
61+
key: password
5762
{{- end }}
5863
args:
5964
- --config
Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,4 @@ data:
1515
{{- else }}
1616
redis-password: {{ randAlphaNum 32 | b64enc}}
1717
{{- end }}
18-
cache-password: {{ .Values.auth.password | b64enc | quote}}
1918
{{- end }}

chart/values.yaml

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,21 @@ image:
99
tag: "main"
1010
pullPolicy: IfNotPresent
1111

12-
# Authentication credentials
12+
# Authentication credentials (role-based)
13+
# reader: read-only access (for Theia IDE / students)
14+
# writer: read-write access (for CI/CD, admin, cache pre-warming)
1315
auth:
1416
enabled: true
15-
username: "gradle"
16-
# IMPORTANT: Change this password in production!
17-
password: "changeme"
17+
reader:
18+
username: "reader"
19+
# IMPORTANT: Change this password in production!
20+
# optimaly from github envrionment secrets
21+
password: "changeme-reader"
22+
writer:
23+
username: "writer"
24+
# IMPORTANT: Change this password in production!
25+
# optimaly from github envrionment secrets
26+
password: "changeme-writer"
1827

1928
# Resource limits
2029
resources:

src/internal/config/config.go

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,9 @@ type CacheConfig struct {
4242
}
4343

4444
type AuthConfig struct {
45-
Enabled bool `mapstructure:"enabled"`
46-
Users []UserAuth `mapstructure:"users"`
45+
Enabled bool `mapstructure:"enabled"`
46+
Reader UserAuth `mapstructure:"reader"`
47+
Writer UserAuth `mapstructure:"writer"`
4748
}
4849

4950
type UserAuth struct {
@@ -106,37 +107,29 @@ func Load(configPath string) (*Config, error) {
106107
// Bind specific environment variables
107108
v.BindEnv("storage.password", "REDIS_PASSWORD")
108109

109-
v.BindEnv("auth.users.0.password", "CACHE_PASSWORD")
110+
v.BindEnv("auth.reader.password", "CACHE_READER_PASSWORD")
111+
v.BindEnv("auth.writer.password", "CACHE_WRITER_PASSWORD")
112+
110113
v.BindEnv("sentry.dsn", "SENTRY_DSN")
111114

112115
var cfg Config
113116
if err := v.Unmarshal(&cfg); err != nil {
114117
return nil, fmt.Errorf("failed to unmarshal config: %w", err)
115118
}
116119

117-
// Handle CACHE_PASSWORD environment variable for default user
118-
if cachePassword := v.GetString("CACHE_PASSWORD"); cachePassword != "" {
119-
if len(cfg.Auth.Users) > 0 {
120-
cfg.Auth.Users[0].Password = cachePassword
121-
}
122-
}
123-
124120
return &cfg, nil
125121
}
126122

127123
func (c *Config) Validate() error {
128124
if c.Storage.Addr == "" {
129125
return fmt.Errorf("storage.addr is required")
130126
}
131-
if c.Auth.Enabled && len(c.Auth.Users) == 0 {
132-
return fmt.Errorf("auth.users is required when auth is enabled")
133-
}
134-
for i, user := range c.Auth.Users {
135-
if user.Username == "" {
136-
return fmt.Errorf("auth.users[%d].username is required", i)
127+
if c.Auth.Enabled {
128+
if c.Auth.Reader.Username == "" || c.Auth.Reader.Password == "" {
129+
return fmt.Errorf("auth.reader.username and auth.reader.password are required when auth is enabled")
137130
}
138-
if user.Password == "" {
139-
return fmt.Errorf("auth.users[%d].password is required", i)
131+
if c.Auth.Writer.Username == "" || c.Auth.Writer.Password == "" {
132+
return fmt.Errorf("auth.writer.username and auth.writer.password are required when auth is enabled")
140133
}
141134
}
142135
if c.Server.TLS.Enabled {

src/internal/middleware/auth.go

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,8 @@ import (
88
"github.com/kevingruber/gradle-cache/internal/config"
99
)
1010

11-
// BasicAuth creates a middleware that validates HTTP Basic Authentication.
12-
func BasicAuth(users []config.UserAuth) gin.HandlerFunc {
13-
// Build a map for O(1) lookup
14-
credentials := make(map[string]string, len(users))
15-
for _, user := range users {
16-
credentials[user.Username] = user.Password
17-
}
11+
// CacheAuth creates a middleware that validates HTTP Basic Authentication
12+
func CacheAuth(auth config.AuthConfig, requireWriter bool) gin.HandlerFunc {
1813

1914
return func(c *gin.Context) {
2015
username, password, ok := c.Request.BasicAuth()
@@ -24,22 +19,23 @@ func BasicAuth(users []config.UserAuth) gin.HandlerFunc {
2419
return
2520
}
2621

27-
expectedPassword, userExists := credentials[username]
28-
if !userExists {
22+
// Check credentials
23+
isReader := username == auth.Reader.Username &&
24+
subtle.ConstantTimeCompare([]byte(password), []byte(auth.Reader.Password)) == 1
25+
isWriter := username == auth.Writer.Username &&
26+
subtle.ConstantTimeCompare([]byte(password), []byte(auth.Writer.Password)) == 1
27+
28+
if !isReader && !isWriter {
2929
c.Header("WWW-Authenticate", `Basic realm="Gradle Build Cache"`)
3030
c.AbortWithStatus(http.StatusUnauthorized)
3131
return
3232
}
3333

34-
// Use constant-time comparison to prevent timing attacks
35-
if subtle.ConstantTimeCompare([]byte(password), []byte(expectedPassword)) != 1 {
36-
c.Header("WWW-Authenticate", `Basic realm="Gradle Build Cache"`)
37-
c.AbortWithStatus(http.StatusUnauthorized)
34+
if requireWriter && !isWriter {
35+
c.AbortWithStatus(http.StatusForbidden)
3836
return
3937
}
4038

41-
// Store username in context for logging/metrics
42-
c.Set("username", username)
4339
c.Next()
4440
}
4541
}

src/internal/server/server.go

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -88,13 +88,19 @@ func (s *Server) setupRoutes() {
8888

8989
// Create cache group with optional auth
9090
cacheGroup := s.router.Group("/cache")
91-
if s.cfg.Auth.Enabled {
92-
cacheGroup.Use(middleware.BasicAuth(s.cfg.Auth.Users))
93-
}
9491

95-
cacheGroup.GET("/:key", cacheHandler.Get)
96-
cacheGroup.PUT("/:key", cacheHandler.Put)
97-
cacheGroup.HEAD("/:key", cacheHandler.Head)
92+
cacheGroup.GET("/:key", s.cacheAuth(false), cacheHandler.Get)
93+
cacheGroup.HEAD("/:key", s.cacheAuth(false), cacheHandler.Head)
94+
cacheGroup.PUT("/:key", s.cacheAuth(true), cacheHandler.Put)
95+
}
96+
97+
func (s *Server) cacheAuth(requireWriter bool) gin.HandlerFunc {
98+
if !s.cfg.Auth.Enabled {
99+
return func(c *gin.Context) {
100+
c.Next()
101+
}
102+
}
103+
return middleware.CacheAuth(s.cfg.Auth, requireWriter)
98104
}
99105

100106
// handlePing is a simple health check endpoint.

0 commit comments

Comments
 (0)